diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index b01f21e..25d95d9 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4351,4 +4351,13 @@ target_link_libraries(step607_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step608_test tests/step608_test.cpp) +target_include_directories(step608_test PRIVATE src) +target_link_libraries(step608_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/IncidentPostmortemLedger.h b/editor/src/IncidentPostmortemLedger.h new file mode 100644 index 0000000..3fc461e --- /dev/null +++ b/editor/src/IncidentPostmortemLedger.h @@ -0,0 +1,85 @@ +#pragma once +// Step 608: Incident Postmortem Ledger + +#include +#include +#include + +#include "ValidationErrorUtil.h" + +enum class PostmortemStatus { + Open, + Completed +}; + +struct PostmortemRecord { + std::string incidentId; + std::string owner; + int correctiveActions = 0; + int completedActions = 0; + PostmortemStatus status = PostmortemStatus::Open; +}; + +class IncidentPostmortemLedger { +public: + bool open(const PostmortemRecord& record, std::string* error) { + if (!error) return false; + error->clear(); + if (record.incidentId.empty()) return failWith(error, "incident_id_missing"); + if (record.owner.empty()) return failWith(error, "owner_missing"); + if (record.correctiveActions < 0) return failWith(error, "corrective_actions_invalid"); + if (record.completedActions < 0) return failWith(error, "completed_actions_invalid"); + if (record.completedActions > record.correctiveActions) return failWith(error, "completed_actions_exceed_total"); + if (record.status != PostmortemStatus::Open) return failWith(error, "status_invalid"); + if (records_.count(record.incidentId) != 0) return failWith(error, "incident_duplicate"); + records_[record.incidentId] = record; + order_.push_back(record.incidentId); + return true; + } + + bool updateActions(const std::string& incidentId, + int completedActions, + std::string* error) { + if (!error) return false; + error->clear(); + auto it = records_.find(incidentId); + if (it == records_.end()) return failWith(error, "incident_missing"); + if (completedActions < 0) return failWith(error, "completed_actions_invalid"); + if (completedActions > it->second.correctiveActions) return failWith(error, "completed_actions_exceed_total"); + it->second.completedActions = completedActions; + if (it->second.completedActions == it->second.correctiveActions) { + it->second.status = PostmortemStatus::Completed; + } else { + it->second.status = PostmortemStatus::Open; + } + return true; + } + + int completionPercent(const std::string& incidentId, std::string* error) const { + if (!error) return 0; + error->clear(); + auto it = records_.find(incidentId); + if (it == records_.end()) { + failWith(error, "incident_missing"); + return 0; + } + if (it->second.correctiveActions == 0) return 100; + return (it->second.completedActions * 100) / it->second.correctiveActions; + } + + int openCount() const { + int count = 0; + for (const auto& id : order_) if (records_.at(id).status == PostmortemStatus::Open) ++count; + return count; + } + + int completedCount() const { + int count = 0; + for (const auto& id : order_) if (records_.at(id).status == PostmortemStatus::Completed) ++count; + return count; + } + +private: + std::map records_; + std::vector order_; +}; diff --git a/editor/tests/step608_test.cpp b/editor/tests/step608_test.cpp new file mode 100644 index 0000000..11da869 --- /dev/null +++ b/editor/tests/step608_test.cpp @@ -0,0 +1,151 @@ +// Step 608: Incident Postmortem Ledger (12 tests) + +#include "IncidentPostmortemLedger.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 PostmortemRecord record(const std::string& id, + const std::string& owner, + int total, + int done = 0) { + return {id, owner, total, done, PostmortemStatus::Open}; +} + +void test_open_success() { + TEST(open_success); + IncidentPostmortemLedger ledger; + std::string error; + CHECK(ledger.open(record("inc-1", "ops", 3), &error), "open should succeed"); + CHECK(ledger.openCount() == 1, "open count mismatch"); + PASS(); +} + +void test_open_rejects_missing_incident_id() { + TEST(open_rejects_missing_incident_id); + IncidentPostmortemLedger ledger; + std::string error; + CHECK(!ledger.open(record("", "ops", 3), &error), "open should fail"); + CHECK(error == "incident_id_missing", "wrong error"); + PASS(); +} + +void test_open_rejects_missing_owner() { + TEST(open_rejects_missing_owner); + IncidentPostmortemLedger ledger; + std::string error; + CHECK(!ledger.open(record("inc-1", "", 3), &error), "open should fail"); + CHECK(error == "owner_missing", "wrong error"); + PASS(); +} + +void test_open_rejects_invalid_corrective_actions() { + TEST(open_rejects_invalid_corrective_actions); + IncidentPostmortemLedger ledger; + std::string error; + CHECK(!ledger.open(record("inc-1", "ops", -1), &error), "open should fail"); + CHECK(error == "corrective_actions_invalid", "wrong error"); + PASS(); +} + +void test_open_rejects_completed_actions_exceed_total() { + TEST(open_rejects_completed_actions_exceed_total); + IncidentPostmortemLedger ledger; + std::string error; + CHECK(!ledger.open(record("inc-1", "ops", 2, 3), &error), "open should fail"); + CHECK(error == "completed_actions_exceed_total", "wrong error"); + PASS(); +} + +void test_open_rejects_duplicate() { + TEST(open_rejects_duplicate); + IncidentPostmortemLedger ledger; + std::string error; + CHECK(ledger.open(record("inc-1", "ops", 3), &error), "first open failed"); + CHECK(!ledger.open(record("inc-1", "ops", 3), &error), "duplicate should fail"); + CHECK(error == "incident_duplicate", "wrong error"); + PASS(); +} + +void test_update_actions_success_changes_percent() { + TEST(update_actions_success_changes_percent); + IncidentPostmortemLedger ledger; + std::string error; + CHECK(ledger.open(record("inc-1", "ops", 4), &error), "open failed"); + CHECK(ledger.updateActions("inc-1", 2, &error), "update failed"); + CHECK(ledger.completionPercent("inc-1", &error) == 50, "percent mismatch"); + PASS(); +} + +void test_update_actions_marks_completed_when_done() { + TEST(update_actions_marks_completed_when_done); + IncidentPostmortemLedger ledger; + std::string error; + CHECK(ledger.open(record("inc-1", "ops", 2), &error), "open failed"); + CHECK(ledger.updateActions("inc-1", 2, &error), "update failed"); + CHECK(ledger.completedCount() == 1, "completed count mismatch"); + CHECK(ledger.openCount() == 0, "open count mismatch"); + PASS(); +} + +void test_update_actions_rejects_missing_incident() { + TEST(update_actions_rejects_missing_incident); + IncidentPostmortemLedger ledger; + std::string error; + CHECK(!ledger.updateActions("missing", 1, &error), "update should fail"); + CHECK(error == "incident_missing", "wrong error"); + PASS(); +} + +void test_update_actions_rejects_invalid_completed_actions() { + TEST(update_actions_rejects_invalid_completed_actions); + IncidentPostmortemLedger ledger; + std::string error; + CHECK(ledger.open(record("inc-1", "ops", 2), &error), "open failed"); + CHECK(!ledger.updateActions("inc-1", 3, &error), "update should fail"); + CHECK(error == "completed_actions_exceed_total", "wrong error"); + PASS(); +} + +void test_completion_percent_returns_100_when_no_actions() { + TEST(completion_percent_returns_100_when_no_actions); + IncidentPostmortemLedger ledger; + std::string error; + CHECK(ledger.open(record("inc-1", "ops", 0), &error), "open failed"); + CHECK(ledger.completionPercent("inc-1", &error) == 100, "zero-action incident should be complete"); + PASS(); +} + +void test_completion_percent_rejects_missing_incident() { + TEST(completion_percent_rejects_missing_incident); + IncidentPostmortemLedger ledger; + std::string error; + CHECK(ledger.completionPercent("missing", &error) == 0, "missing incident should return zero"); + CHECK(error == "incident_missing", "wrong error"); + PASS(); +} + +int main() { + std::cout << "Step 608: Incident Postmortem Ledger\n"; + + test_open_success(); // 1 + test_open_rejects_missing_incident_id(); // 2 + test_open_rejects_missing_owner(); // 3 + test_open_rejects_invalid_corrective_actions(); // 4 + test_open_rejects_completed_actions_exceed_total(); // 5 + test_open_rejects_duplicate(); // 6 + test_update_actions_success_changes_percent(); // 7 + test_update_actions_marks_completed_when_done(); // 8 + test_update_actions_rejects_missing_incident(); // 9 + test_update_actions_rejects_invalid_completed_actions();// 10 + test_completion_percent_returns_100_when_no_actions(); // 11 + test_completion_percent_rejects_missing_incident(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 1c392dc..b7f58e7 100644 --- a/progress.md +++ b/progress.md @@ -11830,3 +11830,33 @@ service-level breach aggregation. - `editor/src/OperationalSLOWatch.h` within header-size limit (`64` <= `600`) - `editor/tests/step607_test.cpp` within test-file size guidance (`150` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 608: Incident Postmortem Ledger +**Status:** PASS (12/12 tests) + +Implements incident postmortem tracking with corrective-action completion and +status/percent rollups. + +**Files added:** +- `editor/src/IncidentPostmortemLedger.h` - postmortem ledger module: + - postmortem intake with validation and duplicate guards + - corrective-action completion updates with status transitions + - completion-percent and open/completed aggregate counters +- `editor/tests/step608_test.cpp` - 12 tests covering: + - intake success/failure validation behavior + - update success/failure behavior and state transitions + - completion-percent behavior including zero-action edge case + +**Files modified:** +- `editor/CMakeLists.txt` - `step608_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step608_test step607_test` - PASS +- `./editor/build-native/step608_test` - PASS (12/12) +- `./editor/build-native/step607_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/IncidentPostmortemLedger.h` within header-size limit (`85` <= `600`) +- `editor/tests/step608_test.cpp` within test-file size guidance (`151` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`