From af9fd6b5474c2ae1b28087a328ef73db56f232ec Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 09:34:31 -0700 Subject: [PATCH] Step 536: add argument shape validator for constrained ops --- editor/CMakeLists.txt | 9 ++ editor/src/ArgumentShapeValidator.h | 113 ++++++++++++++++++++++ editor/tests/step536_test.cpp | 144 ++++++++++++++++++++++++++++ progress.md | 36 +++++++ 4 files changed, 302 insertions(+) create mode 100644 editor/src/ArgumentShapeValidator.h create mode 100644 editor/tests/step536_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index a040082..b53fe0f 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3703,4 +3703,13 @@ target_link_libraries(step535_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step536_test tests/step536_test.cpp) +target_include_directories(step536_test PRIVATE src) +target_link_libraries(step536_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/ArgumentShapeValidator.h b/editor/src/ArgumentShapeValidator.h new file mode 100644 index 0000000..53ace86 --- /dev/null +++ b/editor/src/ArgumentShapeValidator.h @@ -0,0 +1,113 @@ +#pragma once +// Step 536: Argument Shape Validator + +#include +#include +#include + +#include + +#include "TypedTaskitemContractSchema.h" + +using json = nlohmann::json; + +struct ArgumentShapeResult { + bool valid = false; + std::vector errors; +}; + +class ArgumentShapeValidator { +public: + static ArgumentShapeResult validate(const TaskitemContract& contract, + const std::string& operation, + const std::string& symbol, + const json& args) { + ArgumentShapeResult result; + + if (!TypedTaskitemContractSchema::opAllowed(contract, operation)) { + result.errors.push_back("op_not_allowed:" + operation); + } + if (!TypedTaskitemContractSchema::symbolAllowed(contract, symbol)) { + result.errors.push_back("symbol_not_allowed:" + symbol); + } + if (!args.is_object()) { + result.errors.push_back("args_not_object"); + result.valid = false; + return result; + } + + if (operation == "rename") { + requireString(args, "newName", result.errors); + rejectSameValue(args, "newName", symbol, "rename_noop", result.errors); + forbidKeys(args, {"replacement", "insertText", "position", "mode", "target"}, result.errors); + } else if (operation == "update") { + requireString(args, "replacement", result.errors); + forbidKeys(args, {"newName", "insertText", "position", "target"}, result.errors); + } else if (operation == "insert") { + requireString(args, "insertText", result.errors); + requireString(args, "position", result.errors); + validateEnum(args, "position", {"before", "after", "inside"}, result.errors); + forbidKeys(args, {"newName", "replacement"}, result.errors); + } else if (operation == "extract") { + requireString(args, "target", result.errors); + requireString(args, "mode", result.errors); + validateEnum(args, "mode", {"function", "constant"}, result.errors); + } else if (operation == "delete") { + requireBoolean(args, "confirm", result.errors); + forbidKeys(args, {"newName", "replacement", "insertText"}, result.errors); + } else { + result.errors.push_back("unsupported_operation_shape:" + operation); + } + + result.valid = result.errors.empty(); + return result; + } + +private: + static void requireString(const json& args, + const std::string& key, + std::vector& errors) { + if (!args.contains(key) || !args[key].is_string() || args[key].get().empty()) { + errors.push_back("missing_or_invalid_string:" + key); + } + } + + static void requireBoolean(const json& args, + const std::string& key, + std::vector& errors) { + if (!args.contains(key) || !args[key].is_boolean()) { + errors.push_back("missing_or_invalid_boolean:" + key); + } + } + + static void rejectSameValue(const json& args, + const std::string& key, + const std::string& value, + const std::string& code, + std::vector& errors) { + if (!args.contains(key) || !args[key].is_string()) return; + if (args[key].get() == value) { + errors.push_back(code); + } + } + + static void validateEnum(const json& args, + const std::string& key, + const std::set& allowed, + std::vector& errors) { + if (!args.contains(key) || !args[key].is_string()) return; + if (allowed.count(args[key].get()) == 0) { + errors.push_back("invalid_enum:" + key); + } + } + + static void forbidKeys(const json& args, + const std::set& forbidden, + std::vector& errors) { + for (const auto& key : forbidden) { + if (args.contains(key)) { + errors.push_back("forbidden_key:" + key); + } + } + } +}; diff --git a/editor/tests/step536_test.cpp b/editor/tests/step536_test.cpp new file mode 100644 index 0000000..6503848 --- /dev/null +++ b/editor/tests/step536_test.cpp @@ -0,0 +1,144 @@ +// Step 536: Argument Shape Validator (12 tests) + +#include "ArgumentShapeValidator.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 contract() { + TaskitemContract c; + c.id = "ti-536"; + c.nodeId = "n-1"; + c.language = "cpp"; + c.allowedTargets = {"Function"}; + c.allowedOps = {"rename", "update", "insert", "extract", "delete"}; + c.allowedSymbols = {"cursor", "count", "render"}; + c.forbiddenSymbols = {"unsafeGlobal"}; + c.expectedDiagnosticsAdd = {"none"}; + return c; +} + +static bool hasError(const ArgumentShapeResult& r, const std::string& codePrefix) { + for (const auto& e : r.errors) { + if (e.find(codePrefix) == 0) return true; + } + return false; +} + +void test_valid_rename_args_pass() { + TEST(valid_rename_args_pass); + auto r = ArgumentShapeValidator::validate(contract(), "rename", "cursor", + json{{"newName", "cursor2"}}); + CHECK(r.valid, "valid rename should pass"); + PASS(); +} + +void test_rename_missing_new_name_fails() { + TEST(rename_missing_new_name_fails); + auto r = ArgumentShapeValidator::validate(contract(), "rename", "cursor", json::object()); + CHECK(hasError(r, "missing_or_invalid_string:newName"), "newName error expected"); + PASS(); +} + +void test_rename_noop_fails() { + TEST(rename_noop_fails); + auto r = ArgumentShapeValidator::validate(contract(), "rename", "cursor", + json{{"newName", "cursor"}}); + CHECK(hasError(r, "rename_noop"), "rename noop should fail"); + PASS(); +} + +void test_update_requires_replacement() { + TEST(update_requires_replacement); + auto r = ArgumentShapeValidator::validate(contract(), "update", "cursor", json::object()); + CHECK(hasError(r, "missing_or_invalid_string:replacement"), "replacement error expected"); + PASS(); +} + +void test_insert_requires_position_and_insert_text() { + TEST(insert_requires_position_and_insert_text); + auto r = ArgumentShapeValidator::validate(contract(), "insert", "cursor", + json{{"insertText", "x"}}); + CHECK(hasError(r, "missing_or_invalid_string:position"), "position required"); + PASS(); +} + +void test_insert_rejects_invalid_position_enum() { + TEST(insert_rejects_invalid_position_enum); + auto r = ArgumentShapeValidator::validate(contract(), "insert", "cursor", + json{{"insertText", "x"}, {"position", "middle"}}); + CHECK(hasError(r, "invalid_enum:position"), "invalid position enum expected"); + PASS(); +} + +void test_extract_requires_target_and_mode() { + TEST(extract_requires_target_and_mode); + auto r = ArgumentShapeValidator::validate(contract(), "extract", "cursor", json::object()); + CHECK(hasError(r, "missing_or_invalid_string:target"), "target required"); + CHECK(hasError(r, "missing_or_invalid_string:mode"), "mode required"); + PASS(); +} + +void test_extract_rejects_invalid_mode() { + TEST(extract_rejects_invalid_mode); + auto r = ArgumentShapeValidator::validate(contract(), "extract", "cursor", + json{{"target", "tmp"}, {"mode", "lambda"}}); + CHECK(hasError(r, "invalid_enum:mode"), "invalid mode expected"); + PASS(); +} + +void test_delete_requires_confirm_bool() { + TEST(delete_requires_confirm_bool); + auto r = ArgumentShapeValidator::validate(contract(), "delete", "cursor", json::object()); + CHECK(hasError(r, "missing_or_invalid_boolean:confirm"), "confirm required"); + PASS(); +} + +void test_disallowed_operation_fails() { + TEST(disallowed_operation_fails); + auto c = contract(); + c.allowedOps = {"rename"}; + auto r = ArgumentShapeValidator::validate(c, "delete", "cursor", json{{"confirm", true}}); + CHECK(hasError(r, "op_not_allowed:delete"), "disallowed op should fail"); + PASS(); +} + +void test_disallowed_symbol_fails() { + TEST(disallowed_symbol_fails); + auto r = ArgumentShapeValidator::validate(contract(), "rename", "ghost", + json{{"newName", "ghost2"}}); + CHECK(hasError(r, "symbol_not_allowed:ghost"), "disallowed symbol should fail"); + PASS(); +} + +void test_args_must_be_object() { + TEST(args_must_be_object); + auto r = ArgumentShapeValidator::validate(contract(), "rename", "cursor", json::array()); + CHECK(hasError(r, "args_not_object"), "args object requirement expected"); + PASS(); +} + +int main() { + std::cout << "Step 536: Argument Shape Validator\n"; + + test_valid_rename_args_pass(); // 1 + test_rename_missing_new_name_fails(); // 2 + test_rename_noop_fails(); // 3 + test_update_requires_replacement(); // 4 + test_insert_requires_position_and_insert_text(); // 5 + test_insert_rejects_invalid_position_enum(); // 6 + test_extract_requires_target_and_mode(); // 7 + test_extract_rejects_invalid_mode(); // 8 + test_delete_requires_confirm_bool(); // 9 + test_disallowed_operation_fails(); // 10 + test_disallowed_symbol_fails(); // 11 + test_args_must_be_object(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 5f05114..489aa3e 100644 --- a/progress.md +++ b/progress.md @@ -9037,3 +9037,39 @@ category-based filtering. - `editor/src/SymbolSelectorAPI.h` within header-size limit (`81` <= `600`) - `editor/tests/step535_test.cpp` within test-file size guidance (`163` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 536: Argument Shape Validator +**Status:** PASS (12/12 tests) + +Implements argument-shape validation for constrained operations, enforcing +operation-specific required fields, enum constraints, forbidden keys, and +contract legality for operation/symbol pairs. + +**Files added:** +- `editor/src/ArgumentShapeValidator.h` - argument shape validation module: + - operation/symbol contract allow checks + - per-operation required argument schemas (`rename/update/insert/extract/delete`) + - enum validation for constrained argument fields + - semantic no-op guard for rename operations + - malformed-argument object checks and forbidden-key enforcement +- `editor/tests/step536_test.cpp` - 12 tests covering: + - valid rename path + - required field failures per operation + - enum validation failures + - delete confirm-type requirement + - disallowed op/symbol failure paths + - malformed args (non-object) edge case + +**Files modified:** +- `editor/CMakeLists.txt` - `step536_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step536_test step535_test` - PASS +- `./editor/build-native/step536_test` - PASS (12/12) +- `./editor/build-native/step535_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/ArgumentShapeValidator.h` within header-size limit (`113` <= `600`) +- `editor/tests/step536_test.cpp` within test-file size guidance (`144` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`