Step 527: add structured constraint violation diagnostics
This commit is contained in:
@@ -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)
|
||||
|
||||
169
editor/src/ConstraintViolationDiagnostics.h
Normal file
169
editor/src/ConstraintViolationDiagnostics.h
Normal file
@@ -0,0 +1,169 @@
|
||||
#pragma once
|
||||
// Step 527: Constraint Violation Diagnostics
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#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<ConstraintViolation> 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<std::string> 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<ConstraintViolation>& violations) {
|
||||
std::vector<ConstraintViolation> 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<ConstraintViolation>& violations) {
|
||||
if (violations.empty()) return "proceed";
|
||||
for (const auto& v : violations) {
|
||||
if (!v.retryable) return "escalate";
|
||||
}
|
||||
return "retry";
|
||||
}
|
||||
};
|
||||
223
editor/tests/step527_test.cpp
Normal file
223
editor/tests/step527_test.cpp
Normal file
@@ -0,0 +1,223 @@
|
||||
// Step 527: Constraint Violation Diagnostics (12 tests)
|
||||
|
||||
#include "ConstraintViolationDiagnostics.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 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;
|
||||
}
|
||||
38
progress.md
38
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`
|
||||
|
||||
Reference in New Issue
Block a user