Step 536: add argument shape validator for constrained ops

This commit is contained in:
Bill
2026-02-17 09:34:31 -07:00
parent 8503300da6
commit af9fd6b547
4 changed files with 302 additions and 0 deletions

View File

@@ -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)

View File

@@ -0,0 +1,113 @@
#pragma once
// Step 536: Argument Shape Validator
#include <set>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#include "TypedTaskitemContractSchema.h"
using json = nlohmann::json;
struct ArgumentShapeResult {
bool valid = false;
std::vector<std::string> 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<std::string>& errors) {
if (!args.contains(key) || !args[key].is_string() || args[key].get<std::string>().empty()) {
errors.push_back("missing_or_invalid_string:" + key);
}
}
static void requireBoolean(const json& args,
const std::string& key,
std::vector<std::string>& 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<std::string>& errors) {
if (!args.contains(key) || !args[key].is_string()) return;
if (args[key].get<std::string>() == value) {
errors.push_back(code);
}
}
static void validateEnum(const json& args,
const std::string& key,
const std::set<std::string>& allowed,
std::vector<std::string>& errors) {
if (!args.contains(key) || !args[key].is_string()) return;
if (allowed.count(args[key].get<std::string>()) == 0) {
errors.push_back("invalid_enum:" + key);
}
}
static void forbidKeys(const json& args,
const std::set<std::string>& forbidden,
std::vector<std::string>& errors) {
for (const auto& key : forbidden) {
if (args.contains(key)) {
errors.push_back("forbidden_key:" + key);
}
}
}
};

View File

@@ -0,0 +1,144 @@
// Step 536: Argument Shape Validator (12 tests)
#include "ArgumentShapeValidator.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 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;
}

View File

@@ -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`