diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index c1b9f7e..d9277e5 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3622,4 +3622,13 @@ target_link_libraries(step526_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step527_test tests/step527_test.cpp) +target_include_directories(step527_test PRIVATE src) +target_link_libraries(step527_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/ConstraintViolationDiagnostics.h b/editor/src/ConstraintViolationDiagnostics.h new file mode 100644 index 0000000..e926004 --- /dev/null +++ b/editor/src/ConstraintViolationDiagnostics.h @@ -0,0 +1,169 @@ +#pragma once +// Step 527: Constraint Violation Diagnostics + +#include +#include +#include + +#include + +#include "LegalOperationGraph.h" +#include "SymbolScopeExtractor.h" +#include "TypedTaskitemContractSchema.h" + +using json = nlohmann::json; + +struct ConstraintViolation { + std::string code; + std::string message; + std::string field; + bool retryable = false; +}; + +struct ConstraintDiagnosticPacket { + bool ok = true; + std::string recommendedAction = "proceed"; + std::vector violations; + + json toJson() const { + json out; + out["ok"] = ok; + out["recommendedAction"] = recommendedAction; + out["violations"] = json::array(); + for (const auto& v : violations) { + out["violations"].push_back({ + {"code", v.code}, + {"message", v.message}, + {"field", v.field}, + {"retryable", v.retryable} + }); + } + return out; + } +}; + +struct ConstraintCheckInput { + TaskitemContract contract; + std::string language; + std::string nodeKind; + std::string requestedOp; + std::vector requestedSymbols; + ScopeSnapshot scopeSnapshot; +}; + +class ConstraintViolationDiagnostics { +public: + static ConstraintDiagnosticPacket evaluate( + const ConstraintCheckInput& input, + const LegalOperationGraph& graph, + const SymbolScopeExtractor& scopeExtractor) { + ConstraintDiagnosticPacket packet; + + addOperationViolations(packet, input, graph); + addSymbolViolations(packet, input, scopeExtractor); + + deduplicate(packet.violations); + packet.ok = packet.violations.empty(); + packet.recommendedAction = decideAction(packet.violations); + return packet; + } + +private: + static void addOperationViolations(ConstraintDiagnosticPacket& packet, + const ConstraintCheckInput& input, + const LegalOperationGraph& graph) { + auto legal = graph.allowedOps(input.language, input.nodeKind); + if (!legal.supported) { + packet.violations.push_back({ + "unsupported_context", + "No legal operation mapping for language/nodeKind", + input.language + "::" + input.nodeKind, + false + }); + return; + } + + if (!graph.opAllowed(input.language, input.nodeKind, input.requestedOp)) { + packet.violations.push_back({ + "illegal_operation", + "Requested operation is illegal for this AST node", + input.requestedOp, + false + }); + } + + if (!TypedTaskitemContractSchema::opAllowed(input.contract, input.requestedOp)) { + packet.violations.push_back({ + "contract_forbidden_operation", + "Requested operation is outside taskitem contract", + input.requestedOp, + false + }); + } + } + + static void addSymbolViolations(ConstraintDiagnosticPacket& packet, + const ConstraintCheckInput& input, + const SymbolScopeExtractor& scopeExtractor) { + auto unresolved = scopeExtractor.unresolvedSymbols( + input.scopeSnapshot, input.requestedSymbols); + for (const auto& symbol : unresolved) { + packet.violations.push_back({ + "out_of_scope_symbol", + "Requested symbol is not in extracted scope snapshot", + symbol, + true + }); + } + + for (const auto& symbol : input.requestedSymbols) { + if (isForbidden(input.contract, symbol)) { + packet.violations.push_back({ + "forbidden_symbol", + "Requested symbol is explicitly forbidden by contract", + symbol, + false + }); + continue; + } + if (!TypedTaskitemContractSchema::symbolAllowed(input.contract, symbol)) { + packet.violations.push_back({ + "contract_disallowed_symbol", + "Requested symbol is outside contract allowedSymbols", + symbol, + false + }); + } + } + } + + static bool isForbidden(const TaskitemContract& contract, + const std::string& symbol) { + return std::find(contract.forbiddenSymbols.begin(), + contract.forbiddenSymbols.end(), + symbol) != contract.forbiddenSymbols.end(); + } + + static void deduplicate(std::vector& violations) { + std::vector out; + for (const auto& v : violations) { + bool exists = false; + for (const auto& existing : out) { + if (existing.code == v.code && existing.field == v.field) { + exists = true; + break; + } + } + if (!exists) out.push_back(v); + } + violations.swap(out); + } + + static std::string decideAction(const std::vector& violations) { + if (violations.empty()) return "proceed"; + for (const auto& v : violations) { + if (!v.retryable) return "escalate"; + } + return "retry"; + } +}; diff --git a/editor/tests/step527_test.cpp b/editor/tests/step527_test.cpp new file mode 100644 index 0000000..f8c490b --- /dev/null +++ b/editor/tests/step527_test.cpp @@ -0,0 +1,223 @@ +// Step 527: Constraint Violation Diagnostics (12 tests) + +#include "ConstraintViolationDiagnostics.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 TaskitemContract makeContract() { + json j = { + {"id", "ti-527"}, + {"nodeId", "n-1"}, + {"language", "cpp"}, + {"allowedTargets", json::array({"Function"})}, + {"allowedOps", json::array({"rename", "update"})}, + {"allowedSymbols", json::array({"cursor", "count", "EditorState"})}, + {"forbiddenSymbols", json::array({"unsafeGlobal"})}, + {"expectedDiagnosticsAdd", json::array({"none"})}, + {"expectedDiagnosticsRemove", json::array()} + }; + auto result = TypedTaskitemContractSchema::parseAndValidate(j); + return result.contract; +} + +static ScopeSnapshot makeSnapshot(const SymbolScopeExtractor& extractor) { + return extractor.buildSnapshot("cpp", "Function", { + {"cursor", "Variable", 3}, + {"count", "Parameter", 3}, + {"EditorState", "Type", 0}, + {"render", "Function", 1} + }); +} + +static bool hasCode(const ConstraintDiagnosticPacket& packet, + const std::string& code) { + for (const auto& v : packet.violations) { + if (v.code == code) return true; + } + return false; +} + +static int countCode(const ConstraintDiagnosticPacket& packet, + const std::string& code) { + int count = 0; + for (const auto& v : packet.violations) { + if (v.code == code) ++count; + } + return count; +} + +void test_valid_request_produces_no_violations() { + TEST(valid_request_produces_no_violations); + LegalOperationGraph graph; + SymbolScopeExtractor extractor; + ConstraintCheckInput input{makeContract(), "cpp", "Function", "rename", + {"cursor", "count"}, makeSnapshot(extractor)}; + auto packet = ConstraintViolationDiagnostics::evaluate(input, graph, extractor); + CHECK(packet.ok, "packet should be ok"); + CHECK(packet.recommendedAction == "proceed", "action should be proceed"); + CHECK(packet.violations.empty(), "no violations expected"); + PASS(); +} + +void test_contract_forbidden_operation_is_reported() { + TEST(contract_forbidden_operation_is_reported); + LegalOperationGraph graph; + SymbolScopeExtractor extractor; + ConstraintCheckInput input{makeContract(), "cpp", "Function", "extract", + {"cursor"}, makeSnapshot(extractor)}; + auto packet = ConstraintViolationDiagnostics::evaluate(input, graph, extractor); + CHECK(hasCode(packet, "contract_forbidden_operation"), "missing violation"); + CHECK(packet.recommendedAction == "escalate", "non-retryable should escalate"); + PASS(); +} + +void test_illegal_graph_operation_is_reported() { + TEST(illegal_graph_operation_is_reported); + LegalOperationGraph graph; + SymbolScopeExtractor extractor; + ConstraintCheckInput input{makeContract(), "cpp", "Function", "delete", + {"cursor"}, makeSnapshot(extractor)}; + auto packet = ConstraintViolationDiagnostics::evaluate(input, graph, extractor); + CHECK(hasCode(packet, "illegal_operation"), "illegal op violation expected"); + PASS(); +} + +void test_unsupported_context_is_reported() { + TEST(unsupported_context_is_reported); + LegalOperationGraph graph; + SymbolScopeExtractor extractor; + ConstraintCheckInput input{makeContract(), "cpp", "MysteryNode", "rename", + {"cursor"}, makeSnapshot(extractor)}; + auto packet = ConstraintViolationDiagnostics::evaluate(input, graph, extractor); + CHECK(hasCode(packet, "unsupported_context"), "unsupported context violation expected"); + CHECK(packet.recommendedAction == "escalate", "unsupported context should escalate"); + PASS(); +} + +void test_out_of_scope_symbol_is_reported() { + TEST(out_of_scope_symbol_is_reported); + LegalOperationGraph graph; + SymbolScopeExtractor extractor; + ConstraintCheckInput input{makeContract(), "cpp", "Function", "rename", + {"ghost"}, makeSnapshot(extractor)}; + auto packet = ConstraintViolationDiagnostics::evaluate(input, graph, extractor); + CHECK(hasCode(packet, "out_of_scope_symbol"), "missing out-of-scope violation"); + PASS(); +} + +void test_retry_action_for_retryable_only_violations() { + TEST(retry_action_for_retryable_only_violations); + LegalOperationGraph graph; + SymbolScopeExtractor extractor; + auto contract = makeContract(); + contract.allowedSymbols.push_back("ghost"); + ConstraintCheckInput input{contract, "cpp", "Function", "rename", + {"ghost"}, makeSnapshot(extractor)}; + auto packet = ConstraintViolationDiagnostics::evaluate(input, graph, extractor); + CHECK(packet.recommendedAction == "retry", "retryable-only violations should retry"); + CHECK(countCode(packet, "out_of_scope_symbol") == 1, "expected out-of-scope violation"); + PASS(); +} + +void test_forbidden_symbol_is_reported() { + TEST(forbidden_symbol_is_reported); + LegalOperationGraph graph; + SymbolScopeExtractor extractor; + ConstraintCheckInput input{makeContract(), "cpp", "Function", "rename", + {"unsafeGlobal"}, makeSnapshot(extractor)}; + auto packet = ConstraintViolationDiagnostics::evaluate(input, graph, extractor); + CHECK(hasCode(packet, "forbidden_symbol"), "forbidden symbol violation expected"); + CHECK(packet.recommendedAction == "escalate", "forbidden symbol should escalate"); + PASS(); +} + +void test_contract_disallowed_symbol_is_reported() { + TEST(contract_disallowed_symbol_is_reported); + LegalOperationGraph graph; + SymbolScopeExtractor extractor; + ConstraintCheckInput input{makeContract(), "cpp", "Function", "rename", + {"render"}, makeSnapshot(extractor)}; + auto packet = ConstraintViolationDiagnostics::evaluate(input, graph, extractor); + CHECK(hasCode(packet, "contract_disallowed_symbol"), "contract disallowed violation expected"); + PASS(); +} + +void test_duplicate_symbol_requests_are_deduplicated() { + TEST(duplicate_symbol_requests_are_deduplicated); + LegalOperationGraph graph; + SymbolScopeExtractor extractor; + ConstraintCheckInput input{makeContract(), "cpp", "Function", "rename", + {"ghost", "ghost", "ghost"}, makeSnapshot(extractor)}; + auto packet = ConstraintViolationDiagnostics::evaluate(input, graph, extractor); + CHECK(countCode(packet, "out_of_scope_symbol") == 1, "out-of-scope violations should dedupe"); + CHECK(countCode(packet, "contract_disallowed_symbol") == 1, + "contract disallowed violations should dedupe"); + PASS(); +} + +void test_multiple_violation_types_are_aggregated() { + TEST(multiple_violation_types_are_aggregated); + LegalOperationGraph graph; + SymbolScopeExtractor extractor; + ConstraintCheckInput input{makeContract(), "cpp", "Function", "delete", + {"ghost", "unsafeGlobal"}, makeSnapshot(extractor)}; + auto packet = ConstraintViolationDiagnostics::evaluate(input, graph, extractor); + CHECK(hasCode(packet, "illegal_operation"), "missing illegal operation"); + CHECK(hasCode(packet, "contract_forbidden_operation"), "missing forbidden operation"); + CHECK(hasCode(packet, "out_of_scope_symbol"), "missing out-of-scope symbol"); + CHECK(hasCode(packet, "forbidden_symbol"), "missing forbidden symbol"); + PASS(); +} + +void test_to_json_contains_machine_readable_fields() { + TEST(to_json_contains_machine_readable_fields); + LegalOperationGraph graph; + SymbolScopeExtractor extractor; + ConstraintCheckInput input{makeContract(), "cpp", "Function", "rename", + {"ghost"}, makeSnapshot(extractor)}; + auto packet = ConstraintViolationDiagnostics::evaluate(input, graph, extractor); + auto encoded = packet.toJson(); + CHECK(encoded.contains("ok"), "json should contain ok"); + CHECK(encoded.contains("recommendedAction"), "json should contain recommendedAction"); + CHECK(encoded.contains("violations"), "json should contain violations"); + CHECK(encoded["violations"].is_array(), "violations should be array"); + PASS(); +} + +void test_empty_symbol_request_is_valid() { + TEST(empty_symbol_request_is_valid); + LegalOperationGraph graph; + SymbolScopeExtractor extractor; + ConstraintCheckInput input{makeContract(), "cpp", "Function", "rename", + {}, makeSnapshot(extractor)}; + auto packet = ConstraintViolationDiagnostics::evaluate(input, graph, extractor); + CHECK(packet.ok, "empty symbol set should be valid"); + CHECK(packet.recommendedAction == "proceed", "empty request should proceed"); + PASS(); +} + +int main() { + std::cout << "Step 527: Constraint Violation Diagnostics\n"; + + test_valid_request_produces_no_violations(); // 1 + test_contract_forbidden_operation_is_reported(); // 2 + test_illegal_graph_operation_is_reported(); // 3 + test_unsupported_context_is_reported(); // 4 + test_out_of_scope_symbol_is_reported(); // 5 + test_retry_action_for_retryable_only_violations(); // 6 + test_forbidden_symbol_is_reported(); // 7 + test_contract_disallowed_symbol_is_reported(); // 8 + test_duplicate_symbol_requests_are_deduplicated(); // 9 + test_multiple_violation_types_are_aggregated(); // 10 + test_to_json_contains_machine_readable_fields(); // 11 + test_empty_symbol_request_is_valid(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index f39175f..ce0945a 100644 --- a/progress.md +++ b/progress.md @@ -8659,3 +8659,41 @@ unresolved symbol requests before apply. - `editor/src/SymbolScopeExtractor.h` within header-size limit (`102` <= `600`) - `editor/tests/step526_test.cpp` within test-file size guidance (`158` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 527: Constraint Violation Diagnostics +**Status:** PASS (12/12 tests) + +Adds structured constraint diagnostics for constrained execution failures, +including out-of-scope symbol usage, illegal operations, and contract-policy +violations with machine-readable retry/escalation payloads. + +**Files added:** +- `editor/src/ConstraintViolationDiagnostics.h` - diagnostics module: + - evaluates operation legality against legal-op graph + taskitem contract + - evaluates symbol usage against extracted scope snapshot + contract policy + - emits structured violation records with retryability metadata + - computes deterministic recommended action (`proceed`/`retry`/`escalate`) + - serializes diagnostics packet to JSON for retry/escalation routing +- `editor/tests/step527_test.cpp` - 12 tests covering: + - clean-path packet behavior with no violations + - contract-forbidden and graph-illegal operation violations + - unsupported context violations + - out-of-scope and forbidden/disallowed symbol violations + - retry-only vs escalate action selection + - violation deduplication and multi-violation aggregation + - machine-readable JSON payload shape + - empty symbol request edge case + +**Files modified:** +- `editor/CMakeLists.txt` - `step527_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step527_test step526_test` - PASS +- `./editor/build-native/step527_test` - PASS (12/12) +- `./editor/build-native/step526_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/ConstraintViolationDiagnostics.h` within header-size limit (`169` <= `600`) +- `editor/tests/step527_test.cpp` within test-file size guidance (`223` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`