diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index f4b4235..a44b271 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3658,4 +3658,13 @@ target_link_libraries(step530_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step531_test tests/step531_test.cpp) +target_include_directories(step531_test PRIVATE src) +target_link_libraries(step531_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/ContractDeltaChecker.h b/editor/src/ContractDeltaChecker.h new file mode 100644 index 0000000..988e91d --- /dev/null +++ b/editor/src/ContractDeltaChecker.h @@ -0,0 +1,149 @@ +#pragma once +// Step 531: Contract Delta Checker + +#include +#include +#include + +#include "ConstraintViolationDiagnostics.h" + +struct ContractDeltaInput { + TaskitemContract contract; + std::vector diagnosticsBefore; + std::vector diagnosticsAfter; + std::vector requiredSymbolsAfter; + std::vector actualSymbolsAfter; + bool strictUnexpectedDiagnosticDrift = true; +}; + +struct ContractDeltaResult { + bool valid = false; + ConstraintDiagnosticPacket diagnostics; +}; + +class ContractDeltaChecker { +public: + static ContractDeltaResult validate(const ContractDeltaInput& input) { + ContractDeltaResult result; + + auto before = asSet(input.diagnosticsBefore); + auto after = asSet(input.diagnosticsAfter); + auto added = setDifference(after, before); + auto removed = setDifference(before, after); + + validateExpectedAdds(input.contract.expectedDiagnosticsAdd, + added, + result.diagnostics.violations); + validateExpectedRemoves(input.contract.expectedDiagnosticsRemove, + removed, + result.diagnostics.violations); + + if (input.strictUnexpectedDiagnosticDrift) { + validateUnexpectedDrift(input.contract.expectedDiagnosticsAdd, + input.contract.expectedDiagnosticsRemove, + added, + removed, + result.diagnostics.violations); + } + + validateRequiredSymbols(input.requiredSymbolsAfter, + input.actualSymbolsAfter, + 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 validateExpectedAdds(const std::vector& expectedAdds, + const std::set& added, + std::vector& violations) { + for (const auto& expected : asSet(expectedAdds)) { + if (added.count(expected) == 0) { + violations.push_back({ + "missing_expected_diagnostic_add", + "Expected diagnostic add did not occur", + expected, + false + }); + } + } + } + + static void validateExpectedRemoves(const std::vector& expectedRemoves, + const std::set& removed, + std::vector& violations) { + for (const auto& expected : asSet(expectedRemoves)) { + if (removed.count(expected) == 0) { + violations.push_back({ + "missing_expected_diagnostic_remove", + "Expected diagnostic removal did not occur", + expected, + false + }); + } + } + } + + static void validateUnexpectedDrift(const std::vector& expectedAdds, + const std::vector& expectedRemoves, + const std::set& added, + const std::set& removed, + std::vector& violations) { + auto expectedAddSet = asSet(expectedAdds); + auto expectedRemoveSet = asSet(expectedRemoves); + + for (const auto& d : added) { + if (expectedAddSet.count(d) == 0) { + violations.push_back({ + "unexpected_diagnostic_add", + "Unexpected diagnostic added by edit", + d, + false + }); + } + } + + for (const auto& d : removed) { + if (expectedRemoveSet.count(d) == 0) { + violations.push_back({ + "unexpected_diagnostic_remove", + "Unexpected diagnostic removed by edit", + d, + false + }); + } + } + } + + static void validateRequiredSymbols(const std::vector& requiredSymbols, + const std::vector& actualSymbols, + std::vector& violations) { + auto actual = asSet(actualSymbols); + for (const auto& required : asSet(requiredSymbols)) { + if (actual.count(required) == 0) { + violations.push_back({ + "postcondition_symbol_missing", + "Required post-condition symbol missing after apply", + required, + false + }); + } + } + } + + static std::set asSet(const std::vector& values) { + return std::set(values.begin(), values.end()); + } + + static std::set setDifference(const std::set& a, + const std::set& b) { + std::set out; + for (const auto& v : a) { + if (b.count(v) == 0) out.insert(v); + } + return out; + } +}; diff --git a/editor/tests/step531_test.cpp b/editor/tests/step531_test.cpp new file mode 100644 index 0000000..297ffde --- /dev/null +++ b/editor/tests/step531_test.cpp @@ -0,0 +1,200 @@ +// Step 531: Contract Delta Checker (12 tests) + +#include "ContractDeltaChecker.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 ContractDeltaResult& result, const std::string& code) { + for (const auto& v : result.diagnostics.violations) { + if (v.code == code) return true; + } + return false; +} + +static TaskitemContract baseContract() { + TaskitemContract c; + c.id = "ti-531"; + c.nodeId = "n-1"; + c.language = "cpp"; + c.allowedTargets = {"Function"}; + c.allowedOps = {"rename"}; + c.allowedSymbols = {"cursor", "count"}; + c.forbiddenSymbols = {"unsafeGlobal"}; + c.expectedDiagnosticsAdd = {"diag.new_warning"}; + c.expectedDiagnosticsRemove = {"diag.old_warning"}; + return c; +} + +void test_expected_add_and_remove_pass() { + TEST(expected_add_and_remove_pass); + ContractDeltaInput input; + input.contract = baseContract(); + input.diagnosticsBefore = {"diag.old_warning"}; + input.diagnosticsAfter = {"diag.new_warning"}; + input.requiredSymbolsAfter = {"cursor"}; + input.actualSymbolsAfter = {"cursor", "count"}; + auto result = ContractDeltaChecker::validate(input); + CHECK(result.valid, "expected delta should pass"); + PASS(); +} + +void test_missing_expected_add_fails() { + TEST(missing_expected_add_fails); + ContractDeltaInput input; + input.contract = baseContract(); + input.diagnosticsBefore = {"diag.old_warning"}; + input.diagnosticsAfter = {}; + auto result = ContractDeltaChecker::validate(input); + CHECK(hasCode(result, "missing_expected_diagnostic_add"), "missing add violation expected"); + PASS(); +} + +void test_missing_expected_remove_fails() { + TEST(missing_expected_remove_fails); + ContractDeltaInput input; + input.contract = baseContract(); + input.diagnosticsBefore = {"diag.old_warning", "diag.other"}; + input.diagnosticsAfter = {"diag.new_warning", "diag.old_warning"}; + auto result = ContractDeltaChecker::validate(input); + CHECK(hasCode(result, "missing_expected_diagnostic_remove"), "missing remove violation expected"); + PASS(); +} + +void test_unexpected_add_fails_in_strict_mode() { + TEST(unexpected_add_fails_in_strict_mode); + ContractDeltaInput input; + input.contract = baseContract(); + input.diagnosticsBefore = {"diag.old_warning"}; + input.diagnosticsAfter = {"diag.new_warning", "diag.extra"}; + auto result = ContractDeltaChecker::validate(input); + CHECK(hasCode(result, "unexpected_diagnostic_add"), "unexpected add violation expected"); + PASS(); +} + +void test_unexpected_remove_fails_in_strict_mode() { + TEST(unexpected_remove_fails_in_strict_mode); + ContractDeltaInput input; + input.contract = baseContract(); + input.diagnosticsBefore = {"diag.old_warning", "diag.extra"}; + input.diagnosticsAfter = {"diag.new_warning"}; + auto result = ContractDeltaChecker::validate(input); + CHECK(hasCode(result, "unexpected_diagnostic_remove"), "unexpected remove violation expected"); + PASS(); +} + +void test_non_strict_mode_allows_unexpected_drift() { + TEST(non_strict_mode_allows_unexpected_drift); + ContractDeltaInput input; + input.contract = baseContract(); + input.strictUnexpectedDiagnosticDrift = false; + input.diagnosticsBefore = {"diag.old_warning", "diag.extra_old"}; + input.diagnosticsAfter = {"diag.new_warning", "diag.extra_new"}; + auto result = ContractDeltaChecker::validate(input); + CHECK(result.valid, "non-strict mode should allow unexpected drift"); + PASS(); +} + +void test_required_post_symbol_present_passes() { + TEST(required_post_symbol_present_passes); + ContractDeltaInput input; + input.contract = baseContract(); + input.diagnosticsBefore = {"diag.old_warning"}; + input.diagnosticsAfter = {"diag.new_warning"}; + input.requiredSymbolsAfter = {"cursor"}; + input.actualSymbolsAfter = {"cursor", "count"}; + auto result = ContractDeltaChecker::validate(input); + CHECK(result.valid, "required symbol present should pass"); + PASS(); +} + +void test_required_post_symbol_missing_fails() { + TEST(required_post_symbol_missing_fails); + ContractDeltaInput input; + input.contract = baseContract(); + input.diagnosticsBefore = {"diag.old_warning"}; + input.diagnosticsAfter = {"diag.new_warning"}; + input.requiredSymbolsAfter = {"cursor", "count"}; + input.actualSymbolsAfter = {"cursor"}; + auto result = ContractDeltaChecker::validate(input); + CHECK(hasCode(result, "postcondition_symbol_missing"), "missing postcondition symbol violation"); + PASS(); +} + +void test_duplicate_diagnostics_do_not_break_delta_math() { + TEST(duplicate_diagnostics_do_not_break_delta_math); + ContractDeltaInput input; + input.contract = baseContract(); + input.diagnosticsBefore = {"diag.old_warning", "diag.old_warning"}; + input.diagnosticsAfter = {"diag.new_warning", "diag.new_warning"}; + auto result = ContractDeltaChecker::validate(input); + CHECK(result.valid, "duplicate diagnostics should normalize by set semantics"); + PASS(); +} + +void test_empty_delta_expectations_can_pass() { + TEST(empty_delta_expectations_can_pass); + ContractDeltaInput input; + input.contract = baseContract(); + input.contract.expectedDiagnosticsAdd.clear(); + input.contract.expectedDiagnosticsRemove.clear(); + input.diagnosticsBefore = {"diag.keep"}; + input.diagnosticsAfter = {"diag.keep"}; + auto result = ContractDeltaChecker::validate(input); + CHECK(result.valid, "no expected delta and no drift should pass"); + PASS(); +} + +void test_multiple_failures_are_aggregated() { + TEST(multiple_failures_are_aggregated); + ContractDeltaInput input; + input.contract = baseContract(); + input.diagnosticsBefore = {"diag.old_warning", "diag.x"}; + input.diagnosticsAfter = {"diag.old_warning", "diag.y"}; + input.requiredSymbolsAfter = {"cursor"}; + input.actualSymbolsAfter = {}; + auto result = ContractDeltaChecker::validate(input); + CHECK(hasCode(result, "missing_expected_diagnostic_add"), "missing expected add violation"); + CHECK(hasCode(result, "missing_expected_diagnostic_remove"), "missing expected remove violation"); + CHECK(hasCode(result, "unexpected_diagnostic_add"), "missing unexpected add violation"); + CHECK(hasCode(result, "unexpected_diagnostic_remove"), "missing unexpected remove violation"); + CHECK(hasCode(result, "postcondition_symbol_missing"), "missing postcondition symbol violation"); + PASS(); +} + +void test_failed_delta_check_escalates() { + TEST(failed_delta_check_escalates); + ContractDeltaInput input; + input.contract = baseContract(); + input.diagnosticsBefore = {"diag.old_warning"}; + input.diagnosticsAfter = {}; + auto result = ContractDeltaChecker::validate(input); + CHECK(!result.valid, "failed delta should be invalid"); + CHECK(result.diagnostics.recommendedAction == "escalate", "failed delta should escalate"); + PASS(); +} + +int main() { + std::cout << "Step 531: Contract Delta Checker\n"; + + test_expected_add_and_remove_pass(); // 1 + test_missing_expected_add_fails(); // 2 + test_missing_expected_remove_fails(); // 3 + test_unexpected_add_fails_in_strict_mode(); // 4 + test_unexpected_remove_fails_in_strict_mode(); // 5 + test_non_strict_mode_allows_unexpected_drift(); // 6 + test_required_post_symbol_present_passes(); // 7 + test_required_post_symbol_missing_fails(); // 8 + test_duplicate_diagnostics_do_not_break_delta_math(); // 9 + test_empty_delta_expectations_can_pass(); // 10 + test_multiple_failures_are_aggregated(); // 11 + test_failed_delta_check_escalates(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 26bf0cd..da6ac59 100644 --- a/progress.md +++ b/progress.md @@ -8813,3 +8813,41 @@ after candidate edits are applied. - `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` + +### Step 531: Contract Delta Checker +**Status:** PASS (12/12 tests) + +Implements a contract delta checker that validates expected diagnostic +delta semantics and required post-condition symbols, blocking edits that violate +post-apply contract expectations. + +**Files added:** +- `editor/src/ContractDeltaChecker.h` - contract delta validation module: + - computes before/after diagnostic add/remove sets + - verifies expected diagnostic adds/removes occurred + - optionally enforces strict unexpected diagnostic drift rejection + - verifies required post-condition symbols are present after apply + - emits structured violation diagnostics and final pass/fail action +- `editor/tests/step531_test.cpp` - 12 tests covering: + - expected add/remove pass path + - missing expected add/remove failures + - strict unexpected add/remove rejection behavior + - non-strict drift tolerance behavior + - required post-condition symbol presence checks + - duplicate diagnostics normalization behavior + - empty-delta edge case + - aggregated multi-failure reporting and escalation action + +**Files modified:** +- `editor/CMakeLists.txt` - `step531_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step531_test step530_test` - PASS +- `./editor/build-native/step531_test` - PASS (12/12) +- `./editor/build-native/step530_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/ContractDeltaChecker.h` within header-size limit (`149` <= `600`) +- `editor/tests/step531_test.cpp` within test-file size guidance (`200` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`