From 02aa551e770c114f35c3ffd03589d6164d389e61 Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 10:49:01 -0700 Subject: [PATCH] Step 567: add leak corruption signal bridge --- editor/CMakeLists.txt | 9 ++ editor/src/LeakCorruptionSignalBridge.h | 154 +++++++++++++++++++++ editor/tests/step567_test.cpp | 176 ++++++++++++++++++++++++ progress.md | 35 +++++ 4 files changed, 374 insertions(+) create mode 100644 editor/src/LeakCorruptionSignalBridge.h create mode 100644 editor/tests/step567_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index ea285e6..2e81a26 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3982,4 +3982,13 @@ target_link_libraries(step566_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step567_test tests/step567_test.cpp) +target_include_directories(step567_test PRIVATE src) +target_link_libraries(step567_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/LeakCorruptionSignalBridge.h b/editor/src/LeakCorruptionSignalBridge.h new file mode 100644 index 0000000..f4b8e4d --- /dev/null +++ b/editor/src/LeakCorruptionSignalBridge.h @@ -0,0 +1,154 @@ +#pragma once +// Step 567: Leak/Corruption Signal Bridge + +#include "DebugValidationUtil.h" + +#include +#include +#include +#include + +enum class MemorySignalType { + LeakSuspected, + LeakConfirmed, + CorruptionSuspected, + CorruptionConfirmed +}; + +enum class MemorySignalSeverity { + Info = 0, + Warning = 1, + Error = 2, + Critical = 3 +}; + +struct MemorySignalRecord { + std::string signalId; + std::string sessionId; + std::string allocationId; + MemorySignalType type = MemorySignalType::LeakSuspected; + std::string message; + std::uint64_t timestamp = 0; +}; + +struct MemoryDiagnosticRecord { + std::string diagnosticId; + std::string sessionId; + std::string allocationId; + std::string paneKey; + std::string message; + MemorySignalSeverity severity = MemorySignalSeverity::Info; + std::uint64_t timestamp = 0; +}; + +class LeakCorruptionSignalBridge { +public: + bool ingestSignal(const MemorySignalRecord& signal, std::string* error) { + if (!error) return false; + error->clear(); + if (!hasRequiredDebugIds(signal.signalId, signal.sessionId)) return fail(error, "signal_or_session_missing"); + if (signal.allocationId.empty()) return fail(error, "allocation_id_missing"); + if (signal.message.empty()) return fail(error, "signal_message_missing"); + if (signal.timestamp == 0) return fail(error, "signal_timestamp_invalid"); + if (containsSignal(signal.signalId)) return fail(error, "signal_duplicate"); + + signals_.push_back(signal); + diagnostics_.push_back(buildDiagnostic(signal)); + stableSortByTimestamp(&signals_); + stableSortByTimestamp(&diagnostics_); + return true; + } + + std::vector diagnosticsForSession(const std::string& sessionId) const { + std::vector filtered; + for (const auto& diagnostic : diagnostics_) { + if (sessionId.empty() || diagnostic.sessionId == sessionId) filtered.push_back(diagnostic); + } + return filtered; + } + + std::vector signalsForAllocation(const std::string& allocationId) const { + std::vector filtered; + for (const auto& signal : signals_) { + if (allocationId.empty() || signal.allocationId == allocationId) filtered.push_back(signal); + } + return filtered; + } + + MemorySignalSeverity highestSeverityForSession(const std::string& sessionId) const { + MemorySignalSeverity highest = MemorySignalSeverity::Info; + for (const auto& diagnostic : diagnostics_) { + if (!sessionId.empty() && diagnostic.sessionId != sessionId) continue; + if (static_cast(diagnostic.severity) > static_cast(highest)) highest = diagnostic.severity; + } + return highest; + } + +private: + std::vector signals_; + std::vector diagnostics_; + + static bool fail(std::string* error, const char* code) { + *error = code; + return false; + } + + bool containsSignal(const std::string& signalId) const { + for (const auto& signal : signals_) { + if (signal.signalId == signalId) return true; + } + return false; + } + + static MemoryDiagnosticRecord buildDiagnostic(const MemorySignalRecord& signal) { + MemoryDiagnosticRecord diagnostic; + diagnostic.diagnosticId = "diag-" + signal.signalId; + diagnostic.sessionId = signal.sessionId; + diagnostic.allocationId = signal.allocationId; + diagnostic.message = signal.message; + diagnostic.timestamp = signal.timestamp; + diagnostic.paneKey = paneForSignal(signal.type); + diagnostic.severity = severityForSignal(signal.type); + return diagnostic; + } + + static std::string paneForSignal(MemorySignalType type) { + switch (type) { + case MemorySignalType::LeakSuspected: + case MemorySignalType::LeakConfirmed: + return "memory-leaks"; + case MemorySignalType::CorruptionSuspected: + case MemorySignalType::CorruptionConfirmed: + return "memory-corruption"; + } + return "memory-diagnostics"; + } + + static MemorySignalSeverity severityForSignal(MemorySignalType type) { + switch (type) { + case MemorySignalType::LeakSuspected: return MemorySignalSeverity::Warning; + case MemorySignalType::LeakConfirmed: return MemorySignalSeverity::Error; + case MemorySignalType::CorruptionSuspected: return MemorySignalSeverity::Error; + case MemorySignalType::CorruptionConfirmed: return MemorySignalSeverity::Critical; + } + return MemorySignalSeverity::Info; + } + + static void stableSortByTimestamp(std::vector* signals) { + std::stable_sort(signals->begin(), + signals->end(), + [](const MemorySignalRecord& a, const MemorySignalRecord& b) { + if (a.timestamp != b.timestamp) return a.timestamp < b.timestamp; + return a.signalId < b.signalId; + }); + } + + static void stableSortByTimestamp(std::vector* diagnostics) { + std::stable_sort(diagnostics->begin(), + diagnostics->end(), + [](const MemoryDiagnosticRecord& a, const MemoryDiagnosticRecord& b) { + if (a.timestamp != b.timestamp) return a.timestamp < b.timestamp; + return a.diagnosticId < b.diagnosticId; + }); + } +}; diff --git a/editor/tests/step567_test.cpp b/editor/tests/step567_test.cpp new file mode 100644 index 0000000..73d3763 --- /dev/null +++ b/editor/tests/step567_test.cpp @@ -0,0 +1,176 @@ +// Step 567: Leak/Corruption Signal Bridge (12 tests) + +#include "LeakCorruptionSignalBridge.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 MemorySignalRecord signal(const std::string& id, + const std::string& session, + const std::string& allocation, + MemorySignalType type, + std::uint64_t timestamp) { + MemorySignalRecord s; + s.signalId = id; + s.sessionId = session; + s.allocationId = allocation; + s.type = type; + s.message = "message-" + id; + s.timestamp = timestamp; + return s; +} + +void test_ingest_leak_suspected_signal_success() { + TEST(ingest_leak_suspected_signal_success); + LeakCorruptionSignalBridge bridge; + std::string error; + CHECK(bridge.ingestSignal(signal("s1", "sess-1", "alloc-1", MemorySignalType::LeakSuspected, 5), &error), "ingest should succeed"); + const auto diagnostics = bridge.diagnosticsForSession("sess-1"); + CHECK(diagnostics.size() == 1, "expected one diagnostic"); + CHECK(diagnostics[0].paneKey == "memory-leaks", "leak pane expected"); + CHECK(diagnostics[0].severity == MemorySignalSeverity::Warning, "warning severity expected"); + PASS(); +} + +void test_ingest_corruption_confirmed_maps_to_critical() { + TEST(ingest_corruption_confirmed_maps_to_critical); + LeakCorruptionSignalBridge bridge; + std::string error; + CHECK(bridge.ingestSignal(signal("s1", "sess-1", "alloc-1", MemorySignalType::CorruptionConfirmed, 5), &error), "ingest should succeed"); + const auto diagnostics = bridge.diagnosticsForSession("sess-1"); + CHECK(diagnostics[0].paneKey == "memory-corruption", "corruption pane expected"); + CHECK(diagnostics[0].severity == MemorySignalSeverity::Critical, "critical severity expected"); + PASS(); +} + +void test_ingest_rejects_missing_signal_or_session_id() { + TEST(ingest_rejects_missing_signal_or_session_id); + LeakCorruptionSignalBridge bridge; + std::string error; + auto s = signal("", "sess-1", "alloc-1", MemorySignalType::LeakSuspected, 5); + CHECK(!bridge.ingestSignal(s, &error), "ingest should fail"); + CHECK(error == "signal_or_session_missing", "wrong error"); + PASS(); +} + +void test_ingest_rejects_missing_allocation_id() { + TEST(ingest_rejects_missing_allocation_id); + LeakCorruptionSignalBridge bridge; + std::string error; + auto s = signal("s1", "sess-1", "", MemorySignalType::LeakSuspected, 5); + CHECK(!bridge.ingestSignal(s, &error), "ingest should fail"); + CHECK(error == "allocation_id_missing", "wrong error"); + PASS(); +} + +void test_ingest_rejects_missing_message() { + TEST(ingest_rejects_missing_message); + LeakCorruptionSignalBridge bridge; + std::string error; + auto s = signal("s1", "sess-1", "alloc-1", MemorySignalType::LeakSuspected, 5); + s.message = ""; + CHECK(!bridge.ingestSignal(s, &error), "ingest should fail"); + CHECK(error == "signal_message_missing", "wrong error"); + PASS(); +} + +void test_ingest_rejects_zero_timestamp() { + TEST(ingest_rejects_zero_timestamp); + LeakCorruptionSignalBridge bridge; + std::string error; + auto s = signal("s1", "sess-1", "alloc-1", MemorySignalType::LeakSuspected, 0); + CHECK(!bridge.ingestSignal(s, &error), "ingest should fail"); + CHECK(error == "signal_timestamp_invalid", "wrong error"); + PASS(); +} + +void test_ingest_rejects_duplicate_signal_id() { + TEST(ingest_rejects_duplicate_signal_id); + LeakCorruptionSignalBridge bridge; + std::string error; + auto s = signal("s1", "sess-1", "alloc-1", MemorySignalType::LeakSuspected, 5); + CHECK(bridge.ingestSignal(s, &error), "first ingest should succeed"); + CHECK(!bridge.ingestSignal(s, &error), "duplicate ingest should fail"); + CHECK(error == "signal_duplicate", "wrong error"); + PASS(); +} + +void test_diagnostics_for_session_filters_results() { + TEST(diagnostics_for_session_filters_results); + LeakCorruptionSignalBridge bridge; + std::string error; + CHECK(bridge.ingestSignal(signal("s1", "sess-1", "alloc-1", MemorySignalType::LeakConfirmed, 5), &error), "ingest s1 failed"); + CHECK(bridge.ingestSignal(signal("s2", "sess-2", "alloc-2", MemorySignalType::LeakConfirmed, 6), &error), "ingest s2 failed"); + const auto diagnostics = bridge.diagnosticsForSession("sess-2"); + CHECK(diagnostics.size() == 1, "expected one filtered diagnostic"); + CHECK(diagnostics[0].sessionId == "sess-2", "wrong session"); + PASS(); +} + +void test_signals_for_allocation_filters_results() { + TEST(signals_for_allocation_filters_results); + LeakCorruptionSignalBridge bridge; + std::string error; + CHECK(bridge.ingestSignal(signal("s1", "sess-1", "alloc-1", MemorySignalType::LeakConfirmed, 5), &error), "ingest s1 failed"); + CHECK(bridge.ingestSignal(signal("s2", "sess-1", "alloc-2", MemorySignalType::CorruptionSuspected, 6), &error), "ingest s2 failed"); + const auto signals = bridge.signalsForAllocation("alloc-2"); + CHECK(signals.size() == 1, "expected one filtered signal"); + CHECK(signals[0].signalId == "s2", "wrong signal"); + PASS(); +} + +void test_highest_severity_for_session_aggregates() { + TEST(highest_severity_for_session_aggregates); + LeakCorruptionSignalBridge bridge; + std::string error; + CHECK(bridge.ingestSignal(signal("s1", "sess-1", "alloc-1", MemorySignalType::LeakSuspected, 5), &error), "ingest s1 failed"); + CHECK(bridge.ingestSignal(signal("s2", "sess-1", "alloc-1", MemorySignalType::LeakConfirmed, 6), &error), "ingest s2 failed"); + CHECK(bridge.ingestSignal(signal("s3", "sess-1", "alloc-1", MemorySignalType::CorruptionConfirmed, 7), &error), "ingest s3 failed"); + CHECK(bridge.highestSeverityForSession("sess-1") == MemorySignalSeverity::Critical, "critical severity expected"); + PASS(); +} + +void test_highest_severity_defaults_to_info_when_empty() { + TEST(highest_severity_defaults_to_info_when_empty); + LeakCorruptionSignalBridge bridge; + CHECK(bridge.highestSeverityForSession("sess-1") == MemorySignalSeverity::Info, "default info expected"); + PASS(); +} + +void test_diagnostics_sorted_by_timestamp() { + TEST(diagnostics_sorted_by_timestamp); + LeakCorruptionSignalBridge bridge; + std::string error; + CHECK(bridge.ingestSignal(signal("s2", "sess-1", "alloc-1", MemorySignalType::LeakConfirmed, 20), &error), "ingest s2 failed"); + CHECK(bridge.ingestSignal(signal("s1", "sess-1", "alloc-1", MemorySignalType::LeakSuspected, 10), &error), "ingest s1 failed"); + const auto diagnostics = bridge.diagnosticsForSession("sess-1"); + CHECK(diagnostics.size() == 2, "diagnostic count mismatch"); + CHECK(diagnostics[0].diagnosticId == "diag-s1", "first diagnostic should be earliest"); + CHECK(diagnostics[1].diagnosticId == "diag-s2", "second diagnostic should be later"); + PASS(); +} + +int main() { + std::cout << "Step 567: Leak/Corruption Signal Bridge\n"; + + test_ingest_leak_suspected_signal_success(); // 1 + test_ingest_corruption_confirmed_maps_to_critical(); // 2 + test_ingest_rejects_missing_signal_or_session_id(); // 3 + test_ingest_rejects_missing_allocation_id(); // 4 + test_ingest_rejects_missing_message(); // 5 + test_ingest_rejects_zero_timestamp(); // 6 + test_ingest_rejects_duplicate_signal_id(); // 7 + test_diagnostics_for_session_filters_results(); // 8 + test_signals_for_allocation_filters_results(); // 9 + test_highest_severity_for_session_aggregates(); // 10 + test_highest_severity_defaults_to_info_when_empty(); // 11 + test_diagnostics_sorted_by_timestamp(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index aef1d2c..31001ff 100644 --- a/progress.md +++ b/progress.md @@ -10259,3 +10259,38 @@ deterministic timeline ordering, active-allocation tracking, and session filters - `editor/src/AllocationOwnershipTraceHooks.h` within header-size limit (`150` <= `600`) - `editor/tests/step566_test.cpp` within test-file size guidance (`160` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 567: Leak/Corruption Signal Bridge +**Status:** PASS (12/12 tests) + +Implements a bridge that converts runtime leak/corruption signals into +session-scoped diagnostics and pane routing for memory debug surfaces. + +**Files added:** +- `editor/src/LeakCorruptionSignalBridge.h` - signal bridge module: + - leak/corruption signal schema and severity mapping + - signal ingestion validation and duplicate guard + - diagnostic emission with pane routing (`memory-leaks` / `memory-corruption`) + - session/allocation filtering helpers + - session-level highest-severity aggregation +- `editor/tests/step567_test.cpp` - 12 tests covering: + - signal ingestion success and validation failures + - duplicate signal rejection behavior + - pane/severity mapping behavior + - session/allocation filtering behavior + - highest-severity aggregation behavior + - deterministic timestamp ordering behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step567_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step567_test step566_test` - PASS +- `./editor/build-native/step567_test` - PASS (12/12) +- `./editor/build-native/step566_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/LeakCorruptionSignalBridge.h` within header-size limit (`154` <= `600`) +- `editor/tests/step567_test.cpp` within test-file size guidance (`176` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`