diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 106e72e..578dba6 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4243,4 +4243,13 @@ target_link_libraries(step595_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step596_test tests/step596_test.cpp) +target_include_directories(step596_test PRIVATE src) +target_link_libraries(step596_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/EscalationAuditLedger.h b/editor/src/EscalationAuditLedger.h new file mode 100644 index 0000000..7ca17ef --- /dev/null +++ b/editor/src/EscalationAuditLedger.h @@ -0,0 +1,74 @@ +#pragma once +// Step 596: Escalation Audit Ledger + +#include "ApprovalEscalationPlanner.h" + +#include +#include +#include + +struct EscalationAuditEntry { + std::string entryId; + std::string operation; + GuardrailDecision decision = GuardrailDecision::RequireReview; + EscalationLevel level = EscalationLevel::Reviewer; + std::string actor; + std::string outcome; + int riskScore = 0; +}; + +class EscalationAuditLedger { +public: + bool record(const EscalationAuditEntry& entry, std::string* error) { + if (!error) return false; + error->clear(); + if (entry.entryId.empty()) return fail(error, "entry_id_missing"); + if (entry.operation.empty()) return fail(error, "operation_missing"); + if (entry.actor.empty()) return fail(error, "actor_missing"); + if (entry.outcome.empty()) return fail(error, "outcome_missing"); + if (entry.riskScore < 0 || entry.riskScore > 100) return fail(error, "risk_score_invalid"); + if (entriesById_.count(entry.entryId) != 0) return fail(error, "entry_duplicate"); + entriesById_[entry.entryId] = entry; + order_.push_back(entry.entryId); + return true; + } + + std::vector entries() const { + std::vector out; + for (const auto& id : order_) out.push_back(entriesById_.at(id)); + return out; + } + + std::vector byActor(const std::string& actor) const { + std::vector out; + for (const auto& id : order_) { + const auto& e = entriesById_.at(id); + if (actor.empty() || e.actor == actor) out.push_back(e); + } + return out; + } + + int averageRiskScore() const { + if (order_.empty()) return 0; + int sum = 0; + for (const auto& id : order_) sum += entriesById_.at(id).riskScore; + return sum / static_cast(order_.size()); + } + + int deniedCount() const { + int count = 0; + for (const auto& id : order_) { + if (entriesById_.at(id).decision == GuardrailDecision::Deny) ++count; + } + return count; + } + +private: + std::map entriesById_; + std::vector order_; + + static bool fail(std::string* error, const char* code) { + *error = code; + return false; + } +}; diff --git a/editor/tests/step596_test.cpp b/editor/tests/step596_test.cpp new file mode 100644 index 0000000..6193d74 --- /dev/null +++ b/editor/tests/step596_test.cpp @@ -0,0 +1,166 @@ +// Step 596: Escalation Audit Ledger (12 tests) + +#include "EscalationAuditLedger.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 EscalationAuditEntry entry(const std::string& id, + const std::string& actor, + GuardrailDecision decision, + int risk) { + EscalationAuditEntry e; + e.entryId = id; + e.operation = "deploy prod"; + e.decision = decision; + e.level = EscalationLevel::Reviewer; + e.actor = actor; + e.outcome = "approved"; + e.riskScore = risk; + return e; +} + +void test_record_success() { + TEST(record_success); + EscalationAuditLedger l; + std::string error; + CHECK(l.record(entry("e1", "alice", GuardrailDecision::Allow, 20), &error), "record should succeed"); + CHECK(l.entries().size() == 1, "entry count mismatch"); + PASS(); +} + +void test_record_rejects_missing_entry_id() { + TEST(record_rejects_missing_entry_id); + EscalationAuditLedger l; + std::string error; + auto e = entry("", "alice", GuardrailDecision::Allow, 20); + CHECK(!l.record(e, &error), "record should fail"); + CHECK(error == "entry_id_missing", "wrong error"); + PASS(); +} + +void test_record_rejects_missing_operation() { + TEST(record_rejects_missing_operation); + EscalationAuditLedger l; + std::string error; + auto e = entry("e1", "alice", GuardrailDecision::Allow, 20); + e.operation.clear(); + CHECK(!l.record(e, &error), "record should fail"); + CHECK(error == "operation_missing", "wrong error"); + PASS(); +} + +void test_record_rejects_missing_actor() { + TEST(record_rejects_missing_actor); + EscalationAuditLedger l; + std::string error; + auto e = entry("e1", "", GuardrailDecision::Allow, 20); + CHECK(!l.record(e, &error), "record should fail"); + CHECK(error == "actor_missing", "wrong error"); + PASS(); +} + +void test_record_rejects_missing_outcome() { + TEST(record_rejects_missing_outcome); + EscalationAuditLedger l; + std::string error; + auto e = entry("e1", "alice", GuardrailDecision::Allow, 20); + e.outcome.clear(); + CHECK(!l.record(e, &error), "record should fail"); + CHECK(error == "outcome_missing", "wrong error"); + PASS(); +} + +void test_record_rejects_invalid_risk_score() { + TEST(record_rejects_invalid_risk_score); + EscalationAuditLedger l; + std::string error; + auto e = entry("e1", "alice", GuardrailDecision::Allow, 120); + CHECK(!l.record(e, &error), "record should fail"); + CHECK(error == "risk_score_invalid", "wrong error"); + PASS(); +} + +void test_record_rejects_duplicate_entry_id() { + TEST(record_rejects_duplicate_entry_id); + EscalationAuditLedger l; + std::string error; + CHECK(l.record(entry("e1", "alice", GuardrailDecision::Allow, 20), &error), "first record failed"); + CHECK(!l.record(entry("e1", "alice", GuardrailDecision::Allow, 20), &error), "duplicate should fail"); + CHECK(error == "entry_duplicate", "wrong error"); + PASS(); +} + +void test_entries_preserve_insertion_order() { + TEST(entries_preserve_insertion_order); + EscalationAuditLedger l; + std::string error; + CHECK(l.record(entry("e1", "alice", GuardrailDecision::Allow, 20), &error), "e1 failed"); + CHECK(l.record(entry("e2", "bob", GuardrailDecision::Allow, 25), &error), "e2 failed"); + auto all = l.entries(); + CHECK(all[0].entryId == "e1", "first order mismatch"); + CHECK(all[1].entryId == "e2", "second order mismatch"); + PASS(); +} + +void test_by_actor_filters_entries() { + TEST(by_actor_filters_entries); + EscalationAuditLedger l; + std::string error; + CHECK(l.record(entry("e1", "alice", GuardrailDecision::Allow, 20), &error), "e1 failed"); + CHECK(l.record(entry("e2", "bob", GuardrailDecision::Allow, 25), &error), "e2 failed"); + CHECK(l.byActor("alice").size() == 1, "actor filter mismatch"); + PASS(); +} + +void test_average_risk_score_computed() { + TEST(average_risk_score_computed); + EscalationAuditLedger l; + std::string error; + CHECK(l.record(entry("e1", "alice", GuardrailDecision::Allow, 20), &error), "e1 failed"); + CHECK(l.record(entry("e2", "bob", GuardrailDecision::Allow, 40), &error), "e2 failed"); + CHECK(l.averageRiskScore() == 30, "avg risk mismatch"); + PASS(); +} + +void test_average_risk_zero_when_empty() { + TEST(average_risk_zero_when_empty); + EscalationAuditLedger l; + CHECK(l.averageRiskScore() == 0, "empty avg risk should be zero"); + PASS(); +} + +void test_denied_count_tracks_deny_decisions() { + TEST(denied_count_tracks_deny_decisions); + EscalationAuditLedger l; + std::string error; + CHECK(l.record(entry("e1", "alice", GuardrailDecision::Deny, 95), &error), "e1 failed"); + CHECK(l.record(entry("e2", "bob", GuardrailDecision::Allow, 20), &error), "e2 failed"); + CHECK(l.deniedCount() == 1, "denied count mismatch"); + PASS(); +} + +int main() { + std::cout << "Step 596: Escalation Audit Ledger\n"; + + test_record_success(); // 1 + test_record_rejects_missing_entry_id(); // 2 + test_record_rejects_missing_operation(); // 3 + test_record_rejects_missing_actor(); // 4 + test_record_rejects_missing_outcome(); // 5 + test_record_rejects_invalid_risk_score(); // 6 + test_record_rejects_duplicate_entry_id(); // 7 + test_entries_preserve_insertion_order(); // 8 + test_by_actor_filters_entries(); // 9 + test_average_risk_score_computed(); // 10 + test_average_risk_zero_when_empty(); // 11 + test_denied_count_tracks_deny_decisions(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 478ff4c..f16a38f 100644 --- a/progress.md +++ b/progress.md @@ -11407,3 +11407,36 @@ roles, execution eligibility, and normalized risk scoring. - `editor/src/ApprovalEscalationPlanner.h` within header-size limit (`68` <= `600`) - `editor/tests/step595_test.cpp` within test-file size guidance (`149` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 596: Escalation Audit Ledger +**Status:** PASS (12/12 tests) + +Implements escalation audit logging for policy decisions, actors, outcomes, and +risk tracking across approval flows. + +**Files added:** +- `editor/src/EscalationAuditLedger.h` - audit ledger module: + - escalation entry recording with validation guards + - insertion-order audit retrieval + - actor-filtered audit query + - average risk and deny-count aggregation +- `editor/tests/step596_test.cpp` - 12 tests covering: + - record success/failure behavior + - duplicate/id/field/risk validation behavior + - insertion-order behavior + - actor filtering behavior + - aggregate metric behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step596_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step596_test step595_test` - PASS +- `./editor/build-native/step596_test` - PASS (12/12) +- `./editor/build-native/step595_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/EscalationAuditLedger.h` within header-size limit (`74` <= `600`) +- `editor/tests/step596_test.cpp` within test-file size guidance (`166` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`