From dc3177d5e1dd4b354a7892b66fa0fc24e1e12c4c Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 09:31:54 -0700 Subject: [PATCH] Step 534: add constrained operation selector API --- editor/CMakeLists.txt | 9 ++ editor/src/OperationSelectorAPI.h | 88 +++++++++++++ editor/tests/step534_test.cpp | 208 ++++++++++++++++++++++++++++++ progress.md | 36 ++++++ 4 files changed, 341 insertions(+) create mode 100644 editor/src/OperationSelectorAPI.h create mode 100644 editor/tests/step534_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 15d5e2b..4a80323 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3685,4 +3685,13 @@ target_link_libraries(step533_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step534_test tests/step534_test.cpp) +target_include_directories(step534_test PRIVATE src) +target_link_libraries(step534_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/OperationSelectorAPI.h b/editor/src/OperationSelectorAPI.h new file mode 100644 index 0000000..883693c --- /dev/null +++ b/editor/src/OperationSelectorAPI.h @@ -0,0 +1,88 @@ +#pragma once +// Step 534: Operation Selector API + +#include +#include +#include +#include + +#include "LegalOperationGraph.h" +#include "SymbolScopeExtractor.h" +#include "TypedTaskitemContractSchema.h" + +struct OperationCandidate { + std::string operation; + int score = 0; + std::vector reasons; +}; + +struct OperationSelectionResult { + bool supported = false; + std::vector candidates; +}; + +class OperationSelectorAPI { +public: + static OperationSelectionResult select( + const TaskitemContract& contract, + const std::string& nodeKind, + const ScopeSnapshot& snapshot, + const LegalOperationGraph& graph, + const std::vector& preferredOps = {}) { + OperationSelectionResult result; + auto legal = graph.allowedOps(contract.language, nodeKind); + if (!legal.supported) return result; + + result.supported = true; + auto preferred = asSet(preferredOps); + for (const auto& op : legal.operations) { + if (!TypedTaskitemContractSchema::opAllowed(contract, op)) continue; + OperationCandidate c; + c.operation = op; + c.score = baseScore(op); + if (preferred.count(op) > 0) { + c.score += 40; + c.reasons.push_back("preferred_op"); + } + if (snapshot.supported && !snapshot.candidates.empty()) { + c.score += 15; + c.reasons.push_back("symbols_in_scope"); + } else { + c.score -= 10; + c.reasons.push_back("weak_scope_context"); + } + if (op == "rename" || op == "update") { + c.score += 10; + c.reasons.push_back("low_constraint_risk"); + } + if (op == "delete") { + c.score -= 25; + c.reasons.push_back("high_constraint_risk"); + } + result.candidates.push_back(c); + } + + std::sort(result.candidates.begin(), result.candidates.end(), + [](const OperationCandidate& a, const OperationCandidate& b) { + if (a.score != b.score) return a.score > b.score; + return a.operation < b.operation; + }); + return result; + } + +private: + static int baseScore(const std::string& op) { + if (op == "rename") return 100; + if (op == "update") return 95; + if (op == "extract") return 85; + if (op == "inline") return 75; + if (op == "insert") return 70; + if (op == "reorder") return 65; + if (op == "delete") return 50; + return 40; + } + + static std::set asSet(const std::vector& values) { + return std::set(values.begin(), values.end()); + } +}; diff --git a/editor/tests/step534_test.cpp b/editor/tests/step534_test.cpp new file mode 100644 index 0000000..fd9d1ec --- /dev/null +++ b/editor/tests/step534_test.cpp @@ -0,0 +1,208 @@ +// Step 534: Operation Selector API (12 tests) + +#include "OperationSelectorAPI.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() { + TaskitemContract c; + c.id = "ti-534"; + c.nodeId = "n-1"; + c.language = "cpp"; + c.allowedTargets = {"Function"}; + c.allowedOps = {"rename", "update", "extract", "inline", "delete"}; + c.allowedSymbols = {"cursor", "count"}; + c.forbiddenSymbols = {"unsafeGlobal"}; + c.expectedDiagnosticsAdd = {"none"}; + c.expectedDiagnosticsRemove = {}; + return c; +} + +static ScopeSnapshot supportedScope() { + ScopeSnapshot s; + s.supported = true; + s.candidates = {{"cursor", "Variable", 2}}; + return s; +} + +void test_supported_node_returns_candidates() { + TEST(supported_node_returns_candidates); + LegalOperationGraph graph; + auto r = OperationSelectorAPI::select(makeContract(), "Function", supportedScope(), graph); + CHECK(r.supported, "function node should be supported"); + CHECK(!r.candidates.empty(), "candidates expected"); + PASS(); +} + +void test_unsupported_node_returns_not_supported() { + TEST(unsupported_node_returns_not_supported); + LegalOperationGraph graph; + auto r = OperationSelectorAPI::select(makeContract(), "MacroBlock", supportedScope(), graph); + CHECK(!r.supported, "unsupported node should not be supported"); + CHECK(r.candidates.empty(), "unsupported node should have no candidates"); + PASS(); +} + +void test_contract_filters_out_disallowed_ops() { + TEST(contract_filters_out_disallowed_ops); + LegalOperationGraph graph; + auto c = makeContract(); + c.allowedOps = {"rename"}; + auto r = OperationSelectorAPI::select(c, "Function", supportedScope(), graph); + CHECK(r.candidates.size() == 1, "only one op should remain"); + CHECK(r.candidates[0].operation == "rename", "rename should remain"); + PASS(); +} + +void test_candidates_sorted_by_score_descending() { + TEST(candidates_sorted_by_score_descending); + LegalOperationGraph graph; + auto r = OperationSelectorAPI::select(makeContract(), "Function", supportedScope(), graph); + CHECK(r.candidates.size() >= 2, "need at least two candidates"); + CHECK(r.candidates[0].score >= r.candidates[1].score, "candidates should be score-sorted"); + PASS(); +} + +void test_preferred_ops_receive_score_boost() { + TEST(preferred_ops_receive_score_boost); + LegalOperationGraph graph; + auto r = OperationSelectorAPI::select(makeContract(), "Function", supportedScope(), graph, + {"extract"}); + int extractScore = -1; + int inlineScore = -1; + for (const auto& c : r.candidates) { + if (c.operation == "extract") extractScore = c.score; + if (c.operation == "inline") inlineScore = c.score; + } + CHECK(extractScore > inlineScore, "preferred extract should outrank inline"); + PASS(); +} + +void test_scope_context_penalizes_when_empty() { + TEST(scope_context_penalizes_when_empty); + LegalOperationGraph graph; + ScopeSnapshot weak; + weak.supported = true; + auto withScope = OperationSelectorAPI::select(makeContract(), "Function", supportedScope(), graph); + auto weakScope = OperationSelectorAPI::select(makeContract(), "Function", weak, graph); + CHECK(!withScope.candidates.empty() && !weakScope.candidates.empty(), "need candidates"); + CHECK(withScope.candidates[0].score > weakScope.candidates[0].score, + "empty scope should lower top score"); + PASS(); +} + +void test_rename_and_update_get_low_risk_bonus() { + TEST(rename_and_update_get_low_risk_bonus); + LegalOperationGraph graph; + auto c = makeContract(); + c.allowedOps = {"rename", "update", "delete"}; + auto r = OperationSelectorAPI::select(c, "Variable", supportedScope(), graph); + int renameScore = -1; + int deleteScore = -1; + for (const auto& c : r.candidates) { + if (c.operation == "rename") renameScore = c.score; + if (c.operation == "delete") deleteScore = c.score; + } + CHECK(renameScore > deleteScore, "rename should outrank delete due to risk weighting"); + PASS(); +} + +void test_high_risk_delete_gets_penalty() { + TEST(high_risk_delete_gets_penalty); + LegalOperationGraph graph; + auto c = makeContract(); + c.allowedOps = {"rename", "update", "delete"}; + auto r = OperationSelectorAPI::select(c, "Variable", supportedScope(), graph); + for (const auto& c : r.candidates) { + if (c.operation == "delete") { + CHECK(c.score < 80, "delete should be strongly penalized"); + PASS(); + return; + } + } + FAIL("delete candidate not found"); +} + +void test_reasons_include_preference_and_scope_tags() { + TEST(reasons_include_preference_and_scope_tags); + LegalOperationGraph graph; + auto r = OperationSelectorAPI::select(makeContract(), "Function", supportedScope(), graph, + {"rename"}); + for (const auto& c : r.candidates) { + if (c.operation == "rename") { + bool hasPreferred = false; + bool hasScope = false; + for (const auto& reason : c.reasons) { + if (reason == "preferred_op") hasPreferred = true; + if (reason == "symbols_in_scope") hasScope = true; + } + CHECK(hasPreferred, "preferred tag expected"); + CHECK(hasScope, "scope tag expected"); + PASS(); + return; + } + } + FAIL("rename candidate not found"); +} + +void test_tie_breaker_orders_by_operation_name() { + TEST(tie_breaker_orders_by_operation_name); + LegalOperationGraph graph; + auto c = makeContract(); + c.allowedOps = {"alpha", "beta"}; + // Override legal graph for deterministic equal base scores. + graph.registerRule("cpp", "Function", {"alpha", "beta"}); + ScopeSnapshot weak; + weak.supported = false; + auto r = OperationSelectorAPI::select(c, "Function", weak, graph); + CHECK(r.candidates.size() == 2, "two candidates expected"); + CHECK(r.candidates[0].operation == "alpha", "lexicographic tie-break expected"); + PASS(); +} + +void test_empty_allowed_ops_yields_no_candidates() { + TEST(empty_allowed_ops_yields_no_candidates); + LegalOperationGraph graph; + auto c = makeContract(); + c.allowedOps.clear(); + auto r = OperationSelectorAPI::select(c, "Function", supportedScope(), graph); + CHECK(r.supported, "node support should remain true"); + CHECK(r.candidates.empty(), "no allowed ops should yield no candidates"); + PASS(); +} + +void test_unknown_language_in_contract_not_supported() { + TEST(unknown_language_in_contract_not_supported); + LegalOperationGraph graph; + auto c = makeContract(); + c.language = "haskell"; + auto r = OperationSelectorAPI::select(c, "Function", supportedScope(), graph); + CHECK(!r.supported, "unknown language should not be supported"); + PASS(); +} + +int main() { + std::cout << "Step 534: Operation Selector API\n"; + + test_supported_node_returns_candidates(); // 1 + test_unsupported_node_returns_not_supported(); // 2 + test_contract_filters_out_disallowed_ops(); // 3 + test_candidates_sorted_by_score_descending(); // 4 + test_preferred_ops_receive_score_boost(); // 5 + test_scope_context_penalizes_when_empty(); // 6 + test_rename_and_update_get_low_risk_bonus(); // 7 + test_high_risk_delete_gets_penalty(); // 8 + test_reasons_include_preference_and_scope_tags(); // 9 + test_tie_breaker_orders_by_operation_name(); // 10 + test_empty_allowed_ops_yields_no_candidates(); // 11 + test_unknown_language_in_contract_not_supported(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 90879df..778801f 100644 --- a/progress.md +++ b/progress.md @@ -8966,3 +8966,39 @@ re-validate architecture constraints without changing runtime behavior. - `./editor/build-native/step531_test` - PASS (12/12) - `./editor/build-native/step532_test` - PASS (12/12) - `./editor/build-native/step533_test` - PASS (8/8) + +### Step 534: Operation Selector API +**Status:** PASS (12/12 tests) + +Implements legal-operation selection for constrained editing by exposing only +operations legal for the active language/node context and taskitem contract, +ranked by predicted constraint-success signals. + +**Files added:** +- `editor/src/OperationSelectorAPI.h` - operation selection module: + - intersects legal-op graph results with taskitem allowed ops + - ranks candidates using risk/context/preference heuristics + - emits candidate reasons for selector explainability + - deterministic score + tie-break ordering +- `editor/tests/step534_test.cpp` - 12 tests covering: + - supported/unsupported context behavior + - contract op filtering behavior + - score sorting and tie-break behavior + - preferred-op boost behavior + - scope-context weighting behavior + - low-risk/high-risk scoring behavior + - empty allowed-op and unknown-language edge cases + +**Files modified:** +- `editor/CMakeLists.txt` - `step534_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step534_test step533_test` - PASS +- `./editor/build-native/step534_test` - PASS (12/12) +- `./editor/build-native/step533_test` - PASS (8/8) regression coverage + +**Architecture gate check:** +- `editor/src/OperationSelectorAPI.h` within header-size limit (`88` <= `600`) +- `editor/tests/step534_test.cpp` within test-file size guidance (`208` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`