From 8503300da67bc20c40ce72a7ed1188d718bd57af Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 09:33:06 -0700 Subject: [PATCH] Step 535: add constrained symbol selector API --- editor/CMakeLists.txt | 9 ++ editor/src/SymbolSelectorAPI.h | 81 ++++++++++++++++ editor/tests/step535_test.cpp | 163 +++++++++++++++++++++++++++++++++ progress.md | 35 +++++++ 4 files changed, 288 insertions(+) create mode 100644 editor/src/SymbolSelectorAPI.h create mode 100644 editor/tests/step535_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 4a80323..a040082 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3694,4 +3694,13 @@ target_link_libraries(step534_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step535_test tests/step535_test.cpp) +target_include_directories(step535_test PRIVATE src) +target_link_libraries(step535_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/SymbolSelectorAPI.h b/editor/src/SymbolSelectorAPI.h new file mode 100644 index 0000000..d5daf41 --- /dev/null +++ b/editor/src/SymbolSelectorAPI.h @@ -0,0 +1,81 @@ +#pragma once +// Step 535: Symbol Selector API + +#include +#include +#include +#include + +#include "TypedTaskitemContractSchema.h" +#include "SymbolScopeExtractor.h" + +struct SymbolCandidate { + std::string name; + std::string kind; + std::string category; + int scopeDepth = 0; + bool contractAllowed = false; +}; + +struct SymbolSelectionResult { + bool supported = false; + std::vector candidates; +}; + +class SymbolSelectorAPI { +public: + static SymbolSelectionResult select(const TaskitemContract& contract, + const ScopeSnapshot& scope, + const std::vector& categories = {}) { + SymbolSelectionResult result; + if (!scope.supported) return result; + result.supported = true; + + auto filters = asSet(categories); + for (const auto& symbol : scope.candidates) { + if (!TypedTaskitemContractSchema::symbolAllowed(contract, symbol.name)) continue; + SymbolCandidate c; + c.name = symbol.name; + c.kind = symbol.kind; + c.category = toCategory(symbol.kind); + c.scopeDepth = symbol.scopeDepth; + c.contractAllowed = true; + if (!filters.empty() && filters.count(c.category) == 0) continue; + result.candidates.push_back(c); + } + + std::sort(result.candidates.begin(), result.candidates.end(), + [](const SymbolCandidate& a, const SymbolCandidate& b) { + if (a.scopeDepth != b.scopeDepth) return a.scopeDepth > b.scopeDepth; + if (a.category != b.category) return a.category < b.category; + return a.name < b.name; + }); + dedupeByName(result.candidates); + return result; + } + +private: + static std::string toCategory(const std::string& kind) { + if (kind == "Function" || kind == "Method") return "function"; + if (kind == "Type" || kind == "Class" || kind == "Trait") return "type"; + if (kind == "Field") return "field"; + if (kind == "Namespace" || kind == "Import" || kind == "Module") return "module"; + if (kind == "Constant" || kind == "Enum") return "constant"; + return "symbol"; + } + + static std::set asSet(const std::vector& values) { + return std::set(values.begin(), values.end()); + } + + static void dedupeByName(std::vector& candidates) { + std::set seen; + std::vector out; + for (const auto& candidate : candidates) { + if (seen.insert(candidate.name).second) { + out.push_back(candidate); + } + } + candidates.swap(out); + } +}; diff --git a/editor/tests/step535_test.cpp b/editor/tests/step535_test.cpp new file mode 100644 index 0000000..c4d30a5 --- /dev/null +++ b/editor/tests/step535_test.cpp @@ -0,0 +1,163 @@ +// Step 535: Symbol Selector API (12 tests) + +#include "SymbolSelectorAPI.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-535"; + c.nodeId = "n-1"; + c.language = "cpp"; + c.allowedTargets = {"Function"}; + c.allowedOps = {"rename"}; + c.allowedSymbols = {"render", "EditorState", "value", "MAX_COUNT", "std", "fieldA"}; + c.forbiddenSymbols = {"unsafeGlobal"}; + c.expectedDiagnosticsAdd = {"none"}; + return c; +} + +static ScopeSnapshot richScope() { + ScopeSnapshot s; + s.supported = true; + s.candidates = { + {"render", "Function", 2}, + {"EditorState", "Type", 0}, + {"value", "Variable", 3}, + {"MAX_COUNT", "Constant", 0}, + {"std", "Namespace", 0}, + {"fieldA", "Field", 2}, + {"unsafeGlobal", "Variable", 0} + }; + return s; +} + +void test_supported_scope_returns_candidates() { + TEST(supported_scope_returns_candidates); + auto r = SymbolSelectorAPI::select(contract(), richScope()); + CHECK(r.supported, "scope should be supported"); + CHECK(!r.candidates.empty(), "candidates expected"); + PASS(); +} + +void test_unsupported_scope_returns_not_supported() { + TEST(unsupported_scope_returns_not_supported); + ScopeSnapshot s; + s.supported = false; + auto r = SymbolSelectorAPI::select(contract(), s); + CHECK(!r.supported, "unsupported scope expected"); + CHECK(r.candidates.empty(), "no candidates expected"); + PASS(); +} + +void test_forbidden_symbols_are_not_returned() { + TEST(forbidden_symbols_are_not_returned); + auto r = SymbolSelectorAPI::select(contract(), richScope()); + for (const auto& c : r.candidates) { + CHECK(c.name != "unsafeGlobal", "forbidden symbol should be excluded"); + } + PASS(); +} + +void test_contract_allowed_symbols_only() { + TEST(contract_allowed_symbols_only); + auto c = contract(); + c.allowedSymbols = {"render"}; + auto r = SymbolSelectorAPI::select(c, richScope()); + CHECK(r.candidates.size() == 1, "only one symbol should remain"); + CHECK(r.candidates[0].name == "render", "render should remain"); + PASS(); +} + +void test_function_category_filter() { + TEST(function_category_filter); + auto r = SymbolSelectorAPI::select(contract(), richScope(), {"function"}); + CHECK(r.candidates.size() == 1, "one function expected"); + CHECK(r.candidates[0].name == "render", "render should match function category"); + PASS(); +} + +void test_type_category_filter() { + TEST(type_category_filter); + auto r = SymbolSelectorAPI::select(contract(), richScope(), {"type"}); + CHECK(r.candidates.size() == 1, "one type expected"); + CHECK(r.candidates[0].name == "EditorState", "type category mismatch"); + PASS(); +} + +void test_field_category_filter() { + TEST(field_category_filter); + auto r = SymbolSelectorAPI::select(contract(), richScope(), {"field"}); + CHECK(r.candidates.size() == 1, "one field expected"); + CHECK(r.candidates[0].name == "fieldA", "field category mismatch"); + PASS(); +} + +void test_module_category_filter() { + TEST(module_category_filter); + auto r = SymbolSelectorAPI::select(contract(), richScope(), {"module"}); + CHECK(r.candidates.size() == 1, "one module expected"); + CHECK(r.candidates[0].name == "std", "module category mismatch"); + PASS(); +} + +void test_constant_category_filter() { + TEST(constant_category_filter); + auto r = SymbolSelectorAPI::select(contract(), richScope(), {"constant"}); + CHECK(r.candidates.size() == 1, "one constant expected"); + CHECK(r.candidates[0].name == "MAX_COUNT", "constant category mismatch"); + PASS(); +} + +void test_multiple_category_filter() { + TEST(multiple_category_filter); + auto r = SymbolSelectorAPI::select(contract(), richScope(), {"function", "type"}); + CHECK(r.candidates.size() == 2, "two category matches expected"); + PASS(); +} + +void test_candidates_sorted_by_scope_depth_then_name() { + TEST(candidates_sorted_by_scope_depth_then_name); + auto r = SymbolSelectorAPI::select(contract(), richScope()); + CHECK(r.candidates.size() >= 2, "at least two candidates required"); + CHECK(r.candidates[0].scopeDepth >= r.candidates[1].scopeDepth, + "depth sort expected"); + PASS(); +} + +void test_duplicate_symbol_names_are_deduped() { + TEST(duplicate_symbol_names_are_deduped); + auto s = richScope(); + s.candidates.push_back({"value", "Variable", 1}); + auto r = SymbolSelectorAPI::select(contract(), s); + int count = 0; + for (const auto& c : r.candidates) if (c.name == "value") ++count; + CHECK(count == 1, "duplicate symbol names should dedupe"); + PASS(); +} + +int main() { + std::cout << "Step 535: Symbol Selector API\n"; + + test_supported_scope_returns_candidates(); // 1 + test_unsupported_scope_returns_not_supported(); // 2 + test_forbidden_symbols_are_not_returned(); // 3 + test_contract_allowed_symbols_only(); // 4 + test_function_category_filter(); // 5 + test_type_category_filter(); // 6 + test_field_category_filter(); // 7 + test_module_category_filter(); // 8 + test_constant_category_filter(); // 9 + test_multiple_category_filter(); // 10 + test_candidates_sorted_by_scope_depth_then_name(); // 11 + test_duplicate_symbol_names_are_deduped(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 778801f..5f05114 100644 --- a/progress.md +++ b/progress.md @@ -9002,3 +9002,38 @@ ranked by predicted constraint-success signals. - `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` + +### Step 535: Symbol Selector API +**Status:** PASS (12/12 tests) + +Implements constrained symbol selection by exposing only in-scope symbols +allowed by contract policy, enriched with typed role/category metadata and +category-based filtering. + +**Files added:** +- `editor/src/SymbolSelectorAPI.h` - symbol selection module: + - filters scope snapshot symbols through contract allow/forbid policy + - maps symbol kinds to selector categories (`function/type/field/module/constant`) + - supports category filter queries + - deterministic sorting and dedupe behavior +- `editor/tests/step535_test.cpp` - 12 tests covering: + - supported/unsupported scope handling + - forbidden/contract-disallowed symbol filtering + - each category filter behavior + - multi-category filtering + - scope-depth sort behavior + - duplicate symbol-name dedup behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step535_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step535_test step534_test` - PASS +- `./editor/build-native/step535_test` - PASS (12/12) +- `./editor/build-native/step534_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `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`