From da5a01304f2b659180431c0cdde278ae8f07a90a Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 12:08:32 -0700 Subject: [PATCH] Add step 612 canary promotion judge --- editor/CMakeLists.txt | 9 ++ editor/src/CanaryPromotionJudge.h | 72 +++++++++++++++ editor/tests/step612_test.cpp | 149 ++++++++++++++++++++++++++++++ progress.md | 30 ++++++ 4 files changed, 260 insertions(+) create mode 100644 editor/src/CanaryPromotionJudge.h create mode 100644 editor/tests/step612_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 8854964..1beb909 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4387,4 +4387,13 @@ target_link_libraries(step611_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step612_test tests/step612_test.cpp) +target_include_directories(step612_test PRIVATE src) +target_link_libraries(step612_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/CanaryPromotionJudge.h b/editor/src/CanaryPromotionJudge.h new file mode 100644 index 0000000..202e193 --- /dev/null +++ b/editor/src/CanaryPromotionJudge.h @@ -0,0 +1,72 @@ +#pragma once +// Step 612: Canary Promotion Judge + +#include +#include +#include + +#include "ValidationErrorUtil.h" + +struct CanaryAssessment { + std::string canaryId; + int requestSuccessPercent = 0; + int latencyP95Ms = 0; + int errorBudgetBurn = 0; + bool manualOverride = false; +}; + +class CanaryPromotionJudge { +public: + bool record(const CanaryAssessment& assessment, std::string* error) { + if (!error) return false; + error->clear(); + if (assessment.canaryId.empty()) return failWith(error, "canary_id_missing"); + if (assessment.requestSuccessPercent < 0 || assessment.requestSuccessPercent > 100) { + return failWith(error, "success_percent_invalid"); + } + if (assessment.latencyP95Ms < 0) return failWith(error, "latency_invalid"); + if (assessment.errorBudgetBurn < 0) return failWith(error, "error_budget_burn_invalid"); + if (items_.count(assessment.canaryId) == 0) order_.push_back(assessment.canaryId); + items_[assessment.canaryId] = assessment; + return true; + } + + bool promotable(const std::string& canaryId, std::string* error) const { + if (!error) return false; + error->clear(); + auto it = items_.find(canaryId); + if (it == items_.end()) return failWith(error, "canary_missing"); + const auto& a = it->second; + if (a.manualOverride) return true; + return a.requestSuccessPercent >= 99 && a.latencyP95Ms <= 300 && a.errorBudgetBurn <= 100; + } + + int riskScore(const std::string& canaryId, std::string* error) const { + if (!error) return 0; + error->clear(); + auto it = items_.find(canaryId); + if (it == items_.end()) { + failWith(error, "canary_missing"); + return 100; + } + const auto& a = it->second; + int score = 0; + if (a.requestSuccessPercent < 99) score += (99 - a.requestSuccessPercent) * 5; + if (a.latencyP95Ms > 300) score += (a.latencyP95Ms - 300) / 5; + if (a.errorBudgetBurn > 100) score += (a.errorBudgetBurn - 100) / 2; + if (a.manualOverride) score = 0; + if (score > 100) return 100; + return score; + } + + int promotableCount() const { + int count = 0; + std::string error; + for (const auto& id : order_) if (promotable(id, &error)) ++count; + return count; + } + +private: + std::map items_; + std::vector order_; +}; diff --git a/editor/tests/step612_test.cpp b/editor/tests/step612_test.cpp new file mode 100644 index 0000000..db2918c --- /dev/null +++ b/editor/tests/step612_test.cpp @@ -0,0 +1,149 @@ +// Step 612: Canary Promotion Judge (12 tests) + +#include "CanaryPromotionJudge.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 CanaryAssessment assess(const std::string& id, + int success, + int p95, + int burn, + bool overrideFlag = false) { + return {id, success, p95, burn, overrideFlag}; +} + +void test_record_success() { + TEST(record_success); + CanaryPromotionJudge judge; + std::string error; + CHECK(judge.record(assess("c1", 99, 250, 80), &error), "record should succeed"); + CHECK(judge.promotableCount() == 1, "promotable count mismatch"); + PASS(); +} + +void test_record_rejects_missing_canary_id() { + TEST(record_rejects_missing_canary_id); + CanaryPromotionJudge judge; + std::string error; + CHECK(!judge.record(assess("", 99, 250, 80), &error), "record should fail"); + CHECK(error == "canary_id_missing", "wrong error"); + PASS(); +} + +void test_record_rejects_invalid_success_percent() { + TEST(record_rejects_invalid_success_percent); + CanaryPromotionJudge judge; + std::string error; + CHECK(!judge.record(assess("c1", 101, 250, 80), &error), "record should fail"); + CHECK(error == "success_percent_invalid", "wrong error"); + PASS(); +} + +void test_record_rejects_negative_latency() { + TEST(record_rejects_negative_latency); + CanaryPromotionJudge judge; + std::string error; + CHECK(!judge.record(assess("c1", 99, -1, 80), &error), "record should fail"); + CHECK(error == "latency_invalid", "wrong error"); + PASS(); +} + +void test_record_rejects_negative_error_budget_burn() { + TEST(record_rejects_negative_error_budget_burn); + CanaryPromotionJudge judge; + std::string error; + CHECK(!judge.record(assess("c1", 99, 200, -1), &error), "record should fail"); + CHECK(error == "error_budget_burn_invalid", "wrong error"); + PASS(); +} + +void test_promotable_true_when_thresholds_met() { + TEST(promotable_true_when_thresholds_met); + CanaryPromotionJudge judge; + std::string error; + CHECK(judge.record(assess("c1", 99, 300, 100), &error), "record failed"); + CHECK(judge.promotable("c1", &error), "canary should be promotable"); + PASS(); +} + +void test_promotable_false_when_below_thresholds() { + TEST(promotable_false_when_below_thresholds); + CanaryPromotionJudge judge; + std::string error; + CHECK(judge.record(assess("c1", 97, 450, 120), &error), "record failed"); + CHECK(!judge.promotable("c1", &error), "canary should be blocked"); + PASS(); +} + +void test_promotable_true_with_manual_override() { + TEST(promotable_true_with_manual_override); + CanaryPromotionJudge judge; + std::string error; + CHECK(judge.record(assess("c1", 90, 500, 200, true), &error), "record failed"); + CHECK(judge.promotable("c1", &error), "override should allow promotion"); + PASS(); +} + +void test_promotable_rejects_missing_canary() { + TEST(promotable_rejects_missing_canary); + CanaryPromotionJudge judge; + std::string error; + CHECK(!judge.promotable("missing", &error), "query should fail"); + CHECK(error == "canary_missing", "wrong error"); + PASS(); +} + +void test_risk_score_zero_when_healthy() { + TEST(risk_score_zero_when_healthy); + CanaryPromotionJudge judge; + std::string error; + CHECK(judge.record(assess("c1", 99, 250, 80), &error), "record failed"); + CHECK(judge.riskScore("c1", &error) == 0, "healthy score should be zero"); + PASS(); +} + +void test_risk_score_increases_for_degradation() { + TEST(risk_score_increases_for_degradation); + CanaryPromotionJudge judge; + std::string error; + CHECK(judge.record(assess("c1", 98, 320, 130), &error), "record failed"); + CHECK(judge.riskScore("c1", &error) > 0, "degraded score should be non-zero"); + PASS(); +} + +void test_promotable_count_counts_only_allowed_canaries() { + TEST(promotable_count_counts_only_allowed_canaries); + CanaryPromotionJudge judge; + std::string error; + CHECK(judge.record(assess("c1", 99, 250, 80), &error), "record c1 failed"); + CHECK(judge.record(assess("c2", 95, 500, 180), &error), "record c2 failed"); + CHECK(judge.record(assess("c3", 90, 500, 180, true), &error), "record c3 failed"); + CHECK(judge.promotableCount() == 2, "promotable count mismatch"); + PASS(); +} + +int main() { + std::cout << "Step 612: Canary Promotion Judge\n"; + + test_record_success(); // 1 + test_record_rejects_missing_canary_id(); // 2 + test_record_rejects_invalid_success_percent(); // 3 + test_record_rejects_negative_latency(); // 4 + test_record_rejects_negative_error_budget_burn(); // 5 + test_promotable_true_when_thresholds_met(); // 6 + test_promotable_false_when_below_thresholds(); // 7 + test_promotable_true_with_manual_override(); // 8 + test_promotable_rejects_missing_canary(); // 9 + test_risk_score_zero_when_healthy(); // 10 + test_risk_score_increases_for_degradation(); // 11 + test_promotable_count_counts_only_allowed_canaries();// 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index b33e7aa..3dd78a9 100644 --- a/progress.md +++ b/progress.md @@ -11950,3 +11950,33 @@ calculation. - `editor/src/OnCallCoveragePlanner.h` within header-size limit (`78` <= `600`) - `editor/tests/step611_test.cpp` within test-file size guidance (`147` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 612: Canary Promotion Judge +**Status:** PASS (12/12 tests) + +Implements canary promotion decisions using success/latency/burn thresholds with +risk scoring and manual override support. + +**Files added:** +- `editor/src/CanaryPromotionJudge.h` - canary decision module: + - canary assessment upsert with validation guards + - promotion decision logic with manual override path + - risk score derivation and promotable-count summary +- `editor/tests/step612_test.cpp` - 12 tests covering: + - assessment record success/failure behavior + - promotable decision behavior and missing-canary handling + - risk score behavior and promotable count aggregation + +**Files modified:** +- `editor/CMakeLists.txt` - `step612_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step612_test step611_test` - PASS +- `./editor/build-native/step612_test` - PASS (12/12) +- `./editor/build-native/step611_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `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`