From d9bffde839010c53b2c4f2c5641b0f3b54a180c2 Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 11:34:31 -0700 Subject: [PATCH] Step 594: add policy guardrail catalog --- editor/CMakeLists.txt | 9 ++ editor/src/PolicyGuardrailCatalog.h | 75 +++++++++++++ editor/tests/step594_test.cpp | 158 ++++++++++++++++++++++++++++ progress.md | 34 ++++++ 4 files changed, 276 insertions(+) create mode 100644 editor/src/PolicyGuardrailCatalog.h create mode 100644 editor/tests/step594_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 4525772..d0068e8 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4225,4 +4225,13 @@ target_link_libraries(step593_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step594_test tests/step594_test.cpp) +target_include_directories(step594_test PRIVATE src) +target_link_libraries(step594_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/PolicyGuardrailCatalog.h b/editor/src/PolicyGuardrailCatalog.h new file mode 100644 index 0000000..ab2b030 --- /dev/null +++ b/editor/src/PolicyGuardrailCatalog.h @@ -0,0 +1,75 @@ +#pragma once +// Step 594: Policy Guardrail Catalog + +#include +#include +#include + +enum class GuardrailDecision { + Allow, + RequireReview, + Deny +}; + +struct GuardrailRule { + std::string ruleId; + std::string operationPrefix; + GuardrailDecision decision = GuardrailDecision::RequireReview; + std::string reason; +}; + +struct GuardrailDecisionResult { + GuardrailDecision decision = GuardrailDecision::RequireReview; + std::string matchedRuleId; + std::string reason; +}; + +class PolicyGuardrailCatalog { +public: + bool addRule(const GuardrailRule& rule, std::string* error) { + if (!error) return false; + error->clear(); + if (rule.ruleId.empty()) return fail(error, "rule_id_missing"); + if (rule.operationPrefix.empty()) return fail(error, "operation_prefix_missing"); + if (rule.reason.empty()) return fail(error, "rule_reason_missing"); + if (rules_.count(rule.ruleId) != 0) return fail(error, "rule_duplicate"); + rules_[rule.ruleId] = rule; + order_.push_back(rule.ruleId); + return true; + } + + GuardrailDecisionResult evaluate(const std::string& operation) const { + GuardrailDecisionResult result; + result.decision = GuardrailDecision::RequireReview; + result.reason = "default_review_required"; + for (const auto& id : order_) { + const auto& rule = rules_.at(id); + if (startsWith(operation, rule.operationPrefix)) { + result.decision = rule.decision; + result.matchedRuleId = rule.ruleId; + result.reason = rule.reason; + return result; + } + } + return result; + } + + std::vector rules() const { + std::vector out; + for (const auto& id : order_) out.push_back(rules_.at(id)); + return out; + } + +private: + std::map rules_; + std::vector order_; + + static bool fail(std::string* error, const char* code) { + *error = code; + return false; + } + + static bool startsWith(const std::string& text, const std::string& prefix) { + return text.rfind(prefix, 0) == 0; + } +}; diff --git a/editor/tests/step594_test.cpp b/editor/tests/step594_test.cpp new file mode 100644 index 0000000..d7d3c56 --- /dev/null +++ b/editor/tests/step594_test.cpp @@ -0,0 +1,158 @@ +// Step 594: Policy Guardrail Catalog (12 tests) + +#include "PolicyGuardrailCatalog.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 GuardrailRule rule(const std::string& id, + const std::string& prefix, + GuardrailDecision d, + const std::string& reason) { + return {id, prefix, d, reason}; +} + +void test_add_rule_success() { + TEST(add_rule_success); + PolicyGuardrailCatalog c; + std::string error; + CHECK(c.addRule(rule("r1", "git push", GuardrailDecision::Allow, "safe push"), &error), "add should succeed"); + CHECK(c.rules().size() == 1, "rule count mismatch"); + PASS(); +} + +void test_add_rule_rejects_missing_id() { + TEST(add_rule_rejects_missing_id); + PolicyGuardrailCatalog c; + std::string error; + CHECK(!c.addRule(rule("", "git push", GuardrailDecision::Allow, "safe"), &error), "add should fail"); + CHECK(error == "rule_id_missing", "wrong error"); + PASS(); +} + +void test_add_rule_rejects_missing_prefix() { + TEST(add_rule_rejects_missing_prefix); + PolicyGuardrailCatalog c; + std::string error; + CHECK(!c.addRule(rule("r1", "", GuardrailDecision::Allow, "safe"), &error), "add should fail"); + CHECK(error == "operation_prefix_missing", "wrong error"); + PASS(); +} + +void test_add_rule_rejects_missing_reason() { + TEST(add_rule_rejects_missing_reason); + PolicyGuardrailCatalog c; + std::string error; + CHECK(!c.addRule(rule("r1", "git push", GuardrailDecision::Allow, ""), &error), "add should fail"); + CHECK(error == "rule_reason_missing", "wrong error"); + PASS(); +} + +void test_add_rule_rejects_duplicate_id() { + TEST(add_rule_rejects_duplicate_id); + PolicyGuardrailCatalog c; + std::string error; + CHECK(c.addRule(rule("r1", "git push", GuardrailDecision::Allow, "safe"), &error), "first add failed"); + CHECK(!c.addRule(rule("r1", "git pull", GuardrailDecision::Allow, "safe"), &error), "duplicate should fail"); + CHECK(error == "rule_duplicate", "wrong error"); + PASS(); +} + +void test_evaluate_matches_allow_rule() { + TEST(evaluate_matches_allow_rule); + PolicyGuardrailCatalog c; + std::string error; + CHECK(c.addRule(rule("r1", "git push", GuardrailDecision::Allow, "safe push"), &error), "add failed"); + auto d = c.evaluate("git push origin main"); + CHECK(d.decision == GuardrailDecision::Allow, "decision mismatch"); + CHECK(d.matchedRuleId == "r1", "rule id mismatch"); + PASS(); +} + +void test_evaluate_matches_require_review_rule() { + TEST(evaluate_matches_require_review_rule); + PolicyGuardrailCatalog c; + std::string error; + CHECK(c.addRule(rule("r1", "npm publish", GuardrailDecision::RequireReview, "release review"), &error), "add failed"); + auto d = c.evaluate("npm publish"); + CHECK(d.decision == GuardrailDecision::RequireReview, "decision mismatch"); + CHECK(d.reason == "release review", "reason mismatch"); + PASS(); +} + +void test_evaluate_matches_deny_rule() { + TEST(evaluate_matches_deny_rule); + PolicyGuardrailCatalog c; + std::string error; + CHECK(c.addRule(rule("r1", "rm -rf", GuardrailDecision::Deny, "destructive"), &error), "add failed"); + auto d = c.evaluate("rm -rf /tmp/x"); + CHECK(d.decision == GuardrailDecision::Deny, "decision mismatch"); + PASS(); +} + +void test_evaluate_defaults_to_review_when_no_match() { + TEST(evaluate_defaults_to_review_when_no_match); + PolicyGuardrailCatalog c; + auto d = c.evaluate("unknown command"); + CHECK(d.decision == GuardrailDecision::RequireReview, "default decision mismatch"); + CHECK(d.reason == "default_review_required", "default reason mismatch"); + PASS(); +} + +void test_evaluate_uses_first_matching_rule_order() { + TEST(evaluate_uses_first_matching_rule_order); + PolicyGuardrailCatalog c; + std::string error; + CHECK(c.addRule(rule("r1", "git", GuardrailDecision::RequireReview, "generic"), &error), "add r1 failed"); + CHECK(c.addRule(rule("r2", "git push", GuardrailDecision::Allow, "specific"), &error), "add r2 failed"); + auto d = c.evaluate("git push origin"); + CHECK(d.matchedRuleId == "r1", "first-match order should apply"); + PASS(); +} + +void test_rules_return_in_insertion_order() { + TEST(rules_return_in_insertion_order); + PolicyGuardrailCatalog c; + std::string error; + CHECK(c.addRule(rule("r1", "a", GuardrailDecision::Allow, "a"), &error), "add r1 failed"); + CHECK(c.addRule(rule("r2", "b", GuardrailDecision::Allow, "b"), &error), "add r2 failed"); + auto rules = c.rules(); + CHECK(rules[0].ruleId == "r1", "first order mismatch"); + CHECK(rules[1].ruleId == "r2", "second order mismatch"); + PASS(); +} + +void test_reason_propagates_from_matching_rule() { + TEST(reason_propagates_from_matching_rule); + PolicyGuardrailCatalog c; + std::string error; + CHECK(c.addRule(rule("r1", "deploy", GuardrailDecision::RequireReview, "production safety check"), &error), "add failed"); + auto d = c.evaluate("deploy prod"); + CHECK(d.reason == "production safety check", "reason should propagate"); + PASS(); +} + +int main() { + std::cout << "Step 594: Policy Guardrail Catalog\n"; + + test_add_rule_success(); // 1 + test_add_rule_rejects_missing_id(); // 2 + test_add_rule_rejects_missing_prefix(); // 3 + test_add_rule_rejects_missing_reason(); // 4 + test_add_rule_rejects_duplicate_id(); // 5 + test_evaluate_matches_allow_rule(); // 6 + test_evaluate_matches_require_review_rule(); // 7 + test_evaluate_matches_deny_rule(); // 8 + test_evaluate_defaults_to_review_when_no_match();// 9 + test_evaluate_uses_first_matching_rule_order(); // 10 + test_rules_return_in_insertion_order(); // 11 + test_reason_propagates_from_matching_rule(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 46ac523..6f0dda5 100644 --- a/progress.md +++ b/progress.md @@ -11340,3 +11340,37 @@ validation logic across onboarding/workflow/capability modules. - `editor/src/WorkflowVisualizationV2.h` within header-size limit (`112` <= `600`) - `editor/src/CapabilityDiscoveryPanels.h` within header-size limit (`90` <= `600`) - Shared validation centralization reduces duplicated product-surface guard logic and remains aligned with `ARCHITECTURE.md` + +### Step 594: Policy Guardrail Catalog +**Status:** PASS (12/12 tests) + +Implements the first Sprint 34 guardrail catalog for policy-driven operation +allow/review/deny decisions with explainable rule matching. + +**Files added:** +- `editor/src/PolicyGuardrailCatalog.h` - guardrail catalog module: + - rule registration with validation and duplicate guards + - ordered prefix-based decision matching + - default require-review fallback policy + - matched-rule reason propagation +- `editor/tests/step594_test.cpp` - 12 tests covering: + - rule registration success/failure behavior + - allow/review/deny decision behavior + - default fallback behavior + - first-match ordering behavior + - reason propagation behavior + - insertion-order rule listing behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step594_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step594_test step593_test` - PASS +- `./editor/build-native/step594_test` - PASS (12/12) +- `./editor/build-native/step593_test` - PASS (8/8) regression coverage + +**Architecture gate check:** +- `editor/src/PolicyGuardrailCatalog.h` within header-size limit (`75` <= `600`) +- `editor/tests/step594_test.cpp` within test-file size guidance (`158` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`