Step 531: add contract delta checker for post-conditions
This commit is contained in:
@@ -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)
|
||||
|
||||
149
editor/src/ContractDeltaChecker.h
Normal file
149
editor/src/ContractDeltaChecker.h
Normal 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;
|
||||
}
|
||||
};
|
||||
200
editor/tests/step531_test.cpp
Normal file
200
editor/tests/step531_test.cpp
Normal 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;
|
||||
}
|
||||
38
progress.md
38
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`
|
||||
|
||||
Reference in New Issue
Block a user