@cstart/coldstart 2.0.0
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.
- package/README.md +236 -0
- package/dist/cache/disk-cache.d.ts +14 -0
- package/dist/cache/disk-cache.d.ts.map +1 -0
- package/dist/cache/disk-cache.js +371 -0
- package/dist/cache/disk-cache.js.map +1 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +206 -0
- package/dist/cli.js.map +1 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +111 -0
- package/dist/constants.js.map +1 -0
- package/dist/daemon-lock.d.ts +63 -0
- package/dist/daemon-lock.d.ts.map +1 -0
- package/dist/daemon-lock.js +218 -0
- package/dist/daemon-lock.js.map +1 -0
- package/dist/daemon-log.d.ts +26 -0
- package/dist/daemon-log.d.ts.map +1 -0
- package/dist/daemon-log.js +110 -0
- package/dist/daemon-log.js.map +1 -0
- package/dist/index-manager.d.ts +103 -0
- package/dist/index-manager.d.ts.map +1 -0
- package/dist/index-manager.js +361 -0
- package/dist/index-manager.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +724 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer/content-tokens.d.ts +74 -0
- package/dist/indexer/content-tokens.d.ts.map +1 -0
- package/dist/indexer/content-tokens.js +406 -0
- package/dist/indexer/content-tokens.js.map +1 -0
- package/dist/indexer/cpp-include-roots.d.ts +7 -0
- package/dist/indexer/cpp-include-roots.d.ts.map +1 -0
- package/dist/indexer/cpp-include-roots.js +166 -0
- package/dist/indexer/cpp-include-roots.js.map +1 -0
- package/dist/indexer/csharp-synthetic.d.ts +9 -0
- package/dist/indexer/csharp-synthetic.d.ts.map +1 -0
- package/dist/indexer/csharp-synthetic.js +51 -0
- package/dist/indexer/csharp-synthetic.js.map +1 -0
- package/dist/indexer/django-synthetic.d.ts +12 -0
- package/dist/indexer/django-synthetic.d.ts.map +1 -0
- package/dist/indexer/django-synthetic.js +49 -0
- package/dist/indexer/django-synthetic.js.map +1 -0
- package/dist/indexer/extractors/angularjs.d.ts +16 -0
- package/dist/indexer/extractors/angularjs.d.ts.map +1 -0
- package/dist/indexer/extractors/angularjs.js +49 -0
- package/dist/indexer/extractors/angularjs.js.map +1 -0
- package/dist/indexer/extractors/cpp.d.ts +9 -0
- package/dist/indexer/extractors/cpp.d.ts.map +1 -0
- package/dist/indexer/extractors/cpp.js +254 -0
- package/dist/indexer/extractors/cpp.js.map +1 -0
- package/dist/indexer/extractors/csharp.d.ts +19 -0
- package/dist/indexer/extractors/csharp.d.ts.map +1 -0
- package/dist/indexer/extractors/csharp.js +327 -0
- package/dist/indexer/extractors/csharp.js.map +1 -0
- package/dist/indexer/extractors/env.d.ts +9 -0
- package/dist/indexer/extractors/env.d.ts.map +1 -0
- package/dist/indexer/extractors/env.js +43 -0
- package/dist/indexer/extractors/env.js.map +1 -0
- package/dist/indexer/extractors/go.d.ts +9 -0
- package/dist/indexer/extractors/go.d.ts.map +1 -0
- package/dist/indexer/extractors/go.js +227 -0
- package/dist/indexer/extractors/go.js.map +1 -0
- package/dist/indexer/extractors/graphql.d.ts +22 -0
- package/dist/indexer/extractors/graphql.d.ts.map +1 -0
- package/dist/indexer/extractors/graphql.js +88 -0
- package/dist/indexer/extractors/graphql.js.map +1 -0
- package/dist/indexer/extractors/groovy.d.ts +9 -0
- package/dist/indexer/extractors/groovy.d.ts.map +1 -0
- package/dist/indexer/extractors/groovy.js +356 -0
- package/dist/indexer/extractors/groovy.js.map +1 -0
- package/dist/indexer/extractors/java.d.ts +10 -0
- package/dist/indexer/extractors/java.d.ts.map +1 -0
- package/dist/indexer/extractors/java.js +586 -0
- package/dist/indexer/extractors/java.js.map +1 -0
- package/dist/indexer/extractors/kotlin.d.ts +10 -0
- package/dist/indexer/extractors/kotlin.d.ts.map +1 -0
- package/dist/indexer/extractors/kotlin.js +398 -0
- package/dist/indexer/extractors/kotlin.js.map +1 -0
- package/dist/indexer/extractors/node-helpers.d.ts +13 -0
- package/dist/indexer/extractors/node-helpers.d.ts.map +1 -0
- package/dist/indexer/extractors/node-helpers.js +17 -0
- package/dist/indexer/extractors/node-helpers.js.map +1 -0
- package/dist/indexer/extractors/parser-factory.d.ts +4 -0
- package/dist/indexer/extractors/parser-factory.d.ts.map +1 -0
- package/dist/indexer/extractors/parser-factory.js +28 -0
- package/dist/indexer/extractors/parser-factory.js.map +1 -0
- package/dist/indexer/extractors/php.d.ts +17 -0
- package/dist/indexer/extractors/php.d.ts.map +1 -0
- package/dist/indexer/extractors/php.js +363 -0
- package/dist/indexer/extractors/php.js.map +1 -0
- package/dist/indexer/extractors/python.d.ts +14 -0
- package/dist/indexer/extractors/python.d.ts.map +1 -0
- package/dist/indexer/extractors/python.js +449 -0
- package/dist/indexer/extractors/python.js.map +1 -0
- package/dist/indexer/extractors/ruby.d.ts +10 -0
- package/dist/indexer/extractors/ruby.d.ts.map +1 -0
- package/dist/indexer/extractors/ruby.js +996 -0
- package/dist/indexer/extractors/ruby.js.map +1 -0
- package/dist/indexer/extractors/rust.d.ts +9 -0
- package/dist/indexer/extractors/rust.d.ts.map +1 -0
- package/dist/indexer/extractors/rust.js +322 -0
- package/dist/indexer/extractors/rust.js.map +1 -0
- package/dist/indexer/extractors/toml.d.ts +9 -0
- package/dist/indexer/extractors/toml.d.ts.map +1 -0
- package/dist/indexer/extractors/toml.js +84 -0
- package/dist/indexer/extractors/toml.js.map +1 -0
- package/dist/indexer/extractors/xml.d.ts +9 -0
- package/dist/indexer/extractors/xml.d.ts.map +1 -0
- package/dist/indexer/extractors/xml.js +163 -0
- package/dist/indexer/extractors/xml.js.map +1 -0
- package/dist/indexer/extractors/yaml.d.ts +9 -0
- package/dist/indexer/extractors/yaml.d.ts.map +1 -0
- package/dist/indexer/extractors/yaml.js +117 -0
- package/dist/indexer/extractors/yaml.js.map +1 -0
- package/dist/indexer/git.d.ts +19 -0
- package/dist/indexer/git.d.ts.map +1 -0
- package/dist/indexer/git.js +55 -0
- package/dist/indexer/git.js.map +1 -0
- package/dist/indexer/graph.d.ts +7 -0
- package/dist/indexer/graph.d.ts.map +1 -0
- package/dist/indexer/graph.js +20 -0
- package/dist/indexer/graph.js.map +1 -0
- package/dist/indexer/indexed-file.d.ts +15 -0
- package/dist/indexer/indexed-file.d.ts.map +1 -0
- package/dist/indexer/indexed-file.js +50 -0
- package/dist/indexer/indexed-file.js.map +1 -0
- package/dist/indexer/invariants.d.ts +15 -0
- package/dist/indexer/invariants.d.ts.map +1 -0
- package/dist/indexer/invariants.js +96 -0
- package/dist/indexer/invariants.js.map +1 -0
- package/dist/indexer/laravel-synthetic.d.ts +10 -0
- package/dist/indexer/laravel-synthetic.d.ts.map +1 -0
- package/dist/indexer/laravel-synthetic.js +78 -0
- package/dist/indexer/laravel-synthetic.js.map +1 -0
- package/dist/indexer/parser.d.ts +4 -0
- package/dist/indexer/parser.d.ts.map +1 -0
- package/dist/indexer/parser.js +499 -0
- package/dist/indexer/parser.js.map +1 -0
- package/dist/indexer/patch.d.ts +3 -0
- package/dist/indexer/patch.d.ts.map +1 -0
- package/dist/indexer/patch.js +291 -0
- package/dist/indexer/patch.js.map +1 -0
- package/dist/indexer/rails-synthetic.d.ts +11 -0
- package/dist/indexer/rails-synthetic.d.ts.map +1 -0
- package/dist/indexer/rails-synthetic.js +80 -0
- package/dist/indexer/rails-synthetic.js.map +1 -0
- package/dist/indexer/reconcile.d.ts +9 -0
- package/dist/indexer/reconcile.d.ts.map +1 -0
- package/dist/indexer/reconcile.js +96 -0
- package/dist/indexer/reconcile.js.map +1 -0
- package/dist/indexer/resolvers/cpp.d.ts +15 -0
- package/dist/indexer/resolvers/cpp.d.ts.map +1 -0
- package/dist/indexer/resolvers/cpp.js +35 -0
- package/dist/indexer/resolvers/cpp.js.map +1 -0
- package/dist/indexer/resolvers/csharp.d.ts +2 -0
- package/dist/indexer/resolvers/csharp.d.ts.map +1 -0
- package/dist/indexer/resolvers/csharp.js +89 -0
- package/dist/indexer/resolvers/csharp.js.map +1 -0
- package/dist/indexer/resolvers/generic.d.ts +10 -0
- package/dist/indexer/resolvers/generic.d.ts.map +1 -0
- package/dist/indexer/resolvers/generic.js +35 -0
- package/dist/indexer/resolvers/generic.js.map +1 -0
- package/dist/indexer/resolvers/go.d.ts +2 -0
- package/dist/indexer/resolvers/go.d.ts.map +1 -0
- package/dist/indexer/resolvers/go.js +171 -0
- package/dist/indexer/resolvers/go.js.map +1 -0
- package/dist/indexer/resolvers/index.d.ts +26 -0
- package/dist/indexer/resolvers/index.d.ts.map +1 -0
- package/dist/indexer/resolvers/index.js +301 -0
- package/dist/indexer/resolvers/index.js.map +1 -0
- package/dist/indexer/resolvers/java.d.ts +17 -0
- package/dist/indexer/resolvers/java.d.ts.map +1 -0
- package/dist/indexer/resolvers/java.js +104 -0
- package/dist/indexer/resolvers/java.js.map +1 -0
- package/dist/indexer/resolvers/php.d.ts +2 -0
- package/dist/indexer/resolvers/php.d.ts.map +1 -0
- package/dist/indexer/resolvers/php.js +108 -0
- package/dist/indexer/resolvers/php.js.map +1 -0
- package/dist/indexer/resolvers/python.d.ts +17 -0
- package/dist/indexer/resolvers/python.d.ts.map +1 -0
- package/dist/indexer/resolvers/python.js +63 -0
- package/dist/indexer/resolvers/python.js.map +1 -0
- package/dist/indexer/resolvers/ruby.d.ts +13 -0
- package/dist/indexer/resolvers/ruby.d.ts.map +1 -0
- package/dist/indexer/resolvers/ruby.js +199 -0
- package/dist/indexer/resolvers/ruby.js.map +1 -0
- package/dist/indexer/resolvers/rust.d.ts +12 -0
- package/dist/indexer/resolvers/rust.d.ts.map +1 -0
- package/dist/indexer/resolvers/rust.js +61 -0
- package/dist/indexer/resolvers/rust.js.map +1 -0
- package/dist/indexer/resolvers/shared.d.ts +18 -0
- package/dist/indexer/resolvers/shared.d.ts.map +1 -0
- package/dist/indexer/resolvers/shared.js +71 -0
- package/dist/indexer/resolvers/shared.js.map +1 -0
- package/dist/indexer/rust-workspace.d.ts +25 -0
- package/dist/indexer/rust-workspace.d.ts.map +1 -0
- package/dist/indexer/rust-workspace.js +155 -0
- package/dist/indexer/rust-workspace.js.map +1 -0
- package/dist/indexer/symbol-edges.d.ts +19 -0
- package/dist/indexer/symbol-edges.d.ts.map +1 -0
- package/dist/indexer/symbol-edges.js +117 -0
- package/dist/indexer/symbol-edges.js.map +1 -0
- package/dist/indexer/tokenize.d.ts +26 -0
- package/dist/indexer/tokenize.d.ts.map +1 -0
- package/dist/indexer/tokenize.js +200 -0
- package/dist/indexer/tokenize.js.map +1 -0
- package/dist/indexer/ts-parser.d.ts +10 -0
- package/dist/indexer/ts-parser.d.ts.map +1 -0
- package/dist/indexer/ts-parser.js +605 -0
- package/dist/indexer/ts-parser.js.map +1 -0
- package/dist/indexer/walker.d.ts +9 -0
- package/dist/indexer/walker.d.ts.map +1 -0
- package/dist/indexer/walker.js +91 -0
- package/dist/indexer/walker.js.map +1 -0
- package/dist/init.d.ts +111 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +787 -0
- package/dist/init.js.map +1 -0
- package/dist/kb/cli.d.ts +2 -0
- package/dist/kb/cli.d.ts.map +1 -0
- package/dist/kb/cli.js +377 -0
- package/dist/kb/cli.js.map +1 -0
- package/dist/kb/commit.d.ts +6 -0
- package/dist/kb/commit.d.ts.map +1 -0
- package/dist/kb/commit.js +84 -0
- package/dist/kb/commit.js.map +1 -0
- package/dist/kb/fold.d.ts +42 -0
- package/dist/kb/fold.d.ts.map +1 -0
- package/dist/kb/fold.js +285 -0
- package/dist/kb/fold.js.map +1 -0
- package/dist/kb/freshness.d.ts +9 -0
- package/dist/kb/freshness.d.ts.map +1 -0
- package/dist/kb/freshness.js +52 -0
- package/dist/kb/freshness.js.map +1 -0
- package/dist/kb/git.d.ts +4 -0
- package/dist/kb/git.d.ts.map +1 -0
- package/dist/kb/git.js +28 -0
- package/dist/kb/git.js.map +1 -0
- package/dist/kb/ids.d.ts +8 -0
- package/dist/kb/ids.d.ts.map +1 -0
- package/dist/kb/ids.js +42 -0
- package/dist/kb/ids.js.map +1 -0
- package/dist/kb/lint.d.ts +23 -0
- package/dist/kb/lint.d.ts.map +1 -0
- package/dist/kb/lint.js +102 -0
- package/dist/kb/lint.js.map +1 -0
- package/dist/kb/lookup.d.ts +55 -0
- package/dist/kb/lookup.d.ts.map +1 -0
- package/dist/kb/lookup.js +92 -0
- package/dist/kb/lookup.js.map +1 -0
- package/dist/kb/notes-index.d.ts +31 -0
- package/dist/kb/notes-index.d.ts.map +1 -0
- package/dist/kb/notes-index.js +0 -0
- package/dist/kb/notes-index.js.map +1 -0
- package/dist/kb/raw-log.d.ts +34 -0
- package/dist/kb/raw-log.d.ts.map +1 -0
- package/dist/kb/raw-log.js +106 -0
- package/dist/kb/raw-log.js.map +1 -0
- package/dist/kb/render.d.ts +17 -0
- package/dist/kb/render.d.ts.map +1 -0
- package/dist/kb/render.js +101 -0
- package/dist/kb/render.js.map +1 -0
- package/dist/kb/search.d.ts +110 -0
- package/dist/kb/search.d.ts.map +1 -0
- package/dist/kb/search.js +453 -0
- package/dist/kb/search.js.map +1 -0
- package/dist/kb/store.d.ts +35 -0
- package/dist/kb/store.d.ts.map +1 -0
- package/dist/kb/store.js +160 -0
- package/dist/kb/store.js.map +1 -0
- package/dist/kb/types.d.ts +142 -0
- package/dist/kb/types.d.ts.map +1 -0
- package/dist/kb/types.js +13 -0
- package/dist/kb/types.js.map +1 -0
- package/dist/kb/view-template.d.ts +8 -0
- package/dist/kb/view-template.d.ts.map +1 -0
- package/dist/kb/view-template.js +8 -0
- package/dist/kb/view-template.js.map +1 -0
- package/dist/kb/view.d.ts +42 -0
- package/dist/kb/view.d.ts.map +1 -0
- package/dist/kb/view.js +118 -0
- package/dist/kb/view.js.map +1 -0
- package/dist/kb/write.d.ts +72 -0
- package/dist/kb/write.d.ts.map +1 -0
- package/dist/kb/write.js +345 -0
- package/dist/kb/write.js.map +1 -0
- package/dist/keeper-state.d.ts +29 -0
- package/dist/keeper-state.d.ts.map +1 -0
- package/dist/keeper-state.js +89 -0
- package/dist/keeper-state.js.map +1 -0
- package/dist/keeper.d.ts +23 -0
- package/dist/keeper.d.ts.map +1 -0
- package/dist/keeper.js +105 -0
- package/dist/keeper.js.map +1 -0
- package/dist/migrate.d.ts +9 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +102 -0
- package/dist/migrate.js.map +1 -0
- package/dist/restart.d.ts +15 -0
- package/dist/restart.d.ts.map +1 -0
- package/dist/restart.js +48 -0
- package/dist/restart.js.map +1 -0
- package/dist/server/find.d.ts +39 -0
- package/dist/server/find.d.ts.map +1 -0
- package/dist/server/find.js +979 -0
- package/dist/server/find.js.map +1 -0
- package/dist/server/mcp.d.ts +117 -0
- package/dist/server/mcp.d.ts.map +1 -0
- package/dist/server/mcp.js +301 -0
- package/dist/server/mcp.js.map +1 -0
- package/dist/server/searcher.d.ts +14 -0
- package/dist/server/searcher.d.ts.map +1 -0
- package/dist/server/searcher.js +146 -0
- package/dist/server/searcher.js.map +1 -0
- package/dist/server/tools.d.ts +17 -0
- package/dist/server/tools.d.ts.map +1 -0
- package/dist/server/tools.js +875 -0
- package/dist/server/tools.js.map +1 -0
- package/dist/status.d.ts +12 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +157 -0
- package/dist/status.js.map +1 -0
- package/dist/types.d.ts +138 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/watcher.d.ts +11 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +67 -0
- package/dist/watcher.js.map +1 -0
- package/hooks/canonical-find-key.mjs +68 -0
- package/hooks/codex-find-nudge.mjs +15 -0
- package/hooks/codex-find-preguard.mjs +14 -0
- package/hooks/codex-kb-elicit.mjs +329 -0
- package/hooks/codex-kb-recall.mjs +151 -0
- package/hooks/codex-nudge-handler.mjs +370 -0
- package/hooks/codex-preguard-handler.mjs +75 -0
- package/hooks/codex-run-hook.mjs +132 -0
- package/hooks/coldstart-call.mjs +58 -0
- package/hooks/cursor-find-nudge.mjs +15 -0
- package/hooks/cursor-find-preguard.mjs +14 -0
- package/hooks/cursor-input.mjs +72 -0
- package/hooks/cursor-kb-elicit.mjs +335 -0
- package/hooks/cursor-kb-recall.mjs +138 -0
- package/hooks/cursor-nudge-handler.mjs +33 -0
- package/hooks/cursor-preguard-handler.mjs +39 -0
- package/hooks/cursor-run-hook.mjs +129 -0
- package/hooks/find-nudge.mjs +15 -0
- package/hooks/find-preguard.mjs +14 -0
- package/hooks/kb-elicit.mjs +334 -0
- package/hooks/kb-recall.mjs +151 -0
- package/hooks/nudge-handler.mjs +370 -0
- package/hooks/preguard-handler.mjs +75 -0
- package/hooks/run-hook.mjs +132 -0
- package/package.json +69 -0
- package/templates/coldstart.cli.md +60 -0
- package/templates/coldstart.mcp.md +59 -0
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { extractContentTokens, deriveRelatedFiles, deriveNameEchoFiles, RELATED_FILES_CAP, } from '../indexer/content-tokens.js';
|
|
3
|
+
import { buildRichPage } from './find.js';
|
|
4
|
+
// For a target file, find callers of its exported symbols via symbolEdges.
|
|
5
|
+
// Returns up to CALLERS_CAP unique caller files, each with up to a few example
|
|
6
|
+
// caller-symbol/line pairs.
|
|
7
|
+
const CALLERS_CAP = 12;
|
|
8
|
+
const CALL_LINES_PER_CALLER_FILE = 3;
|
|
9
|
+
function buildCallersForFile(fileId, file, index) {
|
|
10
|
+
// Build a map: exported symbolId → list of incoming calls (caller symbolId + line)
|
|
11
|
+
const exportedIds = new Set(file.symbols.filter(s => s.isExported).map(s => s.id));
|
|
12
|
+
if (exportedIds.size === 0)
|
|
13
|
+
return [];
|
|
14
|
+
// exportedSymbolId → caller fileId → [caller info]
|
|
15
|
+
const callsTo = new Map();
|
|
16
|
+
// symInfo for resolving symbolIds → fileId+name. Build lazily on first hit.
|
|
17
|
+
let symInfo = null;
|
|
18
|
+
function info() {
|
|
19
|
+
if (!symInfo)
|
|
20
|
+
symInfo = buildSymbolInfoMap(index);
|
|
21
|
+
return symInfo;
|
|
22
|
+
}
|
|
23
|
+
for (const edge of index.symbolEdges) {
|
|
24
|
+
if (edge.type !== 'calls')
|
|
25
|
+
continue;
|
|
26
|
+
if (!exportedIds.has(edge.to))
|
|
27
|
+
continue;
|
|
28
|
+
const fromInfo = info().get(edge.from);
|
|
29
|
+
// Skip self-references inside the target file
|
|
30
|
+
if (fromInfo?.file === file.relativePath)
|
|
31
|
+
continue;
|
|
32
|
+
const callerFile = fromInfo?.file;
|
|
33
|
+
const callerName = fromInfo?.name ?? edge.from;
|
|
34
|
+
if (!callerFile)
|
|
35
|
+
continue;
|
|
36
|
+
if (!callsTo.has(edge.to))
|
|
37
|
+
callsTo.set(edge.to, new Map());
|
|
38
|
+
const perFile = callsTo.get(edge.to);
|
|
39
|
+
if (!perFile.has(callerFile))
|
|
40
|
+
perFile.set(callerFile, []);
|
|
41
|
+
perFile.get(callerFile).push({ symbolName: callerName, line: edge.line });
|
|
42
|
+
}
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const sym of file.symbols) {
|
|
45
|
+
if (!sym.isExported)
|
|
46
|
+
continue;
|
|
47
|
+
const perFile = callsTo.get(sym.id);
|
|
48
|
+
if (!perFile || perFile.size === 0)
|
|
49
|
+
continue;
|
|
50
|
+
const callers = [];
|
|
51
|
+
let truncated = 0;
|
|
52
|
+
for (const [callerFile, callsList] of perFile) {
|
|
53
|
+
if (callers.length >= CALLERS_CAP) {
|
|
54
|
+
truncated += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const samples = callsList.slice(0, CALL_LINES_PER_CALLER_FILE).map(c => {
|
|
58
|
+
if (typeof c.line === 'number' && c.line > 0) {
|
|
59
|
+
return `${callerFile}:${c.line} (${c.symbolName})`;
|
|
60
|
+
}
|
|
61
|
+
return `${callerFile} (${c.symbolName})`;
|
|
62
|
+
});
|
|
63
|
+
const extra = callsList.length - samples.length;
|
|
64
|
+
callers.push(samples.join('; ') + (extra > 0 ? ` +${extra} more` : ''));
|
|
65
|
+
}
|
|
66
|
+
if (truncated > 0)
|
|
67
|
+
callers.push(`[+${truncated} more caller files]`);
|
|
68
|
+
out.push({ exportedSymbol: sym.name, callers });
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// find — shares ONE engine across transports: buildRichPage. The MCP `find`
|
|
74
|
+
// tool and the CLI `find`/`gs index` reader both call it. Same query in, same
|
|
75
|
+
// page out, regardless of transport. Everything heavy (grep-recall + AST
|
|
76
|
+
// precision against the live root) lives in find.ts; this is a thin shim.
|
|
77
|
+
// ============================================================================
|
|
78
|
+
export async function handleFind(index, params) {
|
|
79
|
+
const query = (params.query ?? params.domain_filter ?? "").trim();
|
|
80
|
+
if (!query) {
|
|
81
|
+
return {
|
|
82
|
+
error: "query is required",
|
|
83
|
+
__rawText: "Error: missing `query`. Pass the salient identifiers from the task, " +
|
|
84
|
+
"e.g. \"ResourceInstance principaluser editable\".",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return { __rawText: await buildRichPage(index, index.rootDir, query) };
|
|
88
|
+
}
|
|
89
|
+
const IMPORTERS_LIST_CAP = 20;
|
|
90
|
+
const BODY_REF_IMPORTERS_CAP = 8;
|
|
91
|
+
// For huge files, show top-K symbols sorted by caller count first; the rest
|
|
92
|
+
// collapse into a tail. Avoids the 60-class wall-of-text problem.
|
|
93
|
+
const SYMBOLS_TOP_K = 15;
|
|
94
|
+
const SYMBOLS_HUGE_FILE_THRESHOLD = 20;
|
|
95
|
+
// Inline single caller when symbol has only 1; switch to newline-per-caller
|
|
96
|
+
// once a symbol has 2+ callers (otherwise the line balloons past 500 chars).
|
|
97
|
+
const CALLER_INLINE_MAX = 1;
|
|
98
|
+
function isTestPath(p) {
|
|
99
|
+
return p.startsWith('tests/') || p.startsWith('test/')
|
|
100
|
+
|| p.includes('/tests/') || p.includes('/test/')
|
|
101
|
+
|| /_tests?\.[a-z]+$/.test(p)
|
|
102
|
+
|| /\.tests?\.[a-z]+$/.test(p);
|
|
103
|
+
}
|
|
104
|
+
export function handleGetStructure(index, params) {
|
|
105
|
+
if (!params.file_path) {
|
|
106
|
+
return { error: 'file_path is required' };
|
|
107
|
+
}
|
|
108
|
+
const fileEntry = findFileByPath(index, params.file_path);
|
|
109
|
+
if (!fileEntry) {
|
|
110
|
+
return { error: `File not found: ${params.file_path}` };
|
|
111
|
+
}
|
|
112
|
+
const [fileId, file] = fileEntry;
|
|
113
|
+
// Grade-2 symbol-body delivery: `--symbol name1,name2` slices the named
|
|
114
|
+
// method bodies straight from the indexed [startLine,endLine] range so the
|
|
115
|
+
// agent gets them in ONE call instead of windowing a god-file at guessed
|
|
116
|
+
// offsets (q22 read graph.py 8× hunting serialize/restore_state). Each slice
|
|
117
|
+
// is followed by 1-line caller/callee POINTERS (not bodies) so the next hop
|
|
118
|
+
// is one directed call away. This is the lever; grade-1 (the matched-symbol
|
|
119
|
+
// line ranges in `find`) only pointed the agent at the offsets.
|
|
120
|
+
if (params.symbol && params.symbol.trim()) {
|
|
121
|
+
return renderSymbolSlice(index, fileId, file, params.symbol);
|
|
122
|
+
}
|
|
123
|
+
// Validate `view`: an invalid value (stale client, typo, or a hallucinated
|
|
124
|
+
// name like "outline") would otherwise silently disable EVERY section and
|
|
125
|
+
// return a header-only response, pushing the agent to grep. Fail open —
|
|
126
|
+
// coerce to 'full' and surface a loud warning so the agent self-corrects.
|
|
127
|
+
const VALID_VIEWS = ['full', 'symbols', 'imports', 'importers', 'callers'];
|
|
128
|
+
const requestedView = params.view;
|
|
129
|
+
const viewIsInvalid = requestedView !== undefined && !VALID_VIEWS.includes(requestedView);
|
|
130
|
+
const view = viewIsInvalid ? 'full' : (requestedView ?? 'full');
|
|
131
|
+
const matchPredicate = compileMatchPredicate(params.match);
|
|
132
|
+
const wantSymbols = view === 'full' || view === 'symbols';
|
|
133
|
+
const wantImports = view === 'full' || view === 'imports';
|
|
134
|
+
const wantImporters = view === 'full' || view === 'importers';
|
|
135
|
+
const wantInlineCallers = view === 'full';
|
|
136
|
+
const wantExpandedCallers = view === 'callers';
|
|
137
|
+
// Pre-compute callers once if any caller view is on
|
|
138
|
+
let callersByName = null;
|
|
139
|
+
if (wantInlineCallers || wantExpandedCallers) {
|
|
140
|
+
const perSym = buildCallersForFile(fileId, file, index);
|
|
141
|
+
callersByName = new Map();
|
|
142
|
+
for (const entry of perSym) {
|
|
143
|
+
callersByName.set(entry.exportedSymbol, entry.callers);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
let symbolsOut = [];
|
|
147
|
+
let totalSymbolCount = 0;
|
|
148
|
+
let filteredSymbolCount = 0;
|
|
149
|
+
let truncatedSymbolCount = 0;
|
|
150
|
+
let matchMissedSymbols = false;
|
|
151
|
+
if (wantSymbols) {
|
|
152
|
+
const baseFiltered = file.symbols
|
|
153
|
+
.filter(s => s.isExported || s.kind === 'class' || s.kind === 'function' || s.kind === 'method')
|
|
154
|
+
.slice()
|
|
155
|
+
.sort((a, b) => a.startLine - b.startLine);
|
|
156
|
+
totalSymbolCount = baseFiltered.length;
|
|
157
|
+
let filtered = matchPredicate
|
|
158
|
+
? baseFiltered.filter(s => matchPredicate(s.name))
|
|
159
|
+
: baseFiltered;
|
|
160
|
+
// Match-miss auto-fallback: a "0 symbols match" result's only possible
|
|
161
|
+
// follow-up is re-calling without the filter (a measured wasted call).
|
|
162
|
+
// Return the full view instead, flagged via matchMissedSymbols.
|
|
163
|
+
if (matchPredicate && filtered.length === 0 && baseFiltered.length > 0) {
|
|
164
|
+
// The fix: before falling back to the full symbol MENU (which the agent
|
|
165
|
+
// ignores, then re-reads the whole file — jmri Q1), try mapping the match
|
|
166
|
+
// terms to their enclosing method bodies. Same machinery the `--symbol`
|
|
167
|
+
// miss uses. Returns null (→ menu) when the toggle is off or nothing hits.
|
|
168
|
+
if (params.match) {
|
|
169
|
+
try {
|
|
170
|
+
const allLines = readFileSync(file.path, 'utf-8').split('\n');
|
|
171
|
+
const methodMatch = renderEnclosingMethodMatch(file, params.match, allLines);
|
|
172
|
+
if (methodMatch)
|
|
173
|
+
return methodMatch;
|
|
174
|
+
}
|
|
175
|
+
catch { /* unreadable — fall through to the menu */ }
|
|
176
|
+
}
|
|
177
|
+
filtered = baseFiltered;
|
|
178
|
+
matchMissedSymbols = true;
|
|
179
|
+
}
|
|
180
|
+
filteredSymbolCount = filtered.length;
|
|
181
|
+
// Compute display names + parent class indent (preserves source-order
|
|
182
|
+
// tree structure before any reorder).
|
|
183
|
+
let lastClassName = null;
|
|
184
|
+
const annotated = filtered.map(s => {
|
|
185
|
+
let displayName = s.name;
|
|
186
|
+
let indent = '';
|
|
187
|
+
if (lastClassName && s.name.startsWith(lastClassName + '.')) {
|
|
188
|
+
displayName = s.name.slice(lastClassName.length + 1);
|
|
189
|
+
indent = ' ';
|
|
190
|
+
}
|
|
191
|
+
else if (s.kind === 'class') {
|
|
192
|
+
lastClassName = s.name;
|
|
193
|
+
}
|
|
194
|
+
const callers = callersByName?.get(s.name) ?? [];
|
|
195
|
+
return {
|
|
196
|
+
name: s.name,
|
|
197
|
+
displayName,
|
|
198
|
+
indent,
|
|
199
|
+
kind: s.kind,
|
|
200
|
+
startLine: s.startLine,
|
|
201
|
+
endLine: s.endLine,
|
|
202
|
+
extendsName: s.extendsName,
|
|
203
|
+
implementsNames: s.implementsNames,
|
|
204
|
+
callers,
|
|
205
|
+
callerFileCount: callers.length,
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
// For full view of huge files (no match filter), reorder by caller count
|
|
209
|
+
// desc so most-used symbols surface first; then truncate to top-K with a
|
|
210
|
+
// summary tail. With a match filter we honour the filter exactly — agent
|
|
211
|
+
// is steering, not browsing.
|
|
212
|
+
if (view === 'full' &&
|
|
213
|
+
(!matchPredicate || matchMissedSymbols) &&
|
|
214
|
+
annotated.length > SYMBOLS_HUGE_FILE_THRESHOLD) {
|
|
215
|
+
const sorted = annotated.slice().sort((a, b) => {
|
|
216
|
+
if (b.callerFileCount !== a.callerFileCount) {
|
|
217
|
+
return b.callerFileCount - a.callerFileCount;
|
|
218
|
+
}
|
|
219
|
+
return a.startLine - b.startLine;
|
|
220
|
+
});
|
|
221
|
+
// After reorder, parent-class indent is no longer meaningful (siblings
|
|
222
|
+
// are scattered), so show the full qualified name and drop indent.
|
|
223
|
+
symbolsOut = sorted.slice(0, SYMBOLS_TOP_K).map(s => ({
|
|
224
|
+
...s,
|
|
225
|
+
displayName: s.name,
|
|
226
|
+
indent: '',
|
|
227
|
+
}));
|
|
228
|
+
truncatedSymbolCount = sorted.length - symbolsOut.length;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
symbolsOut = annotated;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Build imports list
|
|
235
|
+
let importsOut = [];
|
|
236
|
+
let totalImportCount = 0;
|
|
237
|
+
if (wantImports) {
|
|
238
|
+
const internalImports = [...new Set(index.edges
|
|
239
|
+
.filter(e => e.from === fileId)
|
|
240
|
+
.map(e => index.files.get(e.to)?.relativePath)
|
|
241
|
+
.filter((p) => !!p))];
|
|
242
|
+
totalImportCount = internalImports.length;
|
|
243
|
+
importsOut = matchPredicate
|
|
244
|
+
? internalImports.filter(p => matchPredicate(p))
|
|
245
|
+
: internalImports;
|
|
246
|
+
}
|
|
247
|
+
// Build importers list
|
|
248
|
+
let importersShown = [];
|
|
249
|
+
let importersFiltered = 0;
|
|
250
|
+
let importersExtra = 0;
|
|
251
|
+
let totalImporterCount = 0;
|
|
252
|
+
let bodyRefImporters = [];
|
|
253
|
+
let bodyRefTotal = 0;
|
|
254
|
+
if (wantImporters) {
|
|
255
|
+
const allImporters = buildAllImporters(fileId, index);
|
|
256
|
+
totalImporterCount = allImporters.length;
|
|
257
|
+
const filteredImporters = matchPredicate
|
|
258
|
+
? allImporters.filter(p => matchPredicate(p))
|
|
259
|
+
: allImporters;
|
|
260
|
+
importersFiltered = filteredImporters.length;
|
|
261
|
+
importersShown = filteredImporters.slice(0, IMPORTERS_LIST_CAP);
|
|
262
|
+
importersExtra = filteredImporters.length - importersShown.length;
|
|
263
|
+
// Body-reference filter: with `match`, the filename filter alone hides the
|
|
264
|
+
// files an agent actually wants — files that *reference* the matched
|
|
265
|
+
// symbol but don't carry it in their path (q16: admin.py registers
|
|
266
|
+
// models.SpatialView via attribute access; not a call edge, filename
|
|
267
|
+
// doesn't match → invisible, and the agent reconstructed this set with 25
|
|
268
|
+
// greps). Scan is REPO-WIDE, not importer-scoped: the cross-language
|
|
269
|
+
// use-sites this section exists for (a JS file referencing a Python
|
|
270
|
+
// symbol) import nothing from the target file, so an importer-only scan
|
|
271
|
+
// misses them — and only a repo-wide scan makes the rendered
|
|
272
|
+
// exhaustiveness claim true (q16 run-3: the agent held "body-refs =
|
|
273
|
+
// admin.py + graph.py" at call 11 and still spent 20 calls hunting a JS
|
|
274
|
+
// frontend that doesn't exist, because nothing said the list was
|
|
275
|
+
// complete). Shape-gated tokens only, so single-word terms find nothing —
|
|
276
|
+
// the section is omitted, never rendered as a misleading "0".
|
|
277
|
+
if (matchPredicate) {
|
|
278
|
+
const alreadyShown = new Set(importersShown);
|
|
279
|
+
for (const [p, f] of index.files) {
|
|
280
|
+
if (p === fileId || alreadyShown.has(p))
|
|
281
|
+
continue;
|
|
282
|
+
const toks = f.contentTokens;
|
|
283
|
+
if (!toks)
|
|
284
|
+
continue;
|
|
285
|
+
for (const k of Object.keys(toks)) {
|
|
286
|
+
if (matchPredicate(k)) {
|
|
287
|
+
bodyRefImporters.push(p);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
bodyRefTotal = bodyRefImporters.length;
|
|
293
|
+
// Source files first — tests are verification, not mechanism.
|
|
294
|
+
bodyRefImporters = [
|
|
295
|
+
...bodyRefImporters.filter(p => !isTestPath(p)),
|
|
296
|
+
...bodyRefImporters.filter(isTestPath),
|
|
297
|
+
].slice(0, BODY_REF_IMPORTERS_CAP);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Related files via the content-token channel (full view only). When the
|
|
301
|
+
// agent passed `match`, scope the source tokens to the matched symbols'
|
|
302
|
+
// line ranges — the match param is the agent's stated intent, and the echo
|
|
303
|
+
// lands at the attend-moment (the call right before the region gets Read).
|
|
304
|
+
// Dedupe is against what THIS payload renders (importsOut/importersShown),
|
|
305
|
+
// not the raw edge set — a god-file's truncated importer list can hide a
|
|
306
|
+
// file that the edge set technically contains.
|
|
307
|
+
let relatedOut = [];
|
|
308
|
+
if (view === 'full') {
|
|
309
|
+
const excludeIds = new Set([fileId, ...importsOut, ...importersShown, ...bodyRefImporters]);
|
|
310
|
+
let sourceTokens = file.contentTokens;
|
|
311
|
+
if (matchPredicate && !matchMissedSymbols && symbolsOut.length > 0 && index.contentTokenPostings.size > 0) {
|
|
312
|
+
try {
|
|
313
|
+
const allLines = readFileSync(file.path, 'utf-8').split('\n');
|
|
314
|
+
const parts = [];
|
|
315
|
+
let taken = 0;
|
|
316
|
+
for (const s of symbolsOut) {
|
|
317
|
+
parts.push(allLines.slice(s.startLine - 1, s.endLine).join('\n'));
|
|
318
|
+
taken += s.endLine - s.startLine + 1;
|
|
319
|
+
if (taken > 3000)
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
sourceTokens = extractContentTokens(parts.join('\n'), fileId) ?? sourceTokens;
|
|
323
|
+
}
|
|
324
|
+
catch { /* unreadable — fall back to file-level tokens */ }
|
|
325
|
+
}
|
|
326
|
+
if (sourceTokens && index.contentTokenPostings.size > 0) {
|
|
327
|
+
relatedOut = deriveRelatedFiles(fileId, sourceTokens, index, excludeIds);
|
|
328
|
+
}
|
|
329
|
+
// Name-echo: filenames matching the agent's match terms (df-gated,
|
|
330
|
+
// separator-insensitive). Regex specs are skipped — terms only.
|
|
331
|
+
if (params.match && relatedOut.length < RELATED_FILES_CAP) {
|
|
332
|
+
const spec = params.match.trim();
|
|
333
|
+
if (!(spec.length >= 2 && spec.startsWith('/') && spec.endsWith('/'))) {
|
|
334
|
+
const terms = spec.split('|').map(s => s.trim()).filter(s => s.length > 0);
|
|
335
|
+
const echoExclude = new Set([...excludeIds, ...relatedOut.map(r => r.fileId)]);
|
|
336
|
+
relatedOut = [
|
|
337
|
+
...relatedOut,
|
|
338
|
+
...deriveNameEchoFiles(terms, index, echoExclude),
|
|
339
|
+
].slice(0, RELATED_FILES_CAP);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Render text.
|
|
344
|
+
const lines = [`${file.relativePath} (${file.lineCount} lines, importedBy: ${file.importedByCount})`];
|
|
345
|
+
if (viewIsInvalid) {
|
|
346
|
+
lines.push(`[note: view "${String(requestedView)}" is not valid — showing full view. Valid views: ${VALID_VIEWS.join(', ')}.]`);
|
|
347
|
+
}
|
|
348
|
+
if (wantSymbols) {
|
|
349
|
+
if (symbolsOut.length > 0) {
|
|
350
|
+
lines.push('');
|
|
351
|
+
if (matchMissedSymbols) {
|
|
352
|
+
lines.push(`[0 of ${totalSymbolCount} symbols match "${params.match}" — showing all symbols instead:]`);
|
|
353
|
+
}
|
|
354
|
+
lines.push('Symbols:');
|
|
355
|
+
for (const s of symbolsOut) {
|
|
356
|
+
const range = s.startLine === s.endLine ? `[L${s.startLine}]` : `[L${s.startLine}-${s.endLine}]`;
|
|
357
|
+
let line = `${s.indent}${s.kind} ${s.displayName} ${range}`;
|
|
358
|
+
if (s.extendsName)
|
|
359
|
+
line += ` extends ${s.extendsName}`;
|
|
360
|
+
if (s.implementsNames.length > 0)
|
|
361
|
+
line += ` implements ${s.implementsNames.join(', ')}`;
|
|
362
|
+
// Inline single caller; newline-per-caller block when ≥2.
|
|
363
|
+
if (wantInlineCallers && s.callers.length > 0) {
|
|
364
|
+
if (s.callers.length <= CALLER_INLINE_MAX) {
|
|
365
|
+
line += ` ← ${s.callers[0]}`;
|
|
366
|
+
lines.push(line);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
lines.push(line);
|
|
370
|
+
for (const c of s.callers) {
|
|
371
|
+
lines.push(`${s.indent} ← ${c}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
lines.push(line);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (truncatedSymbolCount > 0) {
|
|
380
|
+
lines.push(`[+${truncatedSymbolCount} more symbols — pass \`match\` to filter, or \`view: "symbols"\` for full list without callers]`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
else if (matchPredicate && totalSymbolCount > 0) {
|
|
384
|
+
lines.push('');
|
|
385
|
+
lines.push(`Symbols: 0 of ${totalSymbolCount} match "${params.match}".`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Expanded callers section (view: 'callers' only)
|
|
389
|
+
if (wantExpandedCallers && callersByName) {
|
|
390
|
+
let totalSyms = 0;
|
|
391
|
+
let shownSyms = 0;
|
|
392
|
+
lines.push('');
|
|
393
|
+
lines.push('Callers (per exported symbol):');
|
|
394
|
+
for (const [symName, callers] of callersByName) {
|
|
395
|
+
totalSyms++;
|
|
396
|
+
if (matchPredicate && !matchPredicate(symName))
|
|
397
|
+
continue;
|
|
398
|
+
shownSyms++;
|
|
399
|
+
lines.push(` ${symName}:`);
|
|
400
|
+
for (const c of callers)
|
|
401
|
+
lines.push(` ${c}`);
|
|
402
|
+
}
|
|
403
|
+
if (totalSyms === 0) {
|
|
404
|
+
lines.push(' (no symbol-level callers indexed — no exported symbols, no member-expression call sites, or no callers exist)');
|
|
405
|
+
}
|
|
406
|
+
else if (matchPredicate && shownSyms === 0) {
|
|
407
|
+
lines.push(` (0 of ${totalSyms} symbols with callers match "${params.match}")`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// Imports
|
|
411
|
+
if (wantImports) {
|
|
412
|
+
const IMPORTS_LIST_THRESHOLD = 15;
|
|
413
|
+
if (importsOut.length > 0) {
|
|
414
|
+
lines.push('');
|
|
415
|
+
if (importsOut.length <= IMPORTS_LIST_THRESHOLD || matchPredicate) {
|
|
416
|
+
lines.push('Imports:');
|
|
417
|
+
for (const p of importsOut)
|
|
418
|
+
lines.push(p);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
lines.push(`Imports: ${importsOut.length} internal files — pass \`match\` to scope this list (substring or /regex/).`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
else if (matchPredicate && totalImportCount > 0) {
|
|
425
|
+
lines.push('');
|
|
426
|
+
lines.push(`Imports: 0 of ${totalImportCount} match "${params.match}".`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Importers — section by test/source when both groups present so the agent
|
|
430
|
+
// has a labeled attention slot for test files (they're verification, not
|
|
431
|
+
// peer call sites; agent triages them differently if labeled).
|
|
432
|
+
if (wantImporters) {
|
|
433
|
+
if (importersFiltered > 0) {
|
|
434
|
+
const testImporters = importersShown.filter(isTestPath);
|
|
435
|
+
const sourceImporters = importersShown.filter(p => !isTestPath(p));
|
|
436
|
+
lines.push('');
|
|
437
|
+
lines.push(`Importers (${importersFiltered}${importersExtra > 0 ? `, showing ${importersShown.length}` : ''}):`);
|
|
438
|
+
if (testImporters.length > 0 && sourceImporters.length > 0) {
|
|
439
|
+
lines.push(` Source (${sourceImporters.length}):`);
|
|
440
|
+
for (const p of sourceImporters)
|
|
441
|
+
lines.push(` ${p}`);
|
|
442
|
+
lines.push(` Tests (${testImporters.length}):`);
|
|
443
|
+
for (const p of testImporters)
|
|
444
|
+
lines.push(` ${p}`);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
for (const p of importersShown)
|
|
448
|
+
lines.push(p);
|
|
449
|
+
}
|
|
450
|
+
if (importersExtra > 0)
|
|
451
|
+
lines.push(`[+${importersExtra} more — narrow with \`match\`]`);
|
|
452
|
+
}
|
|
453
|
+
else if (matchPredicate && totalImporterCount > 0) {
|
|
454
|
+
lines.push('');
|
|
455
|
+
lines.push(`Importers: 0 of ${totalImporterCount} match "${params.match}" by filename.`);
|
|
456
|
+
}
|
|
457
|
+
// Body references: the grep-replacement answer to "who uses <match>" —
|
|
458
|
+
// files whose CONTENT references the matched term even though their
|
|
459
|
+
// filename doesn't. Repo-wide scan, so the closing exhaustiveness line is
|
|
460
|
+
// a true claim: absence here = no other indexed file names the term.
|
|
461
|
+
if (bodyRefImporters.length > 0) {
|
|
462
|
+
lines.push('');
|
|
463
|
+
lines.push(`Files referencing "${params.match}" in content (${bodyRefTotal}${bodyRefTotal > bodyRefImporters.length ? `, showing ${bodyRefImporters.length}` : ''}) — use-sites the lists above miss:`);
|
|
464
|
+
for (const p of bodyRefImporters)
|
|
465
|
+
lines.push(` ${p}`);
|
|
466
|
+
if (bodyRefTotal > bodyRefImporters.length) {
|
|
467
|
+
lines.push(` The count (${bodyRefTotal}) is exhaustive over indexed file content — exactly that many files reference "${params.match}" as a named identifier; the list is truncated, narrow \`match\` to see the rest. Do not grep to re-verify.`);
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
lines.push(` This list is exhaustive over indexed file content: no other file references "${params.match}" as a named identifier. Do not grep to re-verify, and do not hunt for use-sites in other subsystems.`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// Related (content-token channel)
|
|
475
|
+
if (relatedOut.length > 0) {
|
|
476
|
+
lines.push('');
|
|
477
|
+
lines.push('Related (shares rare tokens, no import edge — name-reference relations the import graph cannot see):');
|
|
478
|
+
for (const r of relatedOut) {
|
|
479
|
+
if (r.viaName) {
|
|
480
|
+
lines.push(` ${r.fileId} — filename matches "${r.viaName}"`);
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
const ids = [r.fileId, ...(r.alsoFileIds ?? [])];
|
|
484
|
+
const names = ids.slice(0, 2).join(', ') + (ids.length > 2 ? ` +${ids.length - 2}` : '');
|
|
485
|
+
const shown = r.tokens.slice(0, 3).map(t => `\`${t}\``).join(', ');
|
|
486
|
+
const extra = r.tokens.length - 3;
|
|
487
|
+
lines.push(` ${names} — shares ${shown}${extra > 0 ? ` +${extra}` : ''}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// Next-step pointer (only when there's something useful to say)
|
|
492
|
+
const nextHint = computeNextHint({
|
|
493
|
+
hasMatch: !!matchPredicate,
|
|
494
|
+
wantSymbols,
|
|
495
|
+
filteredSymbolCount,
|
|
496
|
+
totalSymbolCount,
|
|
497
|
+
});
|
|
498
|
+
if (nextHint) {
|
|
499
|
+
lines.push('');
|
|
500
|
+
lines.push(nextHint);
|
|
501
|
+
}
|
|
502
|
+
return { __rawText: lines.join('\n') };
|
|
503
|
+
}
|
|
504
|
+
// Grade-2 caps. Bodies are the expensive payload, so bound them; pointers are
|
|
505
|
+
// cheap so they stay generous.
|
|
506
|
+
const SLICE_SYMBOL_CAP = 4; // distinct symbols sliced in one call
|
|
507
|
+
const SLICE_LINE_BUDGET = 450; // total body lines across all slices
|
|
508
|
+
const SLICE_CALLEE_CAP = 10; // callee pointers per symbol
|
|
509
|
+
// Resolve requested symbol name(s) → SymbolNodes in `file`, tiered so a bare
|
|
510
|
+
// last-segment like `serialize` lands on `Graph.serialize` and never on the
|
|
511
|
+
// substring lookalike `serialized_graph`. Tiers, first non-empty wins per name:
|
|
512
|
+
// 1. exact full name (`Graph.serialize`) 2. exact last segment (`serialize`)
|
|
513
|
+
// 3. case-insensitive substring (last resort, e.g. a typo or partial recall)
|
|
514
|
+
function resolveSliceSymbols(file, spec) {
|
|
515
|
+
const names = spec.split(/[,|]/).map(s => s.trim()).filter(Boolean);
|
|
516
|
+
const picked = new Map(); // by symbol id, dedupe
|
|
517
|
+
for (const q of names) {
|
|
518
|
+
const ql = q.toLowerCase();
|
|
519
|
+
const exact = file.symbols.filter(s => s.name.toLowerCase() === ql);
|
|
520
|
+
const lastSeg = exact.length ? exact
|
|
521
|
+
: file.symbols.filter(s => s.name.toLowerCase().split('.').pop() === ql);
|
|
522
|
+
const tier = lastSeg.length ? lastSeg
|
|
523
|
+
: file.symbols.filter(s => s.name.toLowerCase().includes(ql));
|
|
524
|
+
for (const s of tier)
|
|
525
|
+
picked.set(s.id, s);
|
|
526
|
+
}
|
|
527
|
+
return [...picked.values()].sort((a, b) => a.startLine - b.startLine);
|
|
528
|
+
}
|
|
529
|
+
// In-tool grep fallback for `--symbol` when no declared symbol matches: locate
|
|
530
|
+
// the token(s) in the file body and return the matching line regions with
|
|
531
|
+
// context, so the agent never shells out to grep. Caps keep it byte-light.
|
|
532
|
+
const CONTENT_CONTEXT = 2; // context lines each side of a match
|
|
533
|
+
const CONTENT_REGION_CAP = 8; // distinct regions returned
|
|
534
|
+
const CONTENT_LINE_CAP = 70; // total lines returned
|
|
535
|
+
function renderContentMatch(file, spec, allLines) {
|
|
536
|
+
const terms = spec.split(/[,|]/).map(s => s.trim().toLowerCase()).filter(Boolean);
|
|
537
|
+
const hitLines = []; // 0-based
|
|
538
|
+
for (let i = 0; i < allLines.length; i++) {
|
|
539
|
+
const low = allLines[i].toLowerCase();
|
|
540
|
+
if (terms.some(t => low.includes(t)))
|
|
541
|
+
hitLines.push(i);
|
|
542
|
+
}
|
|
543
|
+
if (hitLines.length === 0) {
|
|
544
|
+
const avail = file.symbols
|
|
545
|
+
.filter(s => s.isExported || s.kind === 'class' || s.kind === 'function' || s.kind === 'method')
|
|
546
|
+
.slice(0, 25).map(s => s.name).join(', ');
|
|
547
|
+
return {
|
|
548
|
+
__rawText: `${file.relativePath}: no symbol named "${spec}", and the token does not appear in this file's content either.\n` +
|
|
549
|
+
(avail ? `Declared symbols here: ${avail}${file.symbols.length > 25 ? ', …' : ''}` : 'No top-level symbols indexed in this file.') +
|
|
550
|
+
'\nThe identifier is not in this file — check the file path, or the value may be injected at runtime from elsewhere.',
|
|
551
|
+
error: `no symbol or content match for "${spec}"`,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
// Merge nearby hits into regions.
|
|
555
|
+
const regions = [];
|
|
556
|
+
for (const ln of hitLines) {
|
|
557
|
+
const start = Math.max(0, ln - CONTENT_CONTEXT);
|
|
558
|
+
const end = Math.min(allLines.length - 1, ln + CONTENT_CONTEXT);
|
|
559
|
+
const last = regions[regions.length - 1];
|
|
560
|
+
if (last && start <= last[1] + 1)
|
|
561
|
+
last[1] = Math.max(last[1], end);
|
|
562
|
+
else
|
|
563
|
+
regions.push([start, end]);
|
|
564
|
+
}
|
|
565
|
+
const lines = [
|
|
566
|
+
`${file.relativePath} (${file.lineCount} lines) — no declared symbol matches "${spec}"; showing ${hitLines.length} content match${hitLines.length === 1 ? '' : 'es'} (in-tool grep, so you don't have to):`,
|
|
567
|
+
];
|
|
568
|
+
let shown = 0;
|
|
569
|
+
let r = 0;
|
|
570
|
+
for (const [start, end] of regions) {
|
|
571
|
+
if (r >= CONTENT_REGION_CAP || shown >= CONTENT_LINE_CAP) {
|
|
572
|
+
lines.push(` … [+${regions.length - r} more match regions — narrow \`--symbol\` or read the file]`);
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
r++;
|
|
576
|
+
lines.push('');
|
|
577
|
+
for (let i = start; i <= end && shown < CONTENT_LINE_CAP; i++) {
|
|
578
|
+
lines.push(`${String(i + 1).padStart(5)} ${allLines[i]}`);
|
|
579
|
+
shown++;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
lines.push('');
|
|
583
|
+
lines.push('These are content matches, not a declared symbol — the token has no static definition here (often runtime/template-injected). Grepping this file again returns these same lines; you already have them.');
|
|
584
|
+
// STOP-GUESSING menu: a wrong `--symbol` guess still produces content hits (the word appears
|
|
585
|
+
// somewhere in the body), which reads as "productive" and invites re-guessing OTHER names — the
|
|
586
|
+
// #1 wasted-call pattern (q21: 3 serial gs on one file guessing upload/unzip/write_zip_file…,
|
|
587
|
+
// none of which are methods). Surface the file's ACTUAL declared symbols so the next call picks a
|
|
588
|
+
// real one instead of guessing again. Same menu the no-hit branch already shows.
|
|
589
|
+
const menu = file.symbols
|
|
590
|
+
.filter(s => s.isExported || s.kind === 'class' || s.kind === 'function' || s.kind === 'method')
|
|
591
|
+
.slice(0, 25).map(s => s.name);
|
|
592
|
+
if (menu.length) {
|
|
593
|
+
lines.push('');
|
|
594
|
+
lines.push(`"${spec}" is NOT a declared symbol here. This file's methods are: ${menu.join(', ')}${file.symbols.length > 25 ? ', …' : ''}`);
|
|
595
|
+
lines.push(`These are the file's only declared symbols — a name that isn't listed has no body to slice. Is the name you need in this list? If yes, pass it; if not, the token is runtime/template text — Read the file rather than guessing another \`--symbol\`.`);
|
|
596
|
+
}
|
|
597
|
+
return { __rawText: lines.join('\n') };
|
|
598
|
+
}
|
|
599
|
+
// ── In-body grep → ENCLOSING METHOD (the gs-trace-cost fix) ─────────────────
|
|
600
|
+
// When neither a declared symbol (`--symbol`) nor a symbol-name filter
|
|
601
|
+
// (`--match`) hits, the agent's intent was almost always "show me the body that
|
|
602
|
+
// mentions these terms". The old answers were both wrong: `--symbol` miss dumped
|
|
603
|
+
// raw ±2-line regions; `--match` miss dumped the full symbol MENU (which the
|
|
604
|
+
// agent then ignored, re-reading ~950 lines — jmri Q1). This maps each body hit
|
|
605
|
+
// to its INNERMOST enclosing symbol via [startLine,endLine] ranges and returns
|
|
606
|
+
// those METHOD bodies, ranked by how many distinct terms each covers — the
|
|
607
|
+
// meaningful unit, not a line window and not a menu.
|
|
608
|
+
const METHOD_MATCH_SYMBOL_CAP = 4; // distinct enclosing methods sliced
|
|
609
|
+
const METHOD_MATCH_LINE_BUDGET = 240; // total body lines across all method slices
|
|
610
|
+
const METHOD_MATCH_ORPHAN_CAP = 6; // raw context lines for module-level hits
|
|
611
|
+
// Returns null when the spec is a /regex/ or no body line matches — caller then
|
|
612
|
+
// falls back to the existing menu / raw-region behavior.
|
|
613
|
+
function renderEnclosingMethodMatch(file, spec, allLines) {
|
|
614
|
+
const trimmed = spec.trim();
|
|
615
|
+
// Regex specs aren't word-splittable into terms; leave them to the old path.
|
|
616
|
+
if (trimmed.length >= 2 && trimmed.startsWith('/') && trimmed.endsWith('/'))
|
|
617
|
+
return null;
|
|
618
|
+
const terms = trimmed.split(/[\s,|]+/).map(s => s.trim().toLowerCase()).filter(Boolean);
|
|
619
|
+
if (terms.length === 0)
|
|
620
|
+
return null;
|
|
621
|
+
// hit line (1-based) → set of distinct terms found on it
|
|
622
|
+
const hitTermsByLine = new Map();
|
|
623
|
+
for (let i = 0; i < allLines.length; i++) {
|
|
624
|
+
const low = allLines[i].toLowerCase();
|
|
625
|
+
const matched = terms.filter(t => low.includes(t));
|
|
626
|
+
if (matched.length)
|
|
627
|
+
hitTermsByLine.set(i + 1, new Set(matched));
|
|
628
|
+
}
|
|
629
|
+
if (hitTermsByLine.size === 0)
|
|
630
|
+
return null;
|
|
631
|
+
const candidates = file.symbols.filter(s => s.isExported || s.kind === 'class' || s.kind === 'function' || s.kind === 'method');
|
|
632
|
+
const bySym = new Map();
|
|
633
|
+
const orphanLines = [];
|
|
634
|
+
for (const [line, matchedTerms] of hitTermsByLine) {
|
|
635
|
+
let best = null;
|
|
636
|
+
for (const s of candidates) {
|
|
637
|
+
if (s.startLine <= line && line <= s.endLine) {
|
|
638
|
+
if (!best || (s.endLine - s.startLine) < (best.endLine - best.startLine))
|
|
639
|
+
best = s;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (!best) {
|
|
643
|
+
orphanLines.push(line);
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
let agg = bySym.get(best.id);
|
|
647
|
+
if (!agg) {
|
|
648
|
+
agg = { sym: best, terms: new Set() };
|
|
649
|
+
bySym.set(best.id, agg);
|
|
650
|
+
}
|
|
651
|
+
for (const t of matchedTerms)
|
|
652
|
+
agg.terms.add(t);
|
|
653
|
+
}
|
|
654
|
+
if (bySym.size === 0 && orphanLines.length === 0)
|
|
655
|
+
return null;
|
|
656
|
+
// Rank methods by distinct-term coverage desc, then source order.
|
|
657
|
+
const ranked = [...bySym.values()].sort((a, b) => b.terms.size !== a.terms.size ? b.terms.size - a.terms.size : a.sym.startLine - b.sym.startLine);
|
|
658
|
+
const lines = [
|
|
659
|
+
`${file.relativePath} (${file.lineCount} lines) — no declared symbol matches "${spec}"; ` +
|
|
660
|
+
`${hitTermsByLine.size} content match${hitTermsByLine.size === 1 ? '' : 'es'}` +
|
|
661
|
+
(ranked.length ? ` in ${ranked.length} method${ranked.length === 1 ? '' : 's'}` : '') +
|
|
662
|
+
` (in-tool grep → method body, so you don't have to):`,
|
|
663
|
+
];
|
|
664
|
+
let spent = 0;
|
|
665
|
+
let shownSyms = 0;
|
|
666
|
+
for (const agg of ranked) {
|
|
667
|
+
if (shownSyms >= METHOD_MATCH_SYMBOL_CAP || spent >= METHOD_MATCH_LINE_BUDGET) {
|
|
668
|
+
const rest = ranked.slice(shownSyms).map(a => a.sym.name).join(', ');
|
|
669
|
+
lines.push('');
|
|
670
|
+
lines.push(` … [+${ranked.length - shownSyms} more matching method${ranked.length - shownSyms === 1 ? '' : 's'}: ${rest} — re-call \`--symbol <name>\` for one]`);
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
shownSyms++;
|
|
674
|
+
const s = agg.sym;
|
|
675
|
+
lines.push('');
|
|
676
|
+
lines.push(`━━ ${s.kind} ${s.name} [L${s.startLine}-${s.endLine}] — matches ${agg.terms.size}/${terms.length} terms: ${[...agg.terms].join(', ')} ━━`);
|
|
677
|
+
const want = s.endLine - s.startLine + 1;
|
|
678
|
+
const take = Math.min(want, Math.max(0, METHOD_MATCH_LINE_BUDGET - spent));
|
|
679
|
+
for (let i = s.startLine; i < s.startLine + take && i <= allLines.length; i++) {
|
|
680
|
+
lines.push(`${String(i).padStart(5)} ${allLines[i - 1]}`);
|
|
681
|
+
}
|
|
682
|
+
if (take < want)
|
|
683
|
+
lines.push(` … [+${want - take} more lines — re-call \`--symbol ${s.name}\` for the full body]`);
|
|
684
|
+
spent += take;
|
|
685
|
+
}
|
|
686
|
+
// Module-level hits (no enclosing method): a short raw-context tail so an
|
|
687
|
+
// import/constant/decorator match isn't silently dropped.
|
|
688
|
+
if (orphanLines.length > 0) {
|
|
689
|
+
lines.push('');
|
|
690
|
+
lines.push(`Also matched outside any method (module level):`);
|
|
691
|
+
let shown = 0;
|
|
692
|
+
for (const ln of orphanLines) {
|
|
693
|
+
if (shown >= METHOD_MATCH_ORPHAN_CAP) {
|
|
694
|
+
lines.push(` … [+${orphanLines.length - shown} more module-level match lines]`);
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
lines.push(`${String(ln).padStart(5)} ${allLines[ln - 1]}`);
|
|
698
|
+
shown++;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
lines.push('');
|
|
702
|
+
lines.push('These are the methods whose bodies contain the term(s); grepping this file returns the same lines you already have. Follow up with `--symbol <name>` for a full body.');
|
|
703
|
+
return { __rawText: lines.join('\n') };
|
|
704
|
+
}
|
|
705
|
+
function renderSymbolSlice(index, fileId, file, spec) {
|
|
706
|
+
const wanted = resolveSliceSymbols(file, spec);
|
|
707
|
+
let allLines;
|
|
708
|
+
try {
|
|
709
|
+
allLines = readFileSync(file.path, 'utf-8').split('\n');
|
|
710
|
+
}
|
|
711
|
+
catch (e) {
|
|
712
|
+
return { error: `could not read ${file.relativePath}: ${e}` };
|
|
713
|
+
}
|
|
714
|
+
// No declared symbol matched — fall back to a CONTENT match (in-tool grep):
|
|
715
|
+
// return the body lines where the requested token(s) live, with context. This
|
|
716
|
+
// is the answer for tokens that have no static symbol (runtime/template-
|
|
717
|
+
// injected data like `arches.termSearchTypes`, string keys, config values) —
|
|
718
|
+
// so the agent gets the body text from the SAME call instead of shelling out
|
|
719
|
+
// to grep and guessing spellings (q19's 5 empty greps).
|
|
720
|
+
if (wanted.length === 0) {
|
|
721
|
+
const methodMatch = renderEnclosingMethodMatch(file, spec, allLines);
|
|
722
|
+
return methodMatch ?? renderContentMatch(file, spec, allLines);
|
|
723
|
+
}
|
|
724
|
+
// Caller pointers: reuse the file-level builder (exported symbols only).
|
|
725
|
+
const callersByName = new Map();
|
|
726
|
+
for (const entry of buildCallersForFile(fileId, file, index)) {
|
|
727
|
+
callersByName.set(entry.exportedSymbol, entry.callers);
|
|
728
|
+
}
|
|
729
|
+
// Callee pointers: resolve this symbol's outgoing calls edges → target location.
|
|
730
|
+
const symInfo = buildSymbolInfoMap(index);
|
|
731
|
+
const locById = new Map();
|
|
732
|
+
for (const f of index.files.values()) {
|
|
733
|
+
for (const s of f.symbols)
|
|
734
|
+
locById.set(s.id, { rel: f.relativePath, start: s.startLine, end: s.endLine });
|
|
735
|
+
}
|
|
736
|
+
const calleesOf = (symId) => {
|
|
737
|
+
const seen = new Set();
|
|
738
|
+
const out = [];
|
|
739
|
+
for (const edge of index.symbolEdges) {
|
|
740
|
+
if (edge.type !== 'calls' || edge.from !== symId)
|
|
741
|
+
continue;
|
|
742
|
+
if (seen.has(edge.to))
|
|
743
|
+
continue;
|
|
744
|
+
seen.add(edge.to);
|
|
745
|
+
const meta = symInfo.get(edge.to);
|
|
746
|
+
if (!meta)
|
|
747
|
+
continue;
|
|
748
|
+
const loc = locById.get(edge.to);
|
|
749
|
+
out.push(loc
|
|
750
|
+
? `${meta.name} → ${loc.rel} [L${loc.start}-${loc.end}]`
|
|
751
|
+
: `${meta.name} → ${meta.file}`);
|
|
752
|
+
if (out.length >= SLICE_CALLEE_CAP)
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
return out;
|
|
756
|
+
};
|
|
757
|
+
const lines = [`${file.relativePath} (${file.lineCount} lines) — bodies for: ${wanted.map(s => s.name).join(', ')}`];
|
|
758
|
+
const sliced = wanted.slice(0, SLICE_SYMBOL_CAP);
|
|
759
|
+
if (wanted.length > sliced.length) {
|
|
760
|
+
lines.push(`[${wanted.length} symbols matched; slicing the first ${sliced.length} — narrow \`--symbol\` for the rest: ${wanted.slice(sliced.length).map(s => s.name).join(', ')}]`);
|
|
761
|
+
}
|
|
762
|
+
let spent = 0;
|
|
763
|
+
for (const s of sliced) {
|
|
764
|
+
lines.push('');
|
|
765
|
+
lines.push(`━━ ${s.kind} ${s.name} [L${s.startLine}-${s.endLine}]${s.extendsName ? ` extends ${s.extendsName}` : ''} ━━`);
|
|
766
|
+
const want = s.endLine - s.startLine + 1;
|
|
767
|
+
const room = Math.max(0, SLICE_LINE_BUDGET - spent);
|
|
768
|
+
if (room === 0) {
|
|
769
|
+
lines.push(` [line budget reached — re-call \`--symbol ${s.name}\` alone to read this body]`);
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
const take = Math.min(want, room);
|
|
773
|
+
for (let i = s.startLine; i < s.startLine + take && i <= allLines.length; i++) {
|
|
774
|
+
lines.push(`${String(i).padStart(5)} ${allLines[i - 1]}`);
|
|
775
|
+
}
|
|
776
|
+
if (take < want)
|
|
777
|
+
lines.push(` … [+${want - take} more lines truncated by budget — re-call \`--symbol ${s.name}\` alone for the full body]`);
|
|
778
|
+
spent += take;
|
|
779
|
+
}
|
|
780
|
+
// Pointers (not bodies): who calls this, what this calls — each is one directed `--symbol` hop away.
|
|
781
|
+
const callers = callersByName.get(s.name) ?? [];
|
|
782
|
+
if (callers.length)
|
|
783
|
+
lines.push(` callers: ${callers.slice(0, 6).join(' · ')}`);
|
|
784
|
+
const callees = calleesOf(s.id);
|
|
785
|
+
if (callees.length)
|
|
786
|
+
lines.push(` calls: ${callees.join(' · ')}`);
|
|
787
|
+
if (!callers.length && !callees.length)
|
|
788
|
+
lines.push(' (no indexed callers/callees)');
|
|
789
|
+
}
|
|
790
|
+
lines.push('');
|
|
791
|
+
lines.push('Bodies delivered inline — no Read needed. Follow `calls:`/`callers:` pointers with another `gs <file> --symbol <name>` to hop without windowing.');
|
|
792
|
+
return { __rawText: lines.join('\n') };
|
|
793
|
+
}
|
|
794
|
+
function computeNextHint(opts) {
|
|
795
|
+
if (opts.hasMatch && opts.wantSymbols && opts.filteredSymbolCount === 0 && opts.totalSymbolCount > 0) {
|
|
796
|
+
return 'Next: relax or drop `match`; or call again with a narrower `view` (e.g. "imports", "importers").';
|
|
797
|
+
}
|
|
798
|
+
return '';
|
|
799
|
+
}
|
|
800
|
+
// Full importer list (inbound edges) — capped at IMPORTERS_LIST_CAP at display
|
|
801
|
+
// time, not here.
|
|
802
|
+
function buildAllImporters(fileId, index) {
|
|
803
|
+
const ids = index.inEdges.get(fileId) ?? [];
|
|
804
|
+
return ids
|
|
805
|
+
.map(id => index.files.get(id)?.relativePath)
|
|
806
|
+
.filter((p) => !!p)
|
|
807
|
+
.sort();
|
|
808
|
+
}
|
|
809
|
+
// Compile a `match` filter for GS. Wrapped in `/.../` → regex; `a|b` →
|
|
810
|
+
// OR of case-insensitive substrings; otherwise case-insensitive substring.
|
|
811
|
+
// Returns null when no filter is requested or the spec is empty.
|
|
812
|
+
function compileMatchPredicate(spec) {
|
|
813
|
+
if (!spec)
|
|
814
|
+
return null;
|
|
815
|
+
const trimmed = spec.trim();
|
|
816
|
+
if (!trimmed)
|
|
817
|
+
return null;
|
|
818
|
+
if (trimmed.length >= 2 && trimmed.startsWith('/') && trimmed.endsWith('/')) {
|
|
819
|
+
const body = trimmed.slice(1, -1);
|
|
820
|
+
try {
|
|
821
|
+
const re = new RegExp(body, 'i');
|
|
822
|
+
return (s) => re.test(s);
|
|
823
|
+
}
|
|
824
|
+
catch {
|
|
825
|
+
// Fall through to substring on malformed regex
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (trimmed.includes('|')) {
|
|
829
|
+
const alternatives = trimmed
|
|
830
|
+
.split('|')
|
|
831
|
+
.map(s => s.trim().toLowerCase())
|
|
832
|
+
.filter(s => s.length > 0);
|
|
833
|
+
if (alternatives.length > 1) {
|
|
834
|
+
return (s) => {
|
|
835
|
+
const lower = s.toLowerCase();
|
|
836
|
+
return alternatives.some(a => lower.includes(a));
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
const lower = trimmed.toLowerCase();
|
|
841
|
+
return (s) => s.toLowerCase().includes(lower);
|
|
842
|
+
}
|
|
843
|
+
// ============================================================================
|
|
844
|
+
// Helper: build a flat map of symbolId → { name, file, kind }
|
|
845
|
+
// ============================================================================
|
|
846
|
+
function buildSymbolInfoMap(index) {
|
|
847
|
+
const map = new Map();
|
|
848
|
+
for (const file of index.files.values()) {
|
|
849
|
+
for (const sym of file.symbols) {
|
|
850
|
+
map.set(sym.id, { name: sym.name, file: file.relativePath, kind: sym.kind });
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return map;
|
|
854
|
+
}
|
|
855
|
+
// ============================================================================
|
|
856
|
+
// Helper: find file by path (exact relative, or suffix)
|
|
857
|
+
// ============================================================================
|
|
858
|
+
function findFileByPath(index, pathQuery) {
|
|
859
|
+
if (!pathQuery)
|
|
860
|
+
return null;
|
|
861
|
+
// Normalize
|
|
862
|
+
const normalized = pathQuery.replace(/\\/g, '/');
|
|
863
|
+
// Exact match first
|
|
864
|
+
if (index.files.has(normalized)) {
|
|
865
|
+
return [normalized, index.files.get(normalized)];
|
|
866
|
+
}
|
|
867
|
+
// Suffix match
|
|
868
|
+
for (const [id, file] of index.files) {
|
|
869
|
+
if (id.endsWith(normalized) || id.includes(normalized)) {
|
|
870
|
+
return [id, file];
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return null;
|
|
874
|
+
}
|
|
875
|
+
//# sourceMappingURL=tools.js.map
|