Step 594: add policy guardrail catalog
This commit is contained in:
@@ -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)
|
||||
|
||||
75
editor/src/PolicyGuardrailCatalog.h
Normal file
75
editor/src/PolicyGuardrailCatalog.h
Normal file
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
// Step 594: Policy Guardrail Catalog
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<GuardrailRule> rules() const {
|
||||
std::vector<GuardrailRule> out;
|
||||
for (const auto& id : order_) out.push_back(rules_.at(id));
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::string, GuardrailRule> rules_;
|
||||
std::vector<std::string> 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;
|
||||
}
|
||||
};
|
||||
158
editor/tests/step594_test.cpp
Normal file
158
editor/tests/step594_test.cpp
Normal file
@@ -0,0 +1,158 @@
|
||||
// Step 594: Policy Guardrail Catalog (12 tests)
|
||||
|
||||
#include "PolicyGuardrailCatalog.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
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;
|
||||
}
|
||||
34
progress.md
34
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`
|
||||
|
||||
Reference in New Issue
Block a user