Step 531: add contract delta checker for post-conditions

This commit is contained in:
Bill
2026-02-17 09:24:59 -07:00
parent d1c1660bc6
commit fc9c728521
4 changed files with 396 additions and 0 deletions

View File

@@ -3658,4 +3658,13 @@ target_link_libraries(step530_test PRIVATE
tree_sitter_javascript tree_sitter_typescript tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go) 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) # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)

View File

@@ -0,0 +1,149 @@
#pragma once
// Step 531: Contract Delta Checker
#include <set>
#include <string>
#include <vector>
#include "ConstraintViolationDiagnostics.h"
struct ContractDeltaInput {
TaskitemContract contract;
std::vector<std::string> diagnosticsBefore;
std::vector<std::string> diagnosticsAfter;
std::vector<std::string> requiredSymbolsAfter;
std::vector<std::string> 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<std::string>& expectedAdds,
const std::set<std::string>& added,
std::vector<ConstraintViolation>& 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<std::string>& expectedRemoves,
const std::set<std::string>& removed,
std::vector<ConstraintViolation>& 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<std::string>& expectedAdds,
const std::vector<std::string>& expectedRemoves,
const std::set<std::string>& added,
const std::set<std::string>& removed,
std::vector<ConstraintViolation>& 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<std::string>& requiredSymbols,
const std::vector<std::string>& actualSymbols,
std::vector<ConstraintViolation>& 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<std::string> asSet(const std::vector<std::string>& values) {
return std::set<std::string>(values.begin(), values.end());
}
static std::set<std::string> setDifference(const std::set<std::string>& a,
const std::set<std::string>& b) {
std::set<std::string> out;
for (const auto& v : a) {
if (b.count(v) == 0) out.insert(v);
}
return out;
}
};

View File

@@ -0,0 +1,200 @@
// Step 531: Contract Delta Checker (12 tests)
#include "ContractDeltaChecker.h"
#include <iostream>
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;
}

View File

@@ -8813,3 +8813,41 @@ after candidate edits are applied.
- `editor/src/PostApplyStructuralGate.h` within header-size limit (`123` <= `600`) - `editor/src/PostApplyStructuralGate.h` within header-size limit (`123` <= `600`)
- `editor/tests/step530_test.cpp` within test-file size guidance (`155` lines) - `editor/tests/step530_test.cpp` within test-file size guidance (`155` lines)
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` - 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`