diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 0377e5f..453e96c 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3802,4 +3802,13 @@ target_link_libraries(step546_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step547_test tests/step547_test.cpp) +target_include_directories(step547_test PRIVATE src) +target_link_libraries(step547_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/CostPolicyGuard.h b/editor/src/CostPolicyGuard.h new file mode 100644 index 0000000..e91a879 --- /dev/null +++ b/editor/src/CostPolicyGuard.h @@ -0,0 +1,60 @@ +#pragma once +// Step 547: Cost Policy Guard + +#include +#include + +struct CostPolicyInput { + int stepTokens = 0; + int workflowTokens = 0; + int stepTokenCeiling = 0; + int workflowTokenCeiling = 0; + bool hasRationale = false; + bool hasEscalationApproval = false; +}; + +struct CostPolicyResult { + bool allowed = false; + bool requiresEscalation = false; + std::vector violations; + std::string action = "allow"; // allow/request_rationale/escalate/block +}; + +class CostPolicyGuard { +public: + static CostPolicyResult evaluate(const CostPolicyInput& input) { + CostPolicyResult out; + + const bool stepOver = input.stepTokenCeiling > 0 && input.stepTokens > input.stepTokenCeiling; + const bool workflowOver = input.workflowTokenCeiling > 0 && + input.workflowTokens > input.workflowTokenCeiling; + + if (!stepOver && !workflowOver) { + out.allowed = true; + out.action = "allow"; + return out; + } + + if (stepOver) out.violations.push_back("step_token_ceiling_exceeded"); + if (workflowOver) out.violations.push_back("workflow_token_ceiling_exceeded"); + + if (!input.hasRationale) { + out.allowed = false; + out.action = "request_rationale"; + return out; + } + + // With rationale, require explicit escalation approval for overage execution. + if (!input.hasEscalationApproval) { + out.allowed = false; + out.requiresEscalation = true; + out.action = "escalate"; + return out; + } + + out.allowed = true; + out.requiresEscalation = true; + out.action = "allow"; + return out; + } +}; diff --git a/editor/tests/step547_test.cpp b/editor/tests/step547_test.cpp new file mode 100644 index 0000000..4aa6fe6 --- /dev/null +++ b/editor/tests/step547_test.cpp @@ -0,0 +1,132 @@ +// Step 547: Cost Policy Guard (12 tests) + +#include "CostPolicyGuard.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 bool hasViolation(const CostPolicyResult& r, const std::string& v) { + for (const auto& x : r.violations) if (x == v) return true; + return false; +} + +void test_within_limits_allows_execution() { + TEST(within_limits_allows_execution); + auto r = CostPolicyGuard::evaluate({100, 500, 200, 1000, false, false}); + CHECK(r.allowed, "within limits should allow"); + CHECK(r.action == "allow", "action should be allow"); + PASS(); +} + +void test_step_ceiling_exceeded_without_rationale_requests_rationale() { + TEST(step_ceiling_exceeded_without_rationale_requests_rationale); + auto r = CostPolicyGuard::evaluate({250, 500, 200, 1000, false, false}); + CHECK(!r.allowed, "should not allow without rationale"); + CHECK(r.action == "request_rationale", "action mismatch"); + CHECK(hasViolation(r, "step_token_ceiling_exceeded"), "step violation missing"); + PASS(); +} + +void test_workflow_ceiling_exceeded_without_rationale_requests_rationale() { + TEST(workflow_ceiling_exceeded_without_rationale_requests_rationale); + auto r = CostPolicyGuard::evaluate({150, 1200, 200, 1000, false, false}); + CHECK(!r.allowed, "should not allow without rationale"); + CHECK(r.action == "request_rationale", "action mismatch"); + CHECK(hasViolation(r, "workflow_token_ceiling_exceeded"), "workflow violation missing"); + PASS(); +} + +void test_both_ceilings_exceeded_collects_both_violations() { + TEST(both_ceilings_exceeded_collects_both_violations); + auto r = CostPolicyGuard::evaluate({250, 1200, 200, 1000, false, false}); + CHECK(hasViolation(r, "step_token_ceiling_exceeded"), "step violation missing"); + CHECK(hasViolation(r, "workflow_token_ceiling_exceeded"), "workflow violation missing"); + PASS(); +} + +void test_with_rationale_but_no_approval_requires_escalation() { + TEST(with_rationale_but_no_approval_requires_escalation); + auto r = CostPolicyGuard::evaluate({250, 500, 200, 1000, true, false}); + CHECK(!r.allowed, "should not allow without approval"); + CHECK(r.requiresEscalation, "escalation should be required"); + CHECK(r.action == "escalate", "action should be escalate"); + PASS(); +} + +void test_with_rationale_and_approval_allows_with_escalation_flag() { + TEST(with_rationale_and_approval_allows_with_escalation_flag); + auto r = CostPolicyGuard::evaluate({250, 500, 200, 1000, true, true}); + CHECK(r.allowed, "approved escalation should allow"); + CHECK(r.requiresEscalation, "escalation flag should remain true"); + PASS(); +} + +void test_workflow_overage_with_rationale_and_approval_allows() { + TEST(workflow_overage_with_rationale_and_approval_allows); + auto r = CostPolicyGuard::evaluate({150, 1500, 200, 1000, true, true}); + CHECK(r.allowed, "approved workflow overage should allow"); + CHECK(hasViolation(r, "workflow_token_ceiling_exceeded"), "workflow violation should be tracked"); + PASS(); +} + +void test_equal_to_step_ceiling_is_allowed() { + TEST(equal_to_step_ceiling_is_allowed); + auto r = CostPolicyGuard::evaluate({200, 500, 200, 1000, false, false}); + CHECK(r.allowed, "equal to step ceiling should be allowed"); + PASS(); +} + +void test_equal_to_workflow_ceiling_is_allowed() { + TEST(equal_to_workflow_ceiling_is_allowed); + auto r = CostPolicyGuard::evaluate({150, 1000, 200, 1000, false, false}); + CHECK(r.allowed, "equal to workflow ceiling should be allowed"); + PASS(); +} + +void test_zero_ceiling_disables_check() { + TEST(zero_ceiling_disables_check); + auto r = CostPolicyGuard::evaluate({10000, 10000, 0, 0, false, false}); + CHECK(r.allowed, "zero ceilings should disable checks"); + PASS(); +} + +void test_negative_tokens_do_not_trigger_violations() { + TEST(negative_tokens_do_not_trigger_violations); + auto r = CostPolicyGuard::evaluate({-10, -50, 200, 1000, false, false}); + CHECK(r.allowed, "negative token values should not violate ceilings"); + PASS(); +} + +void test_escalation_not_required_when_within_limits_even_with_rationale() { + TEST(escalation_not_required_when_within_limits_even_with_rationale); + auto r = CostPolicyGuard::evaluate({100, 500, 200, 1000, true, true}); + CHECK(r.allowed, "within limits should allow"); + CHECK(!r.requiresEscalation, "escalation should not be required"); + CHECK(r.violations.empty(), "no violations expected"); + PASS(); +} + +int main() { + std::cout << "Step 547: Cost Policy Guard\n"; + + test_within_limits_allows_execution(); // 1 + test_step_ceiling_exceeded_without_rationale_requests_rationale(); // 2 + test_workflow_ceiling_exceeded_without_rationale_requests_rationale();// 3 + test_both_ceilings_exceeded_collects_both_violations(); // 4 + test_with_rationale_but_no_approval_requires_escalation(); // 5 + test_with_rationale_and_approval_allows_with_escalation_flag(); // 6 + test_workflow_overage_with_rationale_and_approval_allows(); // 7 + test_equal_to_step_ceiling_is_allowed(); // 8 + test_equal_to_workflow_ceiling_is_allowed(); // 9 + test_zero_ceiling_disables_check(); // 10 + test_negative_tokens_do_not_trigger_violations(); // 11 + test_escalation_not_required_when_within_limits_even_with_rationale();// 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 5d3d09a..9db890f 100644 --- a/progress.md +++ b/progress.md @@ -9470,3 +9470,40 @@ penalties for repeated post-apply failures. - `editor/src/ConstrainedConfidenceCalibrator.h` within header-size limit (`97` <= `600`) - `editor/tests/step546_test.cpp` within test-file size guidance (`143` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 547: Cost Policy Guard +**Status:** PASS (12/12 tests) + +Implements cost policy enforcement for constrained orchestration with per-step +and per-workflow token ceilings, rationale requirements, and escalation approval +controls for overage execution. + +**Files added:** +- `editor/src/CostPolicyGuard.h` - cost policy enforcement module: + - step/workflow ceiling checks + - violation capture for exceeded ceilings + - rationale-required flow for policy overages + - escalation-approval gate for over-budget execution + - deterministic action outputs (`allow`/`request_rationale`/`escalate`) +- `editor/tests/step547_test.cpp` - 12 tests covering: + - within-limit allow behavior + - step/workflow/both overage violation behavior + - rationale and escalation approval behavior + - equal-to-ceiling boundary behavior + - zero-ceiling disabled-check behavior + - negative-token edge behavior + - no-escalation behavior when within limits + +**Files modified:** +- `editor/CMakeLists.txt` - `step547_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step547_test step546_test` - PASS +- `./editor/build-native/step547_test` - PASS (12/12) +- `./editor/build-native/step546_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/CostPolicyGuard.h` within header-size limit (`60` <= `600`) +- `editor/tests/step547_test.cpp` within test-file size guidance (`132` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`