From d1c1660bc685511b9b22ac086aef1b4afa45bc2a Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 09:23:19 -0700 Subject: [PATCH] Step 530: add post-apply structural validation gate --- editor/CMakeLists.txt | 9 ++ editor/src/PostApplyStructuralGate.h | 123 +++++++++++++++++++++ editor/tests/step530_test.cpp | 155 +++++++++++++++++++++++++++ progress.md | 37 +++++++ 4 files changed, 324 insertions(+) create mode 100644 editor/src/PostApplyStructuralGate.h create mode 100644 editor/tests/step530_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 3f68f4c..f4b4235 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3649,4 +3649,13 @@ target_link_libraries(step529_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step530_test tests/step530_test.cpp) +target_include_directories(step530_test PRIVATE src) +target_link_libraries(step530_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/PostApplyStructuralGate.h b/editor/src/PostApplyStructuralGate.h new file mode 100644 index 0000000..d318250 --- /dev/null +++ b/editor/src/PostApplyStructuralGate.h @@ -0,0 +1,123 @@ +#pragma once +// Step 530: Post-Apply Structural Gate + +#include +#include +#include +#include + +#include "ConstraintViolationDiagnostics.h" + +struct PostApplyInput { + bool regionReparsed = false; + int parseErrorsAfter = 0; + std::vector symbolsBefore; + std::vector symbolsAfter; + std::vector allowedSymbolDrift; +}; + +struct PostApplyResult { + bool valid = false; + std::vector addedSymbols; + std::vector removedSymbols; + ConstraintDiagnosticPacket diagnostics; +}; + +class PostApplyStructuralGate { +public: + static PostApplyResult validate(const PostApplyInput& input) { + PostApplyResult result; + + if (!input.regionReparsed) { + result.diagnostics.violations.push_back({ + "region_not_reparsed", + "Touched region was not reparsed after apply", + "regionReparsed", + false + }); + } + + if (input.parseErrorsAfter > 0) { + result.diagnostics.violations.push_back({ + "ast_integrity_failure", + "AST parse reported errors after apply", + "parseErrorsAfter", + false + }); + } + + computeSymbolDiff(input, result.addedSymbols, result.removedSymbols); + addUnexpectedDriftViolations(input.allowedSymbolDrift, + result.addedSymbols, + result.removedSymbols, + result.diagnostics.violations); + + dedupe(result.diagnostics.violations); + result.valid = result.diagnostics.violations.empty(); + result.diagnostics.ok = result.valid; + result.diagnostics.recommendedAction = result.valid ? "proceed" : "escalate"; + return result; + } + +private: + static void computeSymbolDiff(const PostApplyInput& input, + std::vector& added, + std::vector& removed) { + auto before = asSet(input.symbolsBefore); + auto after = asSet(input.symbolsAfter); + + for (const auto& s : after) { + if (before.count(s) == 0) added.push_back(s); + } + for (const auto& s : before) { + if (after.count(s) == 0) removed.push_back(s); + } + } + + static void addUnexpectedDriftViolations( + const std::vector& allowedDrift, + const std::vector& added, + const std::vector& removed, + std::vector& violations) { + auto allowed = asSet(allowedDrift); + for (const auto& symbol : added) { + if (allowed.count(symbol) == 0) { + violations.push_back({ + "unexpected_symbol_drift_add", + "Unexpected symbol added after apply", + symbol, + false + }); + } + } + for (const auto& symbol : removed) { + if (allowed.count(symbol) == 0) { + violations.push_back({ + "unexpected_symbol_drift_remove", + "Unexpected symbol removed after apply", + symbol, + false + }); + } + } + } + + static std::set asSet(const std::vector& symbols) { + return std::set(symbols.begin(), symbols.end()); + } + + static void dedupe(std::vector& violations) { + std::vector out; + for (const auto& v : violations) { + bool exists = false; + for (const auto& current : out) { + if (current.code == v.code && current.field == v.field) { + exists = true; + break; + } + } + if (!exists) out.push_back(v); + } + violations.swap(out); + } +}; diff --git a/editor/tests/step530_test.cpp b/editor/tests/step530_test.cpp new file mode 100644 index 0000000..ee5d531 --- /dev/null +++ b/editor/tests/step530_test.cpp @@ -0,0 +1,155 @@ +// Step 530: Post-Apply Structural Gate (12 tests) + +#include "PostApplyStructuralGate.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 bool hasCode(const PostApplyResult& result, const std::string& code) { + for (const auto& v : result.diagnostics.violations) { + if (v.code == code) return true; + } + return false; +} + +static int countCode(const PostApplyResult& result, const std::string& code) { + int count = 0; + for (const auto& v : result.diagnostics.violations) { + if (v.code == code) ++count; + } + return count; +} + +void test_clean_reparse_with_no_drift_passes() { + TEST(clean_reparse_with_no_drift_passes); + PostApplyInput input{true, 0, {"cursor", "count"}, {"cursor", "count"}, {}}; + auto result = PostApplyStructuralGate::validate(input); + CHECK(result.valid, "clean post-apply state should pass"); + CHECK(result.diagnostics.recommendedAction == "proceed", "should proceed"); + PASS(); +} + +void test_parse_errors_after_apply_fail_integrity_gate() { + TEST(parse_errors_after_apply_fail_integrity_gate); + PostApplyInput input{true, 2, {"cursor"}, {"cursor"}, {}}; + auto result = PostApplyStructuralGate::validate(input); + CHECK(!result.valid, "parse errors should fail gate"); + CHECK(hasCode(result, "ast_integrity_failure"), "missing integrity violation"); + PASS(); +} + +void test_missing_reparse_fails_gate() { + TEST(missing_reparse_fails_gate); + PostApplyInput input{false, 0, {"cursor"}, {"cursor"}, {}}; + auto result = PostApplyStructuralGate::validate(input); + CHECK(!result.valid, "missing reparse should fail"); + CHECK(hasCode(result, "region_not_reparsed"), "missing reparse violation"); + PASS(); +} + +void test_unexpected_added_symbol_fails_gate() { + TEST(unexpected_added_symbol_fails_gate); + PostApplyInput input{true, 0, {"cursor"}, {"cursor", "ghost"}, {}}; + auto result = PostApplyStructuralGate::validate(input); + CHECK(!result.valid, "unexpected added symbol should fail"); + CHECK(hasCode(result, "unexpected_symbol_drift_add"), "missing add drift violation"); + PASS(); +} + +void test_unexpected_removed_symbol_fails_gate() { + TEST(unexpected_removed_symbol_fails_gate); + PostApplyInput input{true, 0, {"cursor", "count"}, {"cursor"}, {}}; + auto result = PostApplyStructuralGate::validate(input); + CHECK(!result.valid, "unexpected removed symbol should fail"); + CHECK(hasCode(result, "unexpected_symbol_drift_remove"), "missing remove drift violation"); + PASS(); +} + +void test_allowed_added_symbol_drift_passes() { + TEST(allowed_added_symbol_drift_passes); + PostApplyInput input{true, 0, {"cursor"}, {"cursor", "tempAlias"}, {"tempAlias"}}; + auto result = PostApplyStructuralGate::validate(input); + CHECK(result.valid, "allowed added drift should pass"); + PASS(); +} + +void test_allowed_removed_symbol_drift_passes() { + TEST(allowed_removed_symbol_drift_passes); + PostApplyInput input{true, 0, {"cursor", "tempAlias"}, {"cursor"}, {"tempAlias"}}; + auto result = PostApplyStructuralGate::validate(input); + CHECK(result.valid, "allowed removed drift should pass"); + PASS(); +} + +void test_duplicate_symbols_do_not_create_duplicate_violations() { + TEST(duplicate_symbols_do_not_create_duplicate_violations); + PostApplyInput input{true, 0, {"cursor"}, {"cursor", "ghost", "ghost"}, {}}; + auto result = PostApplyStructuralGate::validate(input); + CHECK(countCode(result, "unexpected_symbol_drift_add") == 1, + "duplicate added symbol violations should dedupe"); + PASS(); +} + +void test_multiple_unexpected_drift_entries_are_aggregated() { + TEST(multiple_unexpected_drift_entries_are_aggregated); + PostApplyInput input{true, 0, {"cursor", "count"}, {"cursor", "newA", "newB"}, {}}; + auto result = PostApplyStructuralGate::validate(input); + CHECK(!result.valid, "multiple unexpected drift should fail"); + CHECK(result.addedSymbols.size() == 2, "expected two added symbols"); + CHECK(result.removedSymbols.size() == 1, "expected one removed symbol"); + PASS(); +} + +void test_empty_symbol_graphs_are_valid_when_reparse_is_clean() { + TEST(empty_symbol_graphs_are_valid_when_reparse_is_clean); + PostApplyInput input{true, 0, {}, {}, {}}; + auto result = PostApplyStructuralGate::validate(input); + CHECK(result.valid, "empty symbol graphs with clean parse should pass"); + PASS(); +} + +void test_combined_reparse_and_drift_failures_report_all() { + TEST(combined_reparse_and_drift_failures_report_all); + PostApplyInput input{false, 1, {"cursor"}, {"ghost"}, {}}; + auto result = PostApplyStructuralGate::validate(input); + CHECK(hasCode(result, "region_not_reparsed"), "missing reparse violation"); + CHECK(hasCode(result, "ast_integrity_failure"), "missing integrity violation"); + CHECK(hasCode(result, "unexpected_symbol_drift_add"), "missing add drift violation"); + CHECK(hasCode(result, "unexpected_symbol_drift_remove"), "missing remove drift violation"); + CHECK(result.diagnostics.recommendedAction == "escalate", "combined failures should escalate"); + PASS(); +} + +void test_allowed_drift_with_clean_parse_proceeds() { + TEST(allowed_drift_with_clean_parse_proceeds); + PostApplyInput input{true, 0, {"cursor"}, {"cursor", "alias"}, {"alias"}}; + auto result = PostApplyStructuralGate::validate(input); + CHECK(result.valid, "allowed drift should remain valid"); + CHECK(result.diagnostics.recommendedAction == "proceed", "allowed drift should proceed"); + PASS(); +} + +int main() { + std::cout << "Step 530: Post-Apply Structural Gate\n"; + + test_clean_reparse_with_no_drift_passes(); // 1 + test_parse_errors_after_apply_fail_integrity_gate(); // 2 + test_missing_reparse_fails_gate(); // 3 + test_unexpected_added_symbol_fails_gate(); // 4 + test_unexpected_removed_symbol_fails_gate(); // 5 + test_allowed_added_symbol_drift_passes(); // 6 + test_allowed_removed_symbol_drift_passes(); // 7 + test_duplicate_symbols_do_not_create_duplicate_violations(); // 8 + test_multiple_unexpected_drift_entries_are_aggregated(); // 9 + test_empty_symbol_graphs_are_valid_when_reparse_is_clean(); // 10 + test_combined_reparse_and_drift_failures_report_all(); // 11 + test_allowed_drift_with_clean_parse_proceeds(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 2709ff2..26bf0cd 100644 --- a/progress.md +++ b/progress.md @@ -8776,3 +8776,40 @@ snapshot before any mutation is allowed. - `editor/src/PreApplyValidationGate.h` within header-size limit (`130` <= `600`) - `editor/tests/step529_test.cpp` within test-file size guidance (`202` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 530: Post-Apply Structural Gate +**Status:** PASS (12/12 tests) + +Adds a deterministic post-apply structural validation gate that requires +successful reparse of touched regions and rejects unexpected symbol-graph drift +after candidate edits are applied. + +**Files added:** +- `editor/src/PostApplyStructuralGate.h` - post-apply gate module: + - validates region reparse completion + - validates AST integrity via parse error count check + - computes symbol additions/removals from before/after snapshots + - enforces allowed symbol-drift policy + - emits structured diagnostics for unexpected drift and integrity failures +- `editor/tests/step530_test.cpp` - 12 tests covering: + - clean reparse/no-drift pass path + - AST integrity and missing-reparse failure paths + - unexpected add/remove symbol drift rejection + - allowed drift pass behavior (add/remove) + - duplicate-symbol dedup behavior + - aggregated multi-failure reporting + - empty graph edge case + +**Files modified:** +- `editor/CMakeLists.txt` - `step530_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step530_test step529_test` - PASS +- `./editor/build-native/step530_test` - PASS (12/12) +- `./editor/build-native/step529_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/PostApplyStructuralGate.h` within header-size limit (`123` <= `600`) +- `editor/tests/step530_test.cpp` within test-file size guidance (`155` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`