arcscope 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,134 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { posix, join } from 'node:path';
3
+ const EXTS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx'];
4
+ // Resolves a module specifier (as written in an import) to the repo-relative file
5
+ // it points at, or null for external/unresolvable specifiers. Handles relative
6
+ // paths and tsconfig `paths` aliases — the one sanctioned config read (spec §4
7
+ // lists tsconfig paths as a secondary signal; no nx.json/project.json/BUILD).
8
+ // File existence is delegated so resolution is tied to arcscope's index, not raw FS.
9
+ export class ModuleResolver {
10
+ root;
11
+ fileExists;
12
+ aliases = null;
13
+ constructor(root, fileExists) {
14
+ this.root = root;
15
+ this.fileExists = fileExists;
16
+ }
17
+ resolve(importerRel, specifier) {
18
+ if (specifier.startsWith('.')) {
19
+ const base = posix.dirname(importerRel);
20
+ return this.tryCandidate(posix.normalize(posix.join(base, specifier)));
21
+ }
22
+ for (const alias of this.loadAliases()) {
23
+ const substituted = this.matchAlias(alias, specifier);
24
+ if (substituted === null)
25
+ continue;
26
+ for (const target of substituted) {
27
+ const hit = this.tryCandidate(posix.normalize(target));
28
+ if (hit)
29
+ return hit;
30
+ }
31
+ }
32
+ return null; // bare specifier (@angular/core, rxjs, …) — external
33
+ }
34
+ matchAlias(alias, specifier) {
35
+ if (!alias.wildcard) {
36
+ return specifier === alias.prefix ? alias.targets : null;
37
+ }
38
+ if (!specifier.startsWith(alias.prefix))
39
+ return null;
40
+ const rest = specifier.slice(alias.prefix.length);
41
+ return alias.targets.map((t) => t.replace('*', rest));
42
+ }
43
+ tryCandidate(p) {
44
+ if (this.fileExists(p))
45
+ return p; // tsconfig targets often already carry .ts
46
+ // NodeNext writes a './x.js' specifier for a './x.ts' source — strip the JS
47
+ // extension so the candidate loop can find the real TS/JS file.
48
+ const base = p.replace(/\.[cm]?jsx?$/, '');
49
+ for (const ext of EXTS)
50
+ if (this.fileExists(base + ext))
51
+ return base + ext;
52
+ for (const ext of EXTS)
53
+ if (this.fileExists(`${base}/index${ext}`))
54
+ return `${base}/index${ext}`;
55
+ return null;
56
+ }
57
+ loadAliases() {
58
+ if (this.aliases)
59
+ return this.aliases;
60
+ this.aliases = [];
61
+ for (const name of ['tsconfig.base.json', 'tsconfig.json']) {
62
+ const file = join(this.root, name);
63
+ if (!existsSync(file))
64
+ continue;
65
+ let config;
66
+ try {
67
+ config = JSON.parse(stripJsonc(readFileSync(file, 'utf8')));
68
+ }
69
+ catch {
70
+ continue;
71
+ }
72
+ const paths = config.compilerOptions?.paths;
73
+ if (!paths)
74
+ continue;
75
+ const baseUrl = config.compilerOptions?.baseUrl ?? '.';
76
+ for (const [key, targets] of Object.entries(paths)) {
77
+ const wildcard = key.endsWith('*');
78
+ this.aliases.push({
79
+ prefix: wildcard ? key.slice(0, -1) : key,
80
+ wildcard,
81
+ targets: targets.map((t) => posix.normalize(posix.join(baseUrl, t))),
82
+ });
83
+ }
84
+ break; // first tsconfig with paths wins
85
+ }
86
+ return this.aliases;
87
+ }
88
+ }
89
+ // Tolerant JSON-with-comments parse prep: strip // and /* */ comments (respecting
90
+ // string literals) and trailing commas, so real-world tsconfigs parse.
91
+ function stripJsonc(s) {
92
+ let out = '';
93
+ let i = 0;
94
+ let inStr = false;
95
+ let strCh = '';
96
+ while (i < s.length) {
97
+ const c = s[i];
98
+ const n = s[i + 1];
99
+ if (inStr) {
100
+ out += c;
101
+ if (c === '\\') {
102
+ out += n ?? '';
103
+ i += 2;
104
+ continue;
105
+ }
106
+ if (c === strCh)
107
+ inStr = false;
108
+ i++;
109
+ continue;
110
+ }
111
+ if (c === '"' || c === "'") {
112
+ inStr = true;
113
+ strCh = c;
114
+ out += c;
115
+ i++;
116
+ continue;
117
+ }
118
+ if (c === '/' && n === '/') {
119
+ while (i < s.length && s[i] !== '\n')
120
+ i++;
121
+ continue;
122
+ }
123
+ if (c === '/' && n === '*') {
124
+ i += 2;
125
+ while (i < s.length && !(s[i] === '*' && s[i + 1] === '/'))
126
+ i++;
127
+ i += 2;
128
+ continue;
129
+ }
130
+ out += c;
131
+ i++;
132
+ }
133
+ return out.replace(/,(\s*[}\]])/g, '$1');
134
+ }
@@ -0,0 +1,88 @@
1
+ import { Query } from 'web-tree-sitter';
2
+ import { extname } from 'node:path';
3
+ // Compiled once per grammar — every value/type identifier node. `type_identifier`
4
+ // is TS-only; the JavaScript grammar doesn't have that node, so its query must omit
5
+ // it (else Query compilation throws "Bad node name 'type_identifier'").
6
+ const refsQueryCache = new Map();
7
+ const REFS_QUERY_SRC = {
8
+ typescript: '[(identifier) (type_identifier)] @id',
9
+ tsx: '[(identifier) (type_identifier)] @id',
10
+ javascript: '(identifier) @id',
11
+ };
12
+ // Parents where the matching identifier is the *name being declared*, not a
13
+ // reference — excluded so a definition doesn't report itself.
14
+ const DECL_NAME_PARENTS = new Set([
15
+ 'class_declaration',
16
+ 'abstract_class_declaration',
17
+ 'function_declaration',
18
+ 'generator_function_declaration',
19
+ 'function_signature',
20
+ 'interface_declaration',
21
+ 'type_alias_declaration',
22
+ 'enum_declaration',
23
+ 'method_definition',
24
+ 'variable_declarator',
25
+ 'internal_module',
26
+ 'module',
27
+ ]);
28
+ // Re-parse one file and find every reference to `localName` (value or type
29
+ // position), classified by refKind. Used by find_refs only on the files the
30
+ // import graph says actually import the symbol — so this never runs repo-wide.
31
+ export async function findOccurrences(registry, rel, source, localName) {
32
+ const grammar = await registry.getForExt(extname(rel));
33
+ if (!grammar)
34
+ return [];
35
+ const parser = await registry.ensureInit();
36
+ parser.setLanguage(grammar.language);
37
+ const tree = parser.parse(source);
38
+ if (!tree)
39
+ return [];
40
+ try {
41
+ let query = refsQueryCache.get(grammar.id);
42
+ if (!query) {
43
+ query = new Query(grammar.language, REFS_QUERY_SRC[grammar.id] ?? '(identifier) @id');
44
+ refsQueryCache.set(grammar.id, query);
45
+ }
46
+ const lines = source.split('\n');
47
+ const out = [];
48
+ for (const cap of query.captures(tree.rootNode)) {
49
+ const node = cap.node;
50
+ if (node.text !== localName || isDeclarationName(node))
51
+ continue;
52
+ out.push({
53
+ file: rel,
54
+ line: node.startPosition.row + 1,
55
+ column: node.startPosition.column + 1,
56
+ snippet: (lines[node.startPosition.row] ?? '').trim().slice(0, 120),
57
+ refKind: classify(node),
58
+ });
59
+ }
60
+ return out;
61
+ }
62
+ finally {
63
+ tree.delete();
64
+ }
65
+ }
66
+ // web-tree-sitter returns fresh Node wrappers per access, so compare by position.
67
+ function isDeclarationName(node) {
68
+ const p = node.parent;
69
+ return !!p && DECL_NAME_PARENTS.has(p.type) && p.childForFieldName('name')?.startIndex === node.startIndex;
70
+ }
71
+ function classify(node) {
72
+ if (node.type === 'type_identifier')
73
+ return 'type';
74
+ const p = node.parent;
75
+ if (!p)
76
+ return 'identifier';
77
+ if (p.type === 'call_expression' && p.childForFieldName('function')?.startIndex === node.startIndex)
78
+ return 'call';
79
+ if (p.type === 'new_expression' && p.childForFieldName('constructor')?.startIndex === node.startIndex)
80
+ return 'new';
81
+ if (p.type === 'member_expression' && p.childForFieldName('object')?.startIndex === node.startIndex)
82
+ return 'access';
83
+ if (p.type === 'import_specifier' || p.type === 'namespace_import' || p.type === 'import_clause')
84
+ return 'import';
85
+ if (p.type === 'extends_clause' || p.type === 'class_heritage' || p.type === 'implements_clause')
86
+ return 'extends';
87
+ return 'identifier';
88
+ }
@@ -0,0 +1,2 @@
1
+ // Shared engine shapes.
2
+ export {};
@@ -0,0 +1,9 @@
1
+ ; arcscope-authored additions, appended to the upstream javascript tags.scm.
2
+ ; Captures exported non-function const/let bindings (upstream only tags
3
+ ; function-valued declarators). Scoped to export_statement so file-local consts
4
+ ; don't pollute the index. Verified to compile against the javascript grammar.
5
+
6
+ (export_statement
7
+ (lexical_declaration
8
+ (variable_declarator
9
+ name: (identifier) @name))) @definition.constant
@@ -0,0 +1,23 @@
1
+ ; arcscope-authored additions, appended to the upstream javascript + typescript
2
+ ; tags.scm. The stock tree-sitter tags omit these TS definition forms, which an
3
+ ; agent realistically navigates to; without them find_def loses to grep on TS.
4
+ ; Verified to compile against the typescript AND tsx grammars (web-tree-sitter 0.26.9),
5
+ ; and to capture the forms below on real code (see the engine extraction tests).
6
+ ; Capture convention matches upstream: @definition.<kind> on the node, @name on its identifier.
7
+
8
+ (type_alias_declaration
9
+ name: (type_identifier) @name) @definition.type
10
+
11
+ (enum_declaration
12
+ name: (identifier) @name) @definition.enum
13
+
14
+ ; `namespace X {}` parses as internal_module (upstream only tags `module "x" {}`).
15
+ (internal_module
16
+ name: (identifier) @name) @definition.module
17
+
18
+ ; Exported const/let bindings of any value (upstream only tags function-valued ones).
19
+ ; Scoped to export_statement so file-local consts don't pollute the index.
20
+ (export_statement
21
+ (lexical_declaration
22
+ (variable_declarator
23
+ name: (identifier) @name))) @definition.constant
@@ -0,0 +1,99 @@
1
+ (
2
+ (comment)* @doc
3
+ .
4
+ (method_definition
5
+ name: (property_identifier) @name) @definition.method
6
+ (#not-eq? @name "constructor")
7
+ (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$")
8
+ (#select-adjacent! @doc @definition.method)
9
+ )
10
+
11
+ (
12
+ (comment)* @doc
13
+ .
14
+ [
15
+ (class
16
+ name: (_) @name)
17
+ (class_declaration
18
+ name: (_) @name)
19
+ ] @definition.class
20
+ (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$")
21
+ (#select-adjacent! @doc @definition.class)
22
+ )
23
+
24
+ (
25
+ (comment)* @doc
26
+ .
27
+ [
28
+ (function_expression
29
+ name: (identifier) @name)
30
+ (function_declaration
31
+ name: (identifier) @name)
32
+ (generator_function
33
+ name: (identifier) @name)
34
+ (generator_function_declaration
35
+ name: (identifier) @name)
36
+ ] @definition.function
37
+ (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$")
38
+ (#select-adjacent! @doc @definition.function)
39
+ )
40
+
41
+ (
42
+ (comment)* @doc
43
+ .
44
+ (lexical_declaration
45
+ (variable_declarator
46
+ name: (identifier) @name
47
+ value: [(arrow_function) (function_expression)]) @definition.function)
48
+ (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$")
49
+ (#select-adjacent! @doc @definition.function)
50
+ )
51
+
52
+ (
53
+ (comment)* @doc
54
+ .
55
+ (variable_declaration
56
+ (variable_declarator
57
+ name: (identifier) @name
58
+ value: [(arrow_function) (function_expression)]) @definition.function)
59
+ (#strip! @doc "^[\\s\\*/]+|^[\\s\\*/]$")
60
+ (#select-adjacent! @doc @definition.function)
61
+ )
62
+
63
+ (assignment_expression
64
+ left: [
65
+ (identifier) @name
66
+ (member_expression
67
+ property: (property_identifier) @name)
68
+ ]
69
+ right: [(arrow_function) (function_expression)]
70
+ ) @definition.function
71
+
72
+ (pair
73
+ key: (property_identifier) @name
74
+ value: [(arrow_function) (function_expression)]) @definition.function
75
+
76
+ (
77
+ (call_expression
78
+ function: (identifier) @name) @reference.call
79
+ (#not-match? @name "^(require)$")
80
+ )
81
+
82
+ (call_expression
83
+ function: (member_expression
84
+ property: (property_identifier) @name)
85
+ arguments: (_) @reference.call)
86
+
87
+ (new_expression
88
+ constructor: (_) @name) @reference.class
89
+
90
+ (export_statement value: (assignment_expression left: (identifier) @name right: ([
91
+ (number)
92
+ (string)
93
+ (identifier)
94
+ (undefined)
95
+ (null)
96
+ (new_expression)
97
+ (binary_expression)
98
+ (call_expression)
99
+ ]))) @definition.constant
@@ -0,0 +1,23 @@
1
+ (function_signature
2
+ name: (identifier) @name) @definition.function
3
+
4
+ (method_signature
5
+ name: (property_identifier) @name) @definition.method
6
+
7
+ (abstract_method_signature
8
+ name: (property_identifier) @name) @definition.method
9
+
10
+ (abstract_class_declaration
11
+ name: (type_identifier) @name) @definition.class
12
+
13
+ (module
14
+ name: (identifier) @name) @definition.module
15
+
16
+ (interface_declaration
17
+ name: (type_identifier) @name) @definition.interface
18
+
19
+ (type_annotation
20
+ (type_identifier) @name) @reference.type
21
+
22
+ (new_expression
23
+ constructor: (identifier) @name) @reference.class
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { logError } from './log.js';
3
+ // Single bin, branches on argv. Subcommands are imported lazily so `init` doesn't
4
+ // pay to load the MCP SDK and `serve` starts with a clean module graph.
5
+ const command = process.argv[2];
6
+ const root = process.cwd();
7
+ try {
8
+ switch (command) {
9
+ case 'init': {
10
+ const { init } = await import('./init/init.js');
11
+ await init(root);
12
+ break;
13
+ }
14
+ case 'serve': {
15
+ const { serve } = await import('./server/serve.js');
16
+ await serve(root);
17
+ break;
18
+ }
19
+ default: {
20
+ process.stderr.write('arcscope — fully-local, architecture-aware code-navigation MCP server\n\n' +
21
+ 'Usage:\n' +
22
+ ' arcscope init Index this repo, write .mcp.json, and update .gitignore\n' +
23
+ ' arcscope serve Run the MCP server over stdio (spawned by your MCP client)\n');
24
+ process.exit(command ? 1 : 0);
25
+ }
26
+ }
27
+ }
28
+ catch (err) {
29
+ logError(err instanceof Error ? (err.stack ?? err.message) : err);
30
+ process.exit(1);
31
+ }
@@ -0,0 +1,66 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { join, relative } from 'node:path';
4
+ import { GrammarRegistry } from '../engine/grammar-registry.js';
5
+ import { IndexStore } from '../engine/index-store.js';
6
+ // `arcscope init`: index the repo once (the acceptance measurement), register the
7
+ // server for the MCP client offline, and ensure local cache files are gitignored.
8
+ // init is a human-facing CLI, so writing to stdout here is fine — the stdout
9
+ // hygiene rule applies to `serve`'s JSON-RPC stream, not this command.
10
+ export async function init(root) {
11
+ const out = (s) => process.stdout.write(s + '\n');
12
+ // 1. Index pass — measure cold-index time + symbol count on the real tree.
13
+ const store = new IndexStore(root, new GrammarRegistry());
14
+ const stats = await store.sync();
15
+ out(`arcscope: indexed ${stats.fileCount} files, ${stats.symbolCount} symbols in ${stats.elapsedMs}ms`);
16
+ // 2. Registration — point the client at the locally-resolved bin so server
17
+ // spawn is offline and deterministic (never `npx arcscope serve`). Derive the
18
+ // bin path from this module (dist/init/init.js -> dist/index.js) rather than
19
+ // process.argv[1], so it's correct even when invoked via a symlink/wrapper.
20
+ const binPath = fileURLToPath(new URL('../index.js', import.meta.url));
21
+ const mcpPath = join(root, '.mcp.json');
22
+ writeMcpJson(mcpPath, binPath);
23
+ out(`arcscope: wrote ${relative(root, mcpPath) || '.mcp.json'} (node -> ${binPath} serve)`);
24
+ // 3. Keep regenerable local state out of git.
25
+ if (ensureGitignore(root))
26
+ out('arcscope: updated .gitignore (.arcscope/* ignored, vocab.yaml committed)');
27
+ out('arcscope: ready — reconnect your MCP client to load the server.');
28
+ }
29
+ export function writeMcpJson(mcpPath, binPath) {
30
+ let config = {};
31
+ if (existsSync(mcpPath)) {
32
+ try {
33
+ const parsed = JSON.parse(readFileSync(mcpPath, 'utf8'));
34
+ if (parsed && typeof parsed === 'object')
35
+ config = parsed;
36
+ }
37
+ catch {
38
+ // unreadable/invalid -> start fresh rather than fail init
39
+ }
40
+ }
41
+ if (!config.mcpServers || typeof config.mcpServers !== 'object')
42
+ config.mcpServers = {};
43
+ config.mcpServers['arcscope'] = { command: 'node', args: [binPath, 'serve'] };
44
+ writeFileSync(mcpPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
45
+ }
46
+ // Ignore the regenerable local cache (index/usage/anchors) but COMMIT the
47
+ // vocabulary — `.arcscope/vocab.yaml` is the repo's declared knowledge and must
48
+ // travel with it. Note: re-including a file requires ignoring `.arcscope/*` (not
49
+ // the whole `.arcscope/` directory), so an old wholesale ignore is upgraded.
50
+ export function ensureGitignore(root) {
51
+ const p = join(root, '.gitignore');
52
+ const lines = (existsSync(p) ? readFileSync(p, 'utf8') : '').split('\n');
53
+ if (lines.some((l) => l.trim() === '.arcscope/*'))
54
+ return false; // already in the right shape
55
+ const kept = lines
56
+ .filter((l) => {
57
+ const t = l.trim();
58
+ return t !== '.arcscope/' && t !== '.arcscope' && !/^#\s*arcscope local index/i.test(t);
59
+ })
60
+ .join('\n')
61
+ .replace(/\n{3,}/g, '\n\n')
62
+ .replace(/\s+$/, '');
63
+ const block = '# arcscope: ignore the local cache (index/usage/anchors); commit the vocabulary\n.arcscope/*\n!.arcscope/vocab.yaml\n';
64
+ writeFileSync(p, kept ? `${kept}\n\n${block}` : block, 'utf8');
65
+ return true;
66
+ }
@@ -0,0 +1,68 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ function sha1(s) {
5
+ return createHash('sha1').update(s).digest('hex').slice(0, 16);
6
+ }
7
+ // Symbol anchors hash the def's kind+signature (so a header change drifts but a
8
+ // pure line move does not); path anchors hash the file's content.
9
+ export function computeAnchors(root, resolved) {
10
+ const byKey = new Map();
11
+ const fileCache = new Map();
12
+ for (const r of resolved) {
13
+ let anchor;
14
+ if (r.via === 'symbol') {
15
+ anchor = { key: `${r.file}#${r.symbol}`, hash: sha1(`${r.kind} ${r.signature ?? ''}`) };
16
+ }
17
+ else {
18
+ let content = fileCache.get(r.file);
19
+ if (content === undefined) {
20
+ try {
21
+ content = readFileSync(join(root, r.file), 'utf8');
22
+ }
23
+ catch {
24
+ content = '';
25
+ }
26
+ fileCache.set(r.file, content);
27
+ }
28
+ anchor = { key: r.file, hash: sha1(content) };
29
+ }
30
+ byKey.set(anchor.key, anchor); // dedupe overlapping locators
31
+ }
32
+ return [...byKey.values()];
33
+ }
34
+ export function compareDrift(current, baseline) {
35
+ const cur = new Map(current.map((a) => [a.key, a.hash]));
36
+ const base = new Map(baseline.map((a) => [a.key, a.hash]));
37
+ const added = [...cur.keys()].filter((k) => !base.has(k));
38
+ const removed = [...base.keys()].filter((k) => !cur.has(k));
39
+ const changed = [...cur.keys()].filter((k) => base.has(k) && base.get(k) !== cur.get(k));
40
+ return { status: added.length || removed.length || changed.length ? 'drifted' : 'fresh', added, removed, changed };
41
+ }
42
+ function anchorsPath(root) {
43
+ return join(root, '.arcscope', 'anchors.json');
44
+ }
45
+ // Local, gitignored baseline (per the chosen drift model).
46
+ export function loadAnchorStore(root) {
47
+ const p = anchorsPath(root);
48
+ if (!existsSync(p))
49
+ return { concepts: {} };
50
+ try {
51
+ const parsed = JSON.parse(readFileSync(p, 'utf8'));
52
+ if (parsed && typeof parsed === 'object' && 'concepts' in parsed)
53
+ return parsed;
54
+ }
55
+ catch {
56
+ // corrupt -> start fresh
57
+ }
58
+ return { concepts: {} };
59
+ }
60
+ export function baselineFor(store, conceptId) {
61
+ return store.concepts[conceptId]?.anchors;
62
+ }
63
+ export function captureBaseline(root, conceptId, anchors, nowIso) {
64
+ const store = loadAnchorStore(root);
65
+ store.concepts[conceptId] = { anchors, capturedAt: nowIso };
66
+ mkdirSync(join(root, '.arcscope'), { recursive: true });
67
+ writeFileSync(anchorsPath(root), JSON.stringify(store, null, 2) + '\n', 'utf8');
68
+ }
@@ -0,0 +1,24 @@
1
+ const KIND_ALIASES = { const: 'constant' };
2
+ // Parse a symbol locator query: "<kind> <namePattern> [= <valueConstraint>]".
3
+ // Examples: "interface I*Repository", "class GraphReducer",
4
+ // "const *_REPOSITORY = InjectionToken", "method normalizeElement".
5
+ // Pure string parsing — no regex injection, no shell-out (invariant 4).
6
+ export function parseSymbolQuery(query) {
7
+ let head = query.trim();
8
+ let valueConstraint;
9
+ const eq = head.indexOf('=');
10
+ if (eq >= 0) {
11
+ valueConstraint = head.slice(eq + 1).trim() || undefined;
12
+ head = head.slice(0, eq).trim();
13
+ }
14
+ const sp = head.indexOf(' ');
15
+ if (sp < 0) {
16
+ throw new Error(`invalid symbol locator query: "${query}" (expected "<kind> <namePattern> [= <value>]")`);
17
+ }
18
+ const rawKind = head.slice(0, sp).trim();
19
+ const namePattern = head.slice(sp + 1).trim();
20
+ if (rawKind.length === 0 || namePattern.length === 0) {
21
+ throw new Error(`invalid symbol locator query: "${query}" (missing kind or name pattern)`);
22
+ }
23
+ return { kind: KIND_ALIASES[rawKind] ?? rawKind, namePattern, valueConstraint };
24
+ }
@@ -0,0 +1,45 @@
1
+ import { matchGlob } from '../engine/glob.js';
2
+ import { parseSymbolQuery } from './locator.js';
3
+ // Resolve a concept LIVE against the current index. Symbol locators filter the def
4
+ // index (reuse P0); path locators filter the file set. Staged concepts resolve
5
+ // each stage in order and tag results with the stage title. Everything routes
6
+ // through the engine — no shell-out.
7
+ export function resolveConcept(store, concept) {
8
+ if (concept.stages) {
9
+ const out = [];
10
+ for (const stage of concept.stages) {
11
+ for (const loc of resolveLocator(store, stage))
12
+ out.push({ ...loc, stage: stage.title });
13
+ }
14
+ return out;
15
+ }
16
+ return (concept.locators ?? []).flatMap((loc) => resolveLocator(store, loc));
17
+ }
18
+ function resolveLocator(store, loc) {
19
+ if (loc.kind === 'symbol') {
20
+ const { kind, namePattern, valueConstraint } = parseSymbolQuery(loc.query);
21
+ return store
22
+ .allDefs()
23
+ .filter((d) => d.kind === kind &&
24
+ matchGlob(d.symbol, namePattern) &&
25
+ (loc.in === undefined || matchGlob(d.file, loc.in)) &&
26
+ (valueConstraint === undefined || d.signature.includes(valueConstraint)))
27
+ .sort(byFileLine)
28
+ .map((d) => ({
29
+ file: d.file,
30
+ line: d.line,
31
+ kind: d.kind,
32
+ symbol: d.symbol,
33
+ signature: d.signature,
34
+ via: 'symbol',
35
+ precisionTier: 'tree-sitter',
36
+ }));
37
+ }
38
+ return [...store.relFileSet()]
39
+ .filter((f) => matchGlob(f, loc.glob) && (loc.in === undefined || matchGlob(f, loc.in)))
40
+ .sort()
41
+ .map((f) => ({ file: f, kind: 'file', via: 'path', precisionTier: 'tree-sitter' }));
42
+ }
43
+ function byFileLine(a, b) {
44
+ return a.file === b.file ? a.line - b.line : a.file < b.file ? -1 : 1;
45
+ }
@@ -0,0 +1,2 @@
1
+ // The architecture vocabulary: named concepts bound to engine-resolved locators.
2
+ export {};