Step 535: add constrained symbol selector API
This commit is contained in:
@@ -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)
|
||||
|
||||
81
editor/src/SymbolSelectorAPI.h
Normal file
81
editor/src/SymbolSelectorAPI.h
Normal 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);
|
||||
}
|
||||
};
|
||||
163
editor/tests/step535_test.cpp
Normal file
163
editor/tests/step535_test.cpp
Normal 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;
|
||||
}
|
||||
35
progress.md
35
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`
|
||||
|
||||
Reference in New Issue
Block a user