diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index f147da6..44b910c 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3595,4 +3595,13 @@ target_link_libraries(step523_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step524_test tests/step524_test.cpp) +target_include_directories(step524_test PRIVATE src) +target_link_libraries(step524_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/TypedTaskitemContractSchema.h b/editor/src/TypedTaskitemContractSchema.h new file mode 100644 index 0000000..9486e2f --- /dev/null +++ b/editor/src/TypedTaskitemContractSchema.h @@ -0,0 +1,114 @@ +#pragma once +// Step 524: Typed Taskitem Contract Schema + +#include +#include +#include +#include + +using json = nlohmann::json; + +struct TaskitemContract { + std::string id; + std::string nodeId; + std::string language; + std::vector allowedTargets; + std::vector allowedOps; + std::vector allowedSymbols; + std::vector forbiddenSymbols; + std::vector expectedDiagnosticsAdd; + std::vector expectedDiagnosticsRemove; +}; + +struct ContractValidationResult { + bool valid = false; + std::vector errors; + TaskitemContract contract; +}; + +class TypedTaskitemContractSchema { +public: + static ContractValidationResult parseAndValidate(const json& j) { + ContractValidationResult r; + + requireString(j, "id", r.errors); + requireString(j, "nodeId", r.errors); + requireString(j, "language", r.errors); + requireStringArray(j, "allowedTargets", r.errors); + requireStringArray(j, "allowedOps", r.errors); + requireStringArray(j, "allowedSymbols", r.errors); + requireStringArray(j, "forbiddenSymbols", r.errors); + requireStringArray(j, "expectedDiagnosticsAdd", r.errors); + requireStringArray(j, "expectedDiagnosticsRemove", r.errors); + + if (r.errors.empty()) { + r.contract.id = j["id"].get(); + r.contract.nodeId = j["nodeId"].get(); + r.contract.language = j["language"].get(); + r.contract.allowedTargets = j["allowedTargets"].get>(); + r.contract.allowedOps = j["allowedOps"].get>(); + r.contract.allowedSymbols = j["allowedSymbols"].get>(); + r.contract.forbiddenSymbols = j["forbiddenSymbols"].get>(); + r.contract.expectedDiagnosticsAdd = j["expectedDiagnosticsAdd"].get>(); + r.contract.expectedDiagnosticsRemove = j["expectedDiagnosticsRemove"].get>(); + + validateSemanticRules(r.contract, r.errors); + } + + r.valid = r.errors.empty(); + return r; + } + + static bool opAllowed(const TaskitemContract& c, const std::string& op) { + for (const auto& a : c.allowedOps) if (a == op) return true; + return false; + } + + static bool symbolAllowed(const TaskitemContract& c, const std::string& sym) { + for (const auto& f : c.forbiddenSymbols) if (f == sym) return false; + for (const auto& a : c.allowedSymbols) if (a == sym) return true; + return false; + } + +private: + static void requireString(const json& j, const std::string& key, + std::vector& errors) { + if (!j.contains(key) || !j[key].is_string() || j[key].get().empty()) { + errors.push_back("missing-or-invalid:" + key); + } + } + + static void requireStringArray(const json& j, const std::string& key, + std::vector& errors) { + if (!j.contains(key) || !j[key].is_array()) { + errors.push_back("missing-or-invalid:" + key); + return; + } + for (const auto& v : j[key]) { + if (!v.is_string() || v.get().empty()) { + errors.push_back("invalid-array-item:" + key); + return; + } + } + } + + static void validateSemanticRules(const TaskitemContract& c, + std::vector& errors) { + if (c.allowedTargets.empty()) errors.push_back("under-specified:allowedTargets"); + if (c.allowedOps.empty()) errors.push_back("under-specified:allowedOps"); + + std::set ops(c.allowedOps.begin(), c.allowedOps.end()); + if (ops.size() != c.allowedOps.size()) errors.push_back("duplicate:allowedOps"); + + std::set allowed(c.allowedSymbols.begin(), c.allowedSymbols.end()); + for (const auto& f : c.forbiddenSymbols) { + if (allowed.count(f) > 0) { + errors.push_back("symbol-conflict:" + f); + } + } + + if (c.expectedDiagnosticsAdd.empty() && c.expectedDiagnosticsRemove.empty()) { + errors.push_back("under-specified:expectedDiagnosticsDelta"); + } + } +}; diff --git a/editor/tests/step524_test.cpp b/editor/tests/step524_test.cpp new file mode 100644 index 0000000..52918f1 --- /dev/null +++ b/editor/tests/step524_test.cpp @@ -0,0 +1,148 @@ +// Step 524: Typed Taskitem Contract Schema (12 tests) + +#include "TypedTaskitemContractSchema.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 json validContractJson() { + return { + {"id", "ti-1"}, + {"nodeId", "fn-1"}, + {"language", "cpp"}, + {"allowedTargets", json::array({"fn-1", "var-2"})}, + {"allowedOps", json::array({"update", "rename"})}, + {"allowedSymbols", json::array({"foo", "bar"})}, + {"forbiddenSymbols", json::array({"baz"})}, + {"expectedDiagnosticsAdd", json::array({"E0201"})}, + {"expectedDiagnosticsRemove", json::array()} + }; +} + +void test_valid_contract_passes() { + TEST(valid_contract_passes); + auto r = TypedTaskitemContractSchema::parseAndValidate(validContractJson()); + CHECK(r.valid, "valid contract should pass"); + PASS(); +} + +void test_missing_id_rejected() { + TEST(missing_id_rejected); + auto j = validContractJson(); + j.erase("id"); + auto r = TypedTaskitemContractSchema::parseAndValidate(j); + CHECK(!r.valid, "missing id should fail"); + PASS(); +} + +void test_missing_allowed_targets_rejected() { + TEST(missing_allowed_targets_rejected); + auto j = validContractJson(); + j.erase("allowedTargets"); + auto r = TypedTaskitemContractSchema::parseAndValidate(j); + CHECK(!r.valid, "missing allowedTargets should fail"); + PASS(); +} + +void test_empty_allowed_ops_rejected() { + TEST(empty_allowed_ops_rejected); + auto j = validContractJson(); + j["allowedOps"] = json::array(); + auto r = TypedTaskitemContractSchema::parseAndValidate(j); + CHECK(!r.valid, "empty allowedOps should fail"); + PASS(); +} + +void test_duplicate_allowed_ops_rejected() { + TEST(duplicate_allowed_ops_rejected); + auto j = validContractJson(); + j["allowedOps"] = json::array({"update", "update"}); + auto r = TypedTaskitemContractSchema::parseAndValidate(j); + CHECK(!r.valid, "duplicate allowedOps should fail"); + PASS(); +} + +void test_symbol_conflict_rejected() { + TEST(symbol_conflict_rejected); + auto j = validContractJson(); + j["forbiddenSymbols"] = json::array({"foo"}); + auto r = TypedTaskitemContractSchema::parseAndValidate(j); + CHECK(!r.valid, "symbol overlap should fail"); + PASS(); +} + +void test_missing_diagnostics_delta_rejected() { + TEST(missing_diagnostics_delta_rejected); + auto j = validContractJson(); + j["expectedDiagnosticsAdd"] = json::array(); + j["expectedDiagnosticsRemove"] = json::array(); + auto r = TypedTaskitemContractSchema::parseAndValidate(j); + CHECK(!r.valid, "missing diagnostics delta should fail"); + PASS(); +} + +void test_invalid_array_item_rejected() { + TEST(invalid_array_item_rejected); + auto j = validContractJson(); + j["allowedSymbols"] = json::array({"foo", 1}); + auto r = TypedTaskitemContractSchema::parseAndValidate(j); + CHECK(!r.valid, "non-string array item should fail"); + PASS(); +} + +void test_op_allowed_true_for_listed_op() { + TEST(op_allowed_true_for_listed_op); + auto r = TypedTaskitemContractSchema::parseAndValidate(validContractJson()); + CHECK(r.valid && TypedTaskitemContractSchema::opAllowed(r.contract, "rename"), + "rename should be allowed"); + PASS(); +} + +void test_op_allowed_false_for_unlisted_op() { + TEST(op_allowed_false_for_unlisted_op); + auto r = TypedTaskitemContractSchema::parseAndValidate(validContractJson()); + CHECK(r.valid && !TypedTaskitemContractSchema::opAllowed(r.contract, "delete"), + "delete should not be allowed"); + PASS(); +} + +void test_symbol_allowed_false_for_forbidden() { + TEST(symbol_allowed_false_for_forbidden); + auto r = TypedTaskitemContractSchema::parseAndValidate(validContractJson()); + CHECK(r.valid && !TypedTaskitemContractSchema::symbolAllowed(r.contract, "baz"), + "forbidden symbol should be blocked"); + PASS(); +} + +void test_symbol_allowed_false_for_unknown() { + TEST(symbol_allowed_false_for_unknown); + auto r = TypedTaskitemContractSchema::parseAndValidate(validContractJson()); + CHECK(r.valid && !TypedTaskitemContractSchema::symbolAllowed(r.contract, "unknown"), + "unknown symbol should be blocked"); + PASS(); +} + +int main() { + std::cout << "Step 524: Typed Taskitem Contract Schema\n"; + + test_valid_contract_passes(); // 1 + test_missing_id_rejected(); // 2 + test_missing_allowed_targets_rejected(); // 3 + test_empty_allowed_ops_rejected(); // 4 + test_duplicate_allowed_ops_rejected(); // 5 + test_symbol_conflict_rejected(); // 6 + test_missing_diagnostics_delta_rejected(); // 7 + test_invalid_array_item_rejected(); // 8 + test_op_allowed_true_for_listed_op(); // 9 + test_op_allowed_false_for_unlisted_op(); // 10 + test_symbol_allowed_false_for_forbidden(); // 11 + test_symbol_allowed_false_for_unknown(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 1628c50..e2babdf 100644 --- a/progress.md +++ b/progress.md @@ -8557,3 +8557,37 @@ architecture-rule alignment without behavior changes. - `./editor/build-native/step521_test` - PASS (12/12) - `./editor/build-native/step522_test` - PASS (12/12) - `./editor/build-native/step523_test` - PASS (8/8) + +### Step 524: Typed Taskitem Contract Schema +**Status:** PASS (12/12 tests) + +Implements a strict typed taskitem contract schema for constrained execution +with required fields for allowed targets/ops/symbols, forbidden symbols, and +expected diagnostics deltas, rejecting under-specified taskitems. + +**Files added:** +- `editor/src/TypedTaskitemContractSchema.h` - schema module: + - JSON parse + required-field validation + - semantic consistency rules (duplicate op detection, symbol conflict detection) + - under-specification guards for constrained execution path + - helper checks for operation/symbol legality +- `editor/tests/step524_test.cpp` - 12 tests covering: + - valid contract acceptance + - required-field rejection paths + - duplicate-op and symbol-conflict rejection + - diagnostics-delta under-specification rejection + - operation/symbol allow/deny helper behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step524_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step524_test` - PASS +- `./editor/build-native/step524_test` - PASS (12/12) +- `./editor/build-native/step523_test` - PASS (8/8) regression coverage + +**Architecture gate check:** +- `editor/src/TypedTaskitemContractSchema.h` within header-size limit (`114` <= `600`) +- `editor/tests/step524_test.cpp` within test-file size guidance (`148` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`