Step 535: add constrained symbol selector API

This commit is contained in:
Bill
2026-02-17 09:33:06 -07:00
parent dc3177d5e1
commit 8503300da6
4 changed files with 288 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,81 @@
#pragma once
// Step 535: Symbol Selector API
#include <algorithm>
#include <set>
#include <string>
#include <vector>
#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<SymbolCandidate> candidates;
};
class SymbolSelectorAPI {
public:
static SymbolSelectionResult select(const TaskitemContract& contract,
const ScopeSnapshot& scope,
const std::vector<std::string>& 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<std::string> asSet(const std::vector<std::string>& values) {
return std::set<std::string>(values.begin(), values.end());
}
static void dedupeByName(std::vector<SymbolCandidate>& candidates) {
std::set<std::string> seen;
std::vector<SymbolCandidate> out;
for (const auto& candidate : candidates) {
if (seen.insert(candidate.name).second) {
out.push_back(candidate);
}
}
candidates.swap(out);
}
};

View File

@@ -0,0 +1,163 @@
// Step 535: Symbol Selector API (12 tests)
#include "SymbolSelectorAPI.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-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;
}

View File

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