diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 1beb909..f70e198 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4396,4 +4396,13 @@ target_link_libraries(step612_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step613_test tests/step613_test.cpp) +target_include_directories(step613_test PRIVATE src) +target_link_libraries(step613_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/Sprint35OperationalReadiness.h b/editor/src/Sprint35OperationalReadiness.h new file mode 100644 index 0000000..cac0c44 --- /dev/null +++ b/editor/src/Sprint35OperationalReadiness.h @@ -0,0 +1,54 @@ +#pragma once +// Step 613: Sprint 35 Operational Readiness + +#include +#include +#include + +#include "ValidationErrorUtil.h" + +struct Sprint35ReadinessInput { + int freezeViolations = 0; + int highRiskDependencies = 0; + int onCallCoverageGaps = 0; + int blockedCanaries = 0; + int incompletePostmortems = 0; +}; + +class Sprint35OperationalReadiness { +public: + static bool validate(const Sprint35ReadinessInput& in, std::string* error) { + if (!error) return false; + error->clear(); + if (in.freezeViolations < 0) return failWith(error, "freeze_violations_invalid"); + if (in.highRiskDependencies < 0) return failWith(error, "high_risk_dependencies_invalid"); + if (in.onCallCoverageGaps < 0) return failWith(error, "oncall_gaps_invalid"); + if (in.blockedCanaries < 0) return failWith(error, "blocked_canaries_invalid"); + if (in.incompletePostmortems < 0) return failWith(error, "incomplete_postmortems_invalid"); + return true; + } + + static int readinessScore(const Sprint35ReadinessInput& in) { + int score = 100; + score -= in.freezeViolations * 12; + score -= in.highRiskDependencies * 8; + score -= in.onCallCoverageGaps; + score -= in.blockedCanaries * 15; + score -= in.incompletePostmortems * 10; + return std::max(0, std::min(100, score)); + } + + static std::vector blockers(const Sprint35ReadinessInput& in) { + std::vector out; + if (in.freezeViolations > 0) out.push_back("freeze_violations_present"); + if (in.highRiskDependencies > 2) out.push_back("dependency_risk_elevated"); + if (in.onCallCoverageGaps > 0) out.push_back("oncall_coverage_incomplete"); + if (in.blockedCanaries > 0) out.push_back("canary_promotions_blocked"); + if (in.incompletePostmortems > 0) out.push_back("postmortems_incomplete"); + return out; + } + + static bool releaseAllowed(const Sprint35ReadinessInput& in) { + return blockers(in).empty() && readinessScore(in) >= 90; + } +}; diff --git a/editor/tests/step613_test.cpp b/editor/tests/step613_test.cpp new file mode 100644 index 0000000..eb63645 --- /dev/null +++ b/editor/tests/step613_test.cpp @@ -0,0 +1,134 @@ +// Step 613: Sprint 35 Operational Readiness (12 tests) + +#include "Sprint35OperationalReadiness.h" + +#include + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {} + +static Sprint35ReadinessInput healthy() { + return {0, 0, 0, 0, 0}; +} + +void test_validate_success() { + TEST(validate_success); + std::string error; + CHECK(Sprint35OperationalReadiness::validate(healthy(), &error), "validate should pass"); + CHECK(error.empty(), "error should be empty"); + PASS(); +} + +void test_validate_rejects_negative_freeze_violations() { + TEST(validate_rejects_negative_freeze_violations); + auto in = healthy(); + in.freezeViolations = -1; + std::string error; + CHECK(!Sprint35OperationalReadiness::validate(in, &error), "validate should fail"); + CHECK(error == "freeze_violations_invalid", "wrong error"); + PASS(); +} + +void test_validate_rejects_negative_high_risk_dependencies() { + TEST(validate_rejects_negative_high_risk_dependencies); + auto in = healthy(); + in.highRiskDependencies = -1; + std::string error; + CHECK(!Sprint35OperationalReadiness::validate(in, &error), "validate should fail"); + CHECK(error == "high_risk_dependencies_invalid", "wrong error"); + PASS(); +} + +void test_validate_rejects_negative_oncall_gaps() { + TEST(validate_rejects_negative_oncall_gaps); + auto in = healthy(); + in.onCallCoverageGaps = -1; + std::string error; + CHECK(!Sprint35OperationalReadiness::validate(in, &error), "validate should fail"); + CHECK(error == "oncall_gaps_invalid", "wrong error"); + PASS(); +} + +void test_validate_rejects_negative_blocked_canaries() { + TEST(validate_rejects_negative_blocked_canaries); + auto in = healthy(); + in.blockedCanaries = -1; + std::string error; + CHECK(!Sprint35OperationalReadiness::validate(in, &error), "validate should fail"); + CHECK(error == "blocked_canaries_invalid", "wrong error"); + PASS(); +} + +void test_validate_rejects_negative_incomplete_postmortems() { + TEST(validate_rejects_negative_incomplete_postmortems); + auto in = healthy(); + in.incompletePostmortems = -1; + std::string error; + CHECK(!Sprint35OperationalReadiness::validate(in, &error), "validate should fail"); + CHECK(error == "incomplete_postmortems_invalid", "wrong error"); + PASS(); +} + +void test_readiness_score_full_when_healthy() { + TEST(readiness_score_full_when_healthy); + CHECK(Sprint35OperationalReadiness::readinessScore(healthy()) == 100, "healthy score should be 100"); + PASS(); +} + +void test_readiness_score_decreases_with_risks() { + TEST(readiness_score_decreases_with_risks); + auto in = Sprint35ReadinessInput{1, 1, 4, 1, 1}; + CHECK(Sprint35OperationalReadiness::readinessScore(in) < 100, "score should be reduced"); + PASS(); +} + +void test_readiness_score_clamps_to_zero() { + TEST(readiness_score_clamps_to_zero); + auto in = Sprint35ReadinessInput{10, 10, 40, 10, 10}; + CHECK(Sprint35OperationalReadiness::readinessScore(in) == 0, "score should clamp to zero"); + PASS(); +} + +void test_blockers_include_expected_flags() { + TEST(blockers_include_expected_flags); + auto in = Sprint35ReadinessInput{1, 3, 1, 1, 1}; + CHECK(Sprint35OperationalReadiness::blockers(in).size() == 5, "all blockers should be present"); + PASS(); +} + +void test_release_allowed_true_when_clean() { + TEST(release_allowed_true_when_clean); + CHECK(Sprint35OperationalReadiness::releaseAllowed(healthy()), "healthy should allow release"); + PASS(); +} + +void test_release_allowed_false_with_blockers() { + TEST(release_allowed_false_with_blockers); + auto in = healthy(); + in.blockedCanaries = 1; + CHECK(!Sprint35OperationalReadiness::releaseAllowed(in), "blocked canaries should block release"); + PASS(); +} + +int main() { + std::cout << "Step 613: Sprint 35 Operational Readiness\n"; + + test_validate_success(); // 1 + test_validate_rejects_negative_freeze_violations(); // 2 + test_validate_rejects_negative_high_risk_dependencies();// 3 + test_validate_rejects_negative_oncall_gaps(); // 4 + test_validate_rejects_negative_blocked_canaries(); // 5 + test_validate_rejects_negative_incomplete_postmortems();// 6 + test_readiness_score_full_when_healthy(); // 7 + test_readiness_score_decreases_with_risks(); // 8 + test_readiness_score_clamps_to_zero(); // 9 + test_blockers_include_expected_flags(); // 10 + test_release_allowed_true_when_clean(); // 11 + test_release_allowed_false_with_blockers(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 3dd78a9..54b3c23 100644 --- a/progress.md +++ b/progress.md @@ -11980,3 +11980,33 @@ risk scoring and manual override support. - `editor/src/CanaryPromotionJudge.h` within header-size limit (`72` <= `600`) - `editor/tests/step612_test.cpp` within test-file size guidance (`149` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 613: Sprint 35 Operational Readiness +**Status:** PASS (12/12 tests) + +Implements sprint-level operational readiness scoring and release gating over +freeze, dependency, coverage, canary, and postmortem signals. + +**Files added:** +- `editor/src/Sprint35OperationalReadiness.h` - sprint readiness module: + - readiness input validation for non-negative metrics + - weighted readiness score with clamp + - blocker derivation and release-allowed decision +- `editor/tests/step613_test.cpp` - 12 tests covering: + - validation success/failure behavior + - score degradation and clamp behavior + - blocker derivation and release gate behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step613_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step613_test step612_test` - PASS +- `./editor/build-native/step613_test` - PASS (12/12) +- `./editor/build-native/step612_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/Sprint35OperationalReadiness.h` within header-size limit (`54` <= `600`) +- `editor/tests/step613_test.cpp` within test-file size guidance (`134` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`