devsmind-mcp 2.4.0 → 4.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 +317 -234
- package/dist/cli/activity.d.ts +9 -0
- package/dist/cli/activity.js +69 -0
- package/dist/cli/activity.js.map +1 -0
- package/dist/cli/describe.d.ts +31 -0
- package/dist/cli/describe.js +201 -0
- package/dist/cli/describe.js.map +1 -0
- package/dist/cli/diff.d.ts +14 -0
- package/dist/cli/diff.js +161 -0
- package/dist/cli/diff.js.map +1 -0
- package/dist/cli/embed.d.ts +6 -0
- package/dist/cli/embed.js +109 -0
- package/dist/cli/embed.js.map +1 -0
- package/dist/cli/extract-agent.d.ts +66 -0
- package/dist/cli/extract-agent.js +192 -0
- package/dist/cli/extract-agent.js.map +1 -0
- package/dist/cli/feedback.d.ts +11 -0
- package/dist/cli/feedback.js +90 -0
- package/dist/cli/feedback.js.map +1 -0
- package/dist/cli/index.js +139 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +30 -0
- package/dist/cli/init.js +89 -18
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/integrations/memory-topics.d.ts +55 -0
- package/dist/cli/integrations/memory-topics.js +318 -0
- package/dist/cli/integrations/memory-topics.js.map +1 -0
- package/dist/cli/integrations/memory.d.ts +17 -6
- package/dist/cli/integrations/memory.js +52 -139
- package/dist/cli/integrations/memory.js.map +1 -1
- package/dist/cli/integrations/prompt.d.ts +8 -0
- package/dist/cli/integrations/prompt.js +24 -3
- package/dist/cli/integrations/prompt.js.map +1 -1
- package/dist/cli/integrations/registry.d.ts +26 -9
- package/dist/cli/integrations/registry.js +61 -30
- package/dist/cli/integrations/registry.js.map +1 -1
- package/dist/cli/llm-client.d.ts +116 -0
- package/dist/cli/llm-client.js +322 -0
- package/dist/cli/llm-client.js.map +1 -0
- package/dist/cli/rule.d.ts +22 -2
- package/dist/cli/rule.js +110 -34
- package/dist/cli/rule.js.map +1 -1
- package/dist/cli/runner.d.ts +21 -0
- package/dist/cli/runner.js +94 -276
- package/dist/cli/runner.js.map +1 -1
- package/dist/cli/sync-progress.d.ts +9 -0
- package/dist/cli/sync-progress.js +41 -0
- package/dist/cli/sync-progress.js.map +1 -0
- package/dist/cli/sync.js +5 -2
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/view.js +3 -2
- package/dist/cli/view.js.map +1 -1
- package/dist/cli/workflow.js +26 -20
- package/dist/cli/workflow.js.map +1 -1
- package/dist/db/activity-graph.d.ts +55 -0
- package/dist/db/activity-graph.js +314 -0
- package/dist/db/activity-graph.js.map +1 -0
- package/dist/db/activity.d.ts +221 -0
- package/dist/db/activity.js +348 -0
- package/dist/db/activity.js.map +1 -0
- package/dist/db/analyze.js +18 -6
- package/dist/db/analyze.js.map +1 -1
- package/dist/db/database.d.ts +654 -102
- package/dist/db/database.js +1911 -558
- package/dist/db/database.js.map +1 -1
- package/dist/db/edges.d.ts +24 -0
- package/dist/db/edges.js +84 -0
- package/dist/db/edges.js.map +1 -1
- package/dist/db/embedder.d.ts +39 -0
- package/dist/db/embedder.js +0 -0
- package/dist/db/embedder.js.map +1 -0
- package/dist/db/feedback.d.ts +128 -0
- package/dist/db/feedback.js +182 -0
- package/dist/db/feedback.js.map +1 -0
- package/dist/db/file-diff.d.ts +32 -0
- package/dist/db/file-diff.js +110 -0
- package/dist/db/file-diff.js.map +1 -0
- package/dist/db/grep.d.ts +78 -0
- package/dist/db/grep.js +475 -0
- package/dist/db/grep.js.map +1 -0
- package/dist/db/index-build.d.ts +75 -0
- package/dist/db/index-build.js +177 -0
- package/dist/db/index-build.js.map +1 -0
- package/dist/db/message-revert.d.ts +63 -0
- package/dist/db/message-revert.js +258 -0
- package/dist/db/message-revert.js.map +1 -0
- package/dist/db/revert.d.ts +31 -0
- package/dist/db/revert.js +108 -0
- package/dist/db/revert.js.map +1 -0
- package/dist/db/schema.d.ts +49 -4
- package/dist/db/schema.js +125 -73
- package/dist/db/schema.js.map +1 -1
- package/dist/db/search-index.d.ts +65 -0
- package/dist/db/search-index.js +74 -0
- package/dist/db/search-index.js.map +1 -0
- package/dist/db/staging.d.ts +91 -5
- package/dist/db/staging.js +148 -22
- package/dist/db/staging.js.map +1 -1
- package/dist/mcp/server.d.ts +29 -7
- package/dist/mcp/server.js +2637 -878
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/vendor/3d-force-graph.min.js +5 -0
- package/dist/mcp/vendor/force-graph.min.js +5 -0
- package/dist/mcp/vendor/model/model_int8.onnx +0 -0
- package/dist/mcp/vendor/model/vocab.txt +30522 -0
- package/dist/mcp/vendor/three.min.js +7 -0
- package/dist/mcp/view.css +419 -0
- package/dist/mcp/view.html +161 -0
- package/dist/mcp/view.js +245 -0
- package/dist/mcp/view_chat.js +382 -0
- package/dist/mcp/view_graph.js +576 -0
- package/dist/mcp/visualizer.d.ts +25 -2
- package/dist/mcp/visualizer.js +31 -4
- package/dist/mcp/visualizer.js.map +1 -1
- package/dist/utils/ast.d.ts +89 -0
- package/dist/utils/ast.js +378 -10
- package/dist/utils/ast.js.map +1 -1
- package/dist/utils/diff.d.ts +44 -0
- package/dist/utils/diff.js +78 -0
- package/dist/utils/diff.js.map +1 -0
- package/dist/utils/edit.d.ts +6 -0
- package/dist/utils/edit.js +47 -14
- package/dist/utils/edit.js.map +1 -1
- package/dist/utils/scanner.d.ts +6 -4
- package/dist/utils/scanner.js +18 -8
- package/dist/utils/scanner.js.map +1 -1
- package/dist/utils/tokenize.d.ts +45 -0
- package/dist/utils/tokenize.js +129 -0
- package/dist/utils/tokenize.js.map +1 -0
- package/dist/utils/version.d.ts +14 -0
- package/dist/utils/version.js +61 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +18 -5
- package/dist/mcp/visualizer_2d.html +0 -635
- package/dist/mcp/visualizer_3d.html +0 -613
package/dist/utils/ast.d.ts
CHANGED
|
@@ -31,6 +31,12 @@ interface ParsedNodeId {
|
|
|
31
31
|
className?: string;
|
|
32
32
|
memberName?: string;
|
|
33
33
|
}
|
|
34
|
+
export interface ImportInfo {
|
|
35
|
+
importedName: string;
|
|
36
|
+
moduleSpecifier: string;
|
|
37
|
+
isDefault: boolean;
|
|
38
|
+
isNamespace?: boolean;
|
|
39
|
+
}
|
|
34
40
|
/** A used reference that resolves to a real repo file but has no node — a Phase-1 gap. */
|
|
35
41
|
export interface MissingRef {
|
|
36
42
|
sourceNodeId: string;
|
|
@@ -41,6 +47,27 @@ export interface MissingRef {
|
|
|
41
47
|
* Parses a DevsMind node ID into constituent parts
|
|
42
48
|
*/
|
|
43
49
|
export declare function parseNodeId(id: string): ParsedNodeId | null;
|
|
50
|
+
/** One RTK Query endpoint found by {@link detectRtkEndpointAliases} — the endpoint's own declared
|
|
51
|
+
* key, plus the hook names RTK Query's codegen convention derives from it. */
|
|
52
|
+
export interface RtkEndpointAlias {
|
|
53
|
+
/** The endpoint key as written inside `endpoints: (builder) => ({ ... })` — expected to match
|
|
54
|
+
* an existing node's declared name (the extractor names the node after this same key). */
|
|
55
|
+
endpointName: string;
|
|
56
|
+
/** Generated hook names this endpoint is ALSO referenceable by — `useLazy...` only applies to
|
|
57
|
+
* `.query(...)` endpoints (RTK Query never generates a lazy variant for a mutation). */
|
|
58
|
+
aliases: string[];
|
|
59
|
+
}
|
|
60
|
+
export declare function detectRtkEndpointAliases(filePath: string): RtkEndpointAlias[];
|
|
61
|
+
/**
|
|
62
|
+
* The candidate-enumeration counterpart to {@link detectRtkEndpointAliases}: produces full
|
|
63
|
+
* {@link ExtractionCandidate}s for each RTK endpoint, so {@link enumerateFileCandidates} doesn't
|
|
64
|
+
* silently skip them (see {@link findRtkEndpointMatches} for why the general declaration walk
|
|
65
|
+
* can't see them). Always `isExported:true` — an endpoint reached only through its generated hook
|
|
66
|
+
* is, for every practical purpose, part of the file's export surface; nothing about whether it
|
|
67
|
+
* "counts" as a node is a judgment call, so this feeds the SAME auto-accept path as a normal
|
|
68
|
+
* exported declaration.
|
|
69
|
+
*/
|
|
70
|
+
export declare function detectRtkEndpointNodes(filePath: string): ExtractionCandidate[];
|
|
44
71
|
/**
|
|
45
72
|
* Locally analyzes the source file and resolves references to candidate nodes
|
|
46
73
|
*/
|
|
@@ -49,6 +76,7 @@ export declare function resolveConnectionsLocally(sourceNodeId: string, sourceFi
|
|
|
49
76
|
name: string;
|
|
50
77
|
type: string;
|
|
51
78
|
file_path: string;
|
|
79
|
+
aliases?: string[];
|
|
52
80
|
}[], devmindPath: string, onMissing?: (rec: MissingRef) => void): string[];
|
|
53
81
|
/**
|
|
54
82
|
* Derive a node's identity/type/code directly from its declaration in a file — deterministic,
|
|
@@ -95,11 +123,72 @@ export interface TouchedSymbol {
|
|
|
95
123
|
type: string;
|
|
96
124
|
signature: string | null;
|
|
97
125
|
codeSnapshot: string;
|
|
126
|
+
/**
|
|
127
|
+
* The symbol's text before this edit, or null when there is nothing to compare against —
|
|
128
|
+
* a brand-new symbol, or a caller that passed no before-content. Feeds the diff/revert path.
|
|
129
|
+
*/
|
|
130
|
+
codeBefore: string | null;
|
|
98
131
|
startLine: number;
|
|
99
132
|
endLine: number;
|
|
100
133
|
/** True when no existing node covered this edit — a symbol that did not exist before. */
|
|
101
134
|
isNew: boolean;
|
|
102
135
|
}
|
|
136
|
+
/** One deterministically-enumerated candidate entity from {@link enumerateFileCandidates}. */
|
|
137
|
+
export interface ExtractionCandidate {
|
|
138
|
+
/** Graph node-id name — `Class.method` for a member, a bare name at top level. */
|
|
139
|
+
qualified: string;
|
|
140
|
+
name: string;
|
|
141
|
+
type: string;
|
|
142
|
+
signature: string | null;
|
|
143
|
+
codeSnapshot: string;
|
|
144
|
+
startLine: number;
|
|
145
|
+
endLine: number;
|
|
146
|
+
/** See {@link isNodeExported} — the auto-accept signal. */
|
|
147
|
+
isExported: boolean;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Enumerates every graph-eligible declaration in a file, deterministically — no LLM, so nothing
|
|
151
|
+
* gets silently missed the way the old whole-file-to-an-LLM extraction could. A named, EXPORTED
|
|
152
|
+
* declaration (`isExported:true`) is unambiguous: whether it "counts" as a node requires no
|
|
153
|
+
* judgment, so the caller can auto-accept it with zero LLM turns. Everything else — unexported
|
|
154
|
+
* helpers, anonymous default exports, tiny inline callbacks, object-literal factory members whose
|
|
155
|
+
* significance is genuinely a judgment call — is still enumerated (nothing is dropped silently)
|
|
156
|
+
* but flagged `isExported:false`, so an agentic curation pass only has to spend turns on THOSE.
|
|
157
|
+
*/
|
|
158
|
+
export declare function enumerateFileCandidates(filePath: string): ExtractionCandidate[];
|
|
159
|
+
/** One entry in {@link outlineFile}'s "what else is declared in this file" list. Deliberately
|
|
160
|
+
* lean — no `codeSnapshot` — see the function doc for why. */
|
|
161
|
+
export interface FileOutlineEntry {
|
|
162
|
+
qualified: string;
|
|
163
|
+
name: string;
|
|
164
|
+
type: string;
|
|
165
|
+
signature: string | null;
|
|
166
|
+
start_line: number;
|
|
167
|
+
end_line: number;
|
|
168
|
+
exported: boolean;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Every OTHER top-level declaration in a file — functions, classes, consts, types, interfaces —
|
|
172
|
+
* the "what else is here" a raw file read used to be the only way to answer. Reuses
|
|
173
|
+
* {@link enumerateFileCandidates}'s own declaration walk (`declarationsOverlapping` /
|
|
174
|
+
* `astBaseType` / `isNodeExported`), but deliberately does NOT call `node.getText()` per hit the
|
|
175
|
+
* way that function does: `enumerateFileCandidates` materializes each declaration's FULL source
|
|
176
|
+
* just to build a one-line signature and then discards it, which is an extra copy of potentially
|
|
177
|
+
* the whole file in string allocations — on every single `get_node_code` call, this is squarely
|
|
178
|
+
* the wrong trade. A short slice of the already-loaded full text plus a line split gets the same
|
|
179
|
+
* signature far cheaper. The parse itself is free either way: `getSourceFile` is mtime-cached
|
|
180
|
+
* ({@link getSourceFile}), and by the time `get_node_code` calls this the file has usually already
|
|
181
|
+
* been parsed once this same request via `getLiveCode`/`extractLiveCode`.
|
|
182
|
+
*/
|
|
183
|
+
export declare function outlineFile(filePath: string): FileOutlineEntry[];
|
|
184
|
+
/**
|
|
185
|
+
* A file's own import list — file-path entry point for {@link getFileImports} (which needs an
|
|
186
|
+
* already-parsed `ts.SourceFile`). Used by the Phase D curation agent's `get_file_imports` tool:
|
|
187
|
+
* whether an ambiguous candidate is worth keeping is often decided by whether it's ever imported
|
|
188
|
+
* anywhere, and the file's OWN imports are the cheap first signal (e.g. a candidate that mirrors
|
|
189
|
+
* a re-exported name, or that only makes sense alongside a specific imported dependency).
|
|
190
|
+
*/
|
|
191
|
+
export declare function listFileImports(filePath: string): ImportInfo[];
|
|
103
192
|
/**
|
|
104
193
|
* Work out which symbols an edit actually changed, from where the edit landed.
|
|
105
194
|
*
|
package/dist/utils/ast.js
CHANGED
|
@@ -38,9 +38,14 @@ exports.isAstParseable = isAstParseable;
|
|
|
38
38
|
exports.normalizeFsPath = normalizeFsPath;
|
|
39
39
|
exports.invalidateParsedFile = invalidateParsedFile;
|
|
40
40
|
exports.parseNodeId = parseNodeId;
|
|
41
|
+
exports.detectRtkEndpointAliases = detectRtkEndpointAliases;
|
|
42
|
+
exports.detectRtkEndpointNodes = detectRtkEndpointNodes;
|
|
41
43
|
exports.resolveConnectionsLocally = resolveConnectionsLocally;
|
|
42
44
|
exports.locateNodeInFile = locateNodeInFile;
|
|
43
45
|
exports.extractNodeFromFile = extractNodeFromFile;
|
|
46
|
+
exports.enumerateFileCandidates = enumerateFileCandidates;
|
|
47
|
+
exports.outlineFile = outlineFile;
|
|
48
|
+
exports.listFileImports = listFileImports;
|
|
44
49
|
exports.findTouchedSymbols = findTouchedSymbols;
|
|
45
50
|
const fs = __importStar(require("fs"));
|
|
46
51
|
const path = __importStar(require("path"));
|
|
@@ -190,6 +195,8 @@ function getFileImports(sourceFile) {
|
|
|
190
195
|
*/
|
|
191
196
|
function isDefinitionName(node) {
|
|
192
197
|
const parent = node.parent;
|
|
198
|
+
/* istanbul ignore if -- defensive: every node from a tree parsed with setParentNodes=true
|
|
199
|
+
(see parseText) has a parent except the SourceFile itself, which is never an Identifier. */
|
|
193
200
|
if (!parent)
|
|
194
201
|
return false;
|
|
195
202
|
// The declared name of a declaration (function foo, class Foo, const foo, param foo, foo() {} …)
|
|
@@ -409,6 +416,128 @@ function findRouteCall(sourceFile, method, arg) {
|
|
|
409
416
|
ts.forEachChild(sourceFile, visit);
|
|
410
417
|
return found;
|
|
411
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Walks a source file for RTK Query endpoint definitions. Endpoint keys live inside
|
|
421
|
+
* `endpoints: (builder) => ({ ... })` — an ARROW FUNCTION BODY — so they are structurally
|
|
422
|
+
* invisible to the general declaration walk ({@link declarationsOverlapping}/`findNodeInAst`
|
|
423
|
+
* both explicitly stop at function-body boundaries, since a local const or a helper defined
|
|
424
|
+
* inside a function body is never a graph entity). RTK endpoints are the one exception to that
|
|
425
|
+
* rule this codebase knows about, which is why they need their own dedicated walk rather than
|
|
426
|
+
* being reachable by the general "find a declaration by name" machinery.
|
|
427
|
+
*/
|
|
428
|
+
function findRtkEndpointMatches(sourceFile) {
|
|
429
|
+
const results = [];
|
|
430
|
+
function findEndpointsObject(fnBody) {
|
|
431
|
+
const unwrap = (e) => ts.isParenthesizedExpression(e) ? unwrap(e.expression) : e;
|
|
432
|
+
if (ts.isBlock(fnBody)) {
|
|
433
|
+
for (const stmt of fnBody.statements) {
|
|
434
|
+
if (ts.isReturnStatement(stmt) && stmt.expression) {
|
|
435
|
+
const expr = unwrap(stmt.expression);
|
|
436
|
+
if (ts.isObjectLiteralExpression(expr))
|
|
437
|
+
return expr;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
const expr = unwrap(fnBody);
|
|
443
|
+
return ts.isObjectLiteralExpression(expr) ? expr : null;
|
|
444
|
+
}
|
|
445
|
+
function visit(node) {
|
|
446
|
+
if (ts.isCallExpression(node) &&
|
|
447
|
+
ts.isIdentifier(node.expression) &&
|
|
448
|
+
(node.expression.text === 'createApi' || node.expression.text === 'injectEndpoints') &&
|
|
449
|
+
node.arguments.length > 0 &&
|
|
450
|
+
ts.isObjectLiteralExpression(node.arguments[0])) {
|
|
451
|
+
const configObj = node.arguments[0];
|
|
452
|
+
for (const prop of configObj.properties) {
|
|
453
|
+
if (!ts.isPropertyAssignment(prop) ||
|
|
454
|
+
!ts.isIdentifier(prop.name) ||
|
|
455
|
+
prop.name.text !== 'endpoints' ||
|
|
456
|
+
!(ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer)))
|
|
457
|
+
continue;
|
|
458
|
+
const endpointsObj = findEndpointsObject(prop.initializer.body);
|
|
459
|
+
if (!endpointsObj)
|
|
460
|
+
continue;
|
|
461
|
+
for (const epProp of endpointsObj.properties) {
|
|
462
|
+
if (!(ts.isPropertyAssignment(epProp) || ts.isMethodDeclaration(epProp)) ||
|
|
463
|
+
!(ts.isIdentifier(epProp.name) || ts.isStringLiteral(epProp.name)))
|
|
464
|
+
continue;
|
|
465
|
+
const epName = epProp.name.text;
|
|
466
|
+
// `.query(...)` / `.mutation(...)` may appear either as `key: builder.query(...)`
|
|
467
|
+
// (property assignment) or `key(builder) { return builder.query(...) }` (method) —
|
|
468
|
+
// only the property-assignment shape is RTK Query's documented form, so that's all we
|
|
469
|
+
// match; a method-shorthand endpoint falls through and simply isn't found.
|
|
470
|
+
if (!ts.isPropertyAssignment(epProp) || !ts.isCallExpression(epProp.initializer))
|
|
471
|
+
continue;
|
|
472
|
+
const callExpr = epProp.initializer;
|
|
473
|
+
if (!ts.isPropertyAccessExpression(callExpr.expression))
|
|
474
|
+
continue;
|
|
475
|
+
const method = callExpr.expression.name.text;
|
|
476
|
+
if (method !== 'query' && method !== 'mutation')
|
|
477
|
+
continue;
|
|
478
|
+
results.push({ propNode: epProp, epName, method });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
ts.forEachChild(node, visit);
|
|
483
|
+
}
|
|
484
|
+
visit(sourceFile);
|
|
485
|
+
return results;
|
|
486
|
+
}
|
|
487
|
+
function detectRtkEndpointAliases(filePath) {
|
|
488
|
+
if (!isAstParseable(filePath))
|
|
489
|
+
return [];
|
|
490
|
+
let sourceFile;
|
|
491
|
+
try {
|
|
492
|
+
sourceFile = getSourceFile(filePath);
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
const toPascal = (name) => (name.length ? name[0].toUpperCase() + name.slice(1) : name);
|
|
498
|
+
return findRtkEndpointMatches(sourceFile).map(({ epName, method }) => {
|
|
499
|
+
const pascal = toPascal(epName);
|
|
500
|
+
const aliases = method === 'query'
|
|
501
|
+
? [`use${pascal}Query`, `useLazy${pascal}Query`]
|
|
502
|
+
: [`use${pascal}Mutation`];
|
|
503
|
+
return { endpointName: epName, aliases };
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* The candidate-enumeration counterpart to {@link detectRtkEndpointAliases}: produces full
|
|
508
|
+
* {@link ExtractionCandidate}s for each RTK endpoint, so {@link enumerateFileCandidates} doesn't
|
|
509
|
+
* silently skip them (see {@link findRtkEndpointMatches} for why the general declaration walk
|
|
510
|
+
* can't see them). Always `isExported:true` — an endpoint reached only through its generated hook
|
|
511
|
+
* is, for every practical purpose, part of the file's export surface; nothing about whether it
|
|
512
|
+
* "counts" as a node is a judgment call, so this feeds the SAME auto-accept path as a normal
|
|
513
|
+
* exported declaration.
|
|
514
|
+
*/
|
|
515
|
+
function detectRtkEndpointNodes(filePath) {
|
|
516
|
+
if (!isAstParseable(filePath))
|
|
517
|
+
return [];
|
|
518
|
+
let sourceFile;
|
|
519
|
+
try {
|
|
520
|
+
sourceFile = getSourceFile(filePath);
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
return [];
|
|
524
|
+
}
|
|
525
|
+
return findRtkEndpointMatches(sourceFile).map(({ propNode, epName }) => {
|
|
526
|
+
const start = propNode.getStart(sourceFile);
|
|
527
|
+
const end = propNode.getEnd();
|
|
528
|
+
const code = propNode.getText(sourceFile);
|
|
529
|
+
return {
|
|
530
|
+
qualified: epName,
|
|
531
|
+
name: epName,
|
|
532
|
+
type: 'rtk_endpoint',
|
|
533
|
+
signature: code.split('\n')[0].slice(0, 200),
|
|
534
|
+
codeSnapshot: code,
|
|
535
|
+
startLine: sourceFile.getLineAndCharacterOfPosition(start).line + 1,
|
|
536
|
+
endLine: sourceFile.getLineAndCharacterOfPosition(end).line + 1,
|
|
537
|
+
isExported: true
|
|
538
|
+
};
|
|
539
|
+
});
|
|
540
|
+
}
|
|
412
541
|
/** Navigate a dotted path through nested object-literal properties. */
|
|
413
542
|
function navigateObjectPath(obj, segments) {
|
|
414
543
|
let current = obj;
|
|
@@ -428,6 +557,10 @@ function navigateObjectPath(obj, segments) {
|
|
|
428
557
|
current = null;
|
|
429
558
|
}
|
|
430
559
|
}
|
|
560
|
+
/* istanbul ignore next -- unreachable: every iteration of the loop above returns (either
|
|
561
|
+
`null` via a missing prop, or `prop` on the last segment), so control can only fall past
|
|
562
|
+
the loop when `segments` is empty. The sole caller (findInFrameworkContainer) already
|
|
563
|
+
guards `segments.length === 0` before ever calling this function. */
|
|
431
564
|
return null;
|
|
432
565
|
}
|
|
433
566
|
/**
|
|
@@ -438,6 +571,9 @@ function navigateObjectPath(obj, segments) {
|
|
|
438
571
|
* factory call's object-literal argument, so we isolate just that method.
|
|
439
572
|
*/
|
|
440
573
|
function findInFrameworkContainer(sourceFile, segments) {
|
|
574
|
+
/* istanbul ignore if -- defensive: the sole caller (findNodeInAst) only reaches here when
|
|
575
|
+
`symbolName.includes('.')`, so `segments` (symbolName.split('.').slice(1)) always has at
|
|
576
|
+
least one element. */
|
|
441
577
|
if (segments.length === 0)
|
|
442
578
|
return null;
|
|
443
579
|
const objArgs = [];
|
|
@@ -538,6 +674,10 @@ function findNodeInAst(sourceFile, className, symbolName) {
|
|
|
538
674
|
// Fallback: className resolved to a non-class declaration (object literal, factory call,
|
|
539
675
|
// etc). Search inside it for a property/method matching the member name, at any depth.
|
|
540
676
|
if (!foundNode && className && containerCandidate) {
|
|
677
|
+
/* istanbul ignore next -- defensive: every current caller sets `className` from a symbolName
|
|
678
|
+
that included a '.' in the first place (locateNodeInFile/codeOfSymbol split on 2 parts,
|
|
679
|
+
resolveConnectionsLocally's className comes from parseNodeId, which only sets it alongside
|
|
680
|
+
a dotted symbolName), so the ": symbolName" (no-dot) fallback is never exercised. */
|
|
541
681
|
const memberName = symbolName.includes('.') ? symbolName.split('.').pop() : symbolName;
|
|
542
682
|
foundNode = findPropertyInContainer(containerCandidate, memberName);
|
|
543
683
|
}
|
|
@@ -681,6 +821,9 @@ function resolveToExistingFile(basePaths) {
|
|
|
681
821
|
return cached;
|
|
682
822
|
let result = null;
|
|
683
823
|
outer: for (const base of basePaths) {
|
|
824
|
+
/* istanbul ignore if -- defensive: every entry basePaths can contain comes from
|
|
825
|
+
resolveImportToPaths, which only ever pushes path.resolve(...) results (always a
|
|
826
|
+
non-empty string) or pushes nothing at all — never a falsy element. */
|
|
684
827
|
if (!base)
|
|
685
828
|
continue;
|
|
686
829
|
for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
|
|
@@ -725,6 +868,9 @@ function getBarrelReexports(resolvedImportPath) {
|
|
|
725
868
|
barrelMissCache.add(resolvedImportPath);
|
|
726
869
|
return [];
|
|
727
870
|
}
|
|
871
|
+
/* istanbul ignore next -- defensive TOCTOU guard: `indexPath` was confirmed to exist one line
|
|
872
|
+
above via fs.existsSync; the `?? -1` only matters if the file is deleted in the instant
|
|
873
|
+
between that check and this statSync, a race not worth simulating in a unit test. */
|
|
728
874
|
const mtimeMs = statMtime(indexPath) ?? -1;
|
|
729
875
|
const cached = barrelCache.get(indexPath);
|
|
730
876
|
if (cached && cached.mtimeMs === mtimeMs)
|
|
@@ -764,11 +910,30 @@ function getBarrelReexports(resolvedImportPath) {
|
|
|
764
910
|
// import alias by (case-insensitive) node name,
|
|
765
911
|
// - null when there is no default export at all.
|
|
766
912
|
const ANON_DEFAULT = 'anon';
|
|
913
|
+
/**
|
|
914
|
+
* The name for a non-identifier `export default <expr>` (an ExportAssignment): a named class or
|
|
915
|
+
* function expression's own name, or the ANON_DEFAULT marker for anything else.
|
|
916
|
+
*/
|
|
917
|
+
function anonOrNamedExpressionDefaultName(expr) {
|
|
918
|
+
/* istanbul ignore next -- unreachable: `export default class Foo {}` / `export default
|
|
919
|
+
function foo() {}`, named or anonymous, always parse as a ClassDeclaration/FunctionDeclaration
|
|
920
|
+
(getDefaultExportName's other, `stmt.name ? ... : ANON_DEFAULT`, branch), never as this
|
|
921
|
+
ExportAssignment's `expression`. The only way a ClassExpression/FunctionExpression can reach
|
|
922
|
+
here at all is wrapped (`export default (class Foo {})`), and then `expr` is the
|
|
923
|
+
ParenthesizedExpression, not the class/function expression itself — so this never matches in
|
|
924
|
+
practice, verified directly against the TS parser. */
|
|
925
|
+
if ((ts.isClassExpression(expr) || ts.isFunctionExpression(expr)) && expr.name)
|
|
926
|
+
return expr.name.text;
|
|
927
|
+
return ANON_DEFAULT; // `export default Joi.object({...})`, `{...}`, `() => …`
|
|
928
|
+
}
|
|
767
929
|
const defaultExportCache = new Map();
|
|
768
930
|
function getDefaultExportName(filePath) {
|
|
769
931
|
const ext = path.extname(filePath).toLowerCase();
|
|
770
932
|
if (!['.ts', '.tsx', '.js', '.jsx'].includes(ext))
|
|
771
933
|
return null;
|
|
934
|
+
/* istanbul ignore next -- defensive TOCTOU guard: every caller passes a `targetNode.file_path`
|
|
935
|
+
it already has from a live candidate-node list, i.e. a file that exists; the `?? -1` only
|
|
936
|
+
matters if it's deleted in the instant before this statSync. */
|
|
772
937
|
const mtimeMs = statMtime(filePath) ?? -1;
|
|
773
938
|
const cached = defaultExportCache.get(filePath);
|
|
774
939
|
if (cached && cached.mtimeMs === mtimeMs)
|
|
@@ -780,12 +945,7 @@ function getDefaultExportName(filePath) {
|
|
|
780
945
|
// `export default <expr>`
|
|
781
946
|
if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
|
|
782
947
|
const expr = stmt.expression;
|
|
783
|
-
|
|
784
|
-
name = expr.text;
|
|
785
|
-
else if ((ts.isClassExpression(expr) || ts.isFunctionExpression(expr)) && expr.name)
|
|
786
|
-
name = expr.name.text;
|
|
787
|
-
else
|
|
788
|
-
name = ANON_DEFAULT; // `export default Joi.object({...})`, `{...}`, `() => …`
|
|
948
|
+
name = ts.isIdentifier(expr) ? expr.text : anonOrNamedExpressionDefaultName(expr);
|
|
789
949
|
break;
|
|
790
950
|
}
|
|
791
951
|
// `export default class X {}` / `export default function X() {}`
|
|
@@ -887,6 +1047,8 @@ function resolveConnectionsLocally(sourceNodeId, sourceFilePath, candidateNodes,
|
|
|
887
1047
|
const paths = resolveImportToPaths(imp.moduleSpecifier, sourceDir, repoRoot, tsPaths);
|
|
888
1048
|
const barrels = [];
|
|
889
1049
|
for (const p of paths) {
|
|
1050
|
+
/* istanbul ignore if -- defensive: see the matching guard in resolveToExistingFile —
|
|
1051
|
+
resolveImportToPaths never pushes a falsy path. */
|
|
890
1052
|
if (!p)
|
|
891
1053
|
continue;
|
|
892
1054
|
const rx = getBarrelReexports(p);
|
|
@@ -954,8 +1116,11 @@ function resolveConnectionsLocally(sourceNodeId, sourceFilePath, candidateNodes,
|
|
|
954
1116
|
connections.add(targetNode.id);
|
|
955
1117
|
continue;
|
|
956
1118
|
}
|
|
1119
|
+
// Check the target's own name AND any alias it's exported under (e.g. an RTK-generated
|
|
1120
|
+
// hook name for its endpoint) — one implementation, several referenceable handles.
|
|
957
1121
|
const nameToCheck = memberName || symbolName;
|
|
958
|
-
|
|
1122
|
+
const targetNames = [nameToCheck, ...(targetNode.aliases ?? [])];
|
|
1123
|
+
if (targetNames.some(n => referencedNames.has(n))) {
|
|
959
1124
|
connections.add(targetNode.id);
|
|
960
1125
|
}
|
|
961
1126
|
continue;
|
|
@@ -1038,8 +1203,13 @@ function resolveConnectionsLocally(sourceNodeId, sourceFilePath, candidateNodes,
|
|
|
1038
1203
|
// matching a `Utils.formatDate` method) — a pure false positive.
|
|
1039
1204
|
}
|
|
1040
1205
|
else {
|
|
1041
|
-
// Top-level function/variable imported & referenced
|
|
1042
|
-
|
|
1206
|
+
// Top-level function/variable imported & referenced — check the target's own declared
|
|
1207
|
+
// name AND any alias it's exported under. This is the RTK fix: a caller importing
|
|
1208
|
+
// `useGetAdminOrdersQuery` (generated by createApi, never a name that appears in the
|
|
1209
|
+
// endpoint's own declaration) still resolves to the `getAdminOrders` endpoint node, as
|
|
1210
|
+
// long as that alias was attached to it (see the RTK detector / Phase C).
|
|
1211
|
+
const targetNames = [symbolName, ...(targetNode.aliases ?? [])];
|
|
1212
|
+
if (targetNames.some(n => importedAsNames.includes(n) && referencedNames.has(n))) {
|
|
1043
1213
|
connections.add(targetNode.id);
|
|
1044
1214
|
continue;
|
|
1045
1215
|
}
|
|
@@ -1072,10 +1242,15 @@ function resolveConnectionsLocally(sourceNodeId, sourceFilePath, candidateNodes,
|
|
|
1072
1242
|
// default-imported and its alias is referenced here, and the file is small enough that
|
|
1073
1243
|
// its default export is unambiguous, link it. The node-count gate keeps this from
|
|
1074
1244
|
// re-exploding on large files (whose default export is virtually always named anyway).
|
|
1245
|
+
// `?? 99` is defensive only: `nodesPerFile` is built from `candidateNodes`, which always
|
|
1246
|
+
// includes `targetNode` itself, so its own file is always present with count >= 1; the
|
|
1247
|
+
// `?? 99` fallback can never actually be taken.
|
|
1248
|
+
/* istanbul ignore next */
|
|
1249
|
+
const targetFileNodeCount = nodesPerFile.get(normFile(targetNode.file_path)) ?? 99;
|
|
1075
1250
|
if (isImported &&
|
|
1076
1251
|
importedAsDefaultNames.some(alias => referencedNames.has(alias)) &&
|
|
1077
1252
|
getDefaultExportName(targetNode.file_path) === ANON_DEFAULT &&
|
|
1078
|
-
|
|
1253
|
+
targetFileNodeCount <= 3) {
|
|
1079
1254
|
connections.add(targetNode.id);
|
|
1080
1255
|
continue;
|
|
1081
1256
|
}
|
|
@@ -1202,6 +1377,9 @@ function declarationNameOf(node) {
|
|
|
1202
1377
|
// object's CONTENTS, not entities of their own: the graph models `Page.data`, never
|
|
1203
1378
|
// `Page.n`. Methods are exempt — a function nested at any depth is still a real symbol.
|
|
1204
1379
|
if (ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node)) {
|
|
1380
|
+
/* istanbul ignore next -- defensive: by grammar, a PropertyAssignment/
|
|
1381
|
+
ShorthandPropertyAssignment's parent is always its containing ObjectLiteralExpression, so
|
|
1382
|
+
the `: undefined` (not-an-object-literal-parent) side of this ternary is unreachable. */
|
|
1205
1383
|
const owner = node.parent && ts.isObjectLiteralExpression(node.parent) ? node.parent.parent : undefined;
|
|
1206
1384
|
const namedObject = owner && (ts.isCallExpression(owner) || ts.isVariableDeclaration(owner));
|
|
1207
1385
|
if (!namedObject)
|
|
@@ -1246,6 +1424,195 @@ function declarationsOverlapping(sf, ranges) {
|
|
|
1246
1424
|
ts.forEachChild(sf, visit);
|
|
1247
1425
|
return hits;
|
|
1248
1426
|
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Whether a declaration is part of the file's export surface — directly (`export function foo`,
|
|
1429
|
+
* `export const foo = ...`), via a separate `export { foo }` statement, or transitively (a method
|
|
1430
|
+
* of an exported class, a member of an object literal assigned to an exported variable). This is
|
|
1431
|
+
* the auto-accept signal for {@link enumerateFileCandidates}: an exported, named declaration is
|
|
1432
|
+
* unambiguously a real entity — nothing about its EXISTENCE is a judgment call, only an
|
|
1433
|
+
* unexported/anonymous one needs curation.
|
|
1434
|
+
*/
|
|
1435
|
+
function isNodeExported(node, sourceFile) {
|
|
1436
|
+
// Class/object-literal members inherit their container's export status — a method isn't
|
|
1437
|
+
// separately "exported", it's reachable (or not) through whatever names its class/object.
|
|
1438
|
+
if (ts.isMethodDeclaration(node) || ts.isPropertyDeclaration(node) || ts.isGetAccessorDeclaration(node) ||
|
|
1439
|
+
ts.isSetAccessorDeclaration(node) || ts.isConstructorDeclaration(node) ||
|
|
1440
|
+
ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node)) {
|
|
1441
|
+
if (node.parent && ts.isClassLike(node.parent))
|
|
1442
|
+
return isNodeExported(node.parent, sourceFile);
|
|
1443
|
+
for (let cur = node.parent; cur; cur = cur.parent) {
|
|
1444
|
+
if (ts.isVariableStatement(cur))
|
|
1445
|
+
return isNodeExported(cur, sourceFile);
|
|
1446
|
+
if (ts.isSourceFile(cur))
|
|
1447
|
+
break;
|
|
1448
|
+
}
|
|
1449
|
+
return false;
|
|
1450
|
+
}
|
|
1451
|
+
const hasExportModifier = (n) => !!n.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
1452
|
+
// `export const foo = ...`: the `export` modifier sits on the enclosing VariableStatement, not
|
|
1453
|
+
// on the VariableDeclaration itself.
|
|
1454
|
+
const modifierHost = ts.isVariableDeclaration(node) && node.parent && ts.isVariableDeclarationList(node.parent)
|
|
1455
|
+
? node.parent.parent
|
|
1456
|
+
: node;
|
|
1457
|
+
if (hasExportModifier(modifierHost))
|
|
1458
|
+
return true;
|
|
1459
|
+
// `export { foo, bar }` as its own statement elsewhere in the file, not a modifier at all.
|
|
1460
|
+
const declaredName = (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name)) ? node.name.text :
|
|
1461
|
+
(node.name && ts.isIdentifier(node.name))
|
|
1462
|
+
? node.name.text
|
|
1463
|
+
: null;
|
|
1464
|
+
if (!declaredName)
|
|
1465
|
+
return false;
|
|
1466
|
+
for (const stmt of sourceFile.statements) {
|
|
1467
|
+
if (ts.isExportDeclaration(stmt) && !stmt.moduleSpecifier &&
|
|
1468
|
+
stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
|
|
1469
|
+
if (stmt.exportClause.elements.some(el => el.name.text === declaredName || el.propertyName?.text === declaredName)) {
|
|
1470
|
+
return true;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Enumerates every graph-eligible declaration in a file, deterministically — no LLM, so nothing
|
|
1478
|
+
* gets silently missed the way the old whole-file-to-an-LLM extraction could. A named, EXPORTED
|
|
1479
|
+
* declaration (`isExported:true`) is unambiguous: whether it "counts" as a node requires no
|
|
1480
|
+
* judgment, so the caller can auto-accept it with zero LLM turns. Everything else — unexported
|
|
1481
|
+
* helpers, anonymous default exports, tiny inline callbacks, object-literal factory members whose
|
|
1482
|
+
* significance is genuinely a judgment call — is still enumerated (nothing is dropped silently)
|
|
1483
|
+
* but flagged `isExported:false`, so an agentic curation pass only has to spend turns on THOSE.
|
|
1484
|
+
*/
|
|
1485
|
+
function enumerateFileCandidates(filePath) {
|
|
1486
|
+
if (!isAstParseable(filePath))
|
|
1487
|
+
return [];
|
|
1488
|
+
let sourceFile;
|
|
1489
|
+
try {
|
|
1490
|
+
sourceFile = getSourceFile(filePath);
|
|
1491
|
+
}
|
|
1492
|
+
catch {
|
|
1493
|
+
return [];
|
|
1494
|
+
}
|
|
1495
|
+
const fullText = sourceFile.getFullText();
|
|
1496
|
+
const hits = declarationsOverlapping(sourceFile, [{ start: 0, end: fullText.length }]);
|
|
1497
|
+
// A single declaration can surface more than once under different matching rules (e.g. a
|
|
1498
|
+
// property assignment's own span, and — if it were itself a match — an initializer nested
|
|
1499
|
+
// inside it). Keep the FIRST hit per qualified name: `declarationsOverlapping` walks
|
|
1500
|
+
// parent-before-child, so the first hit is always the outermost, correct one.
|
|
1501
|
+
const byQualified = new Map();
|
|
1502
|
+
for (const h of hits) {
|
|
1503
|
+
if (!byQualified.has(h.qualified))
|
|
1504
|
+
byQualified.set(h.qualified, h);
|
|
1505
|
+
}
|
|
1506
|
+
const results = [];
|
|
1507
|
+
for (const h of byQualified.values()) {
|
|
1508
|
+
const code = h.node.getText(sourceFile);
|
|
1509
|
+
results.push({
|
|
1510
|
+
qualified: h.qualified,
|
|
1511
|
+
name: h.name,
|
|
1512
|
+
type: astBaseType(h.node),
|
|
1513
|
+
signature: code.split('\n')[0].slice(0, 200),
|
|
1514
|
+
codeSnapshot: code,
|
|
1515
|
+
startLine: sourceFile.getLineAndCharacterOfPosition(h.start).line + 1,
|
|
1516
|
+
endLine: sourceFile.getLineAndCharacterOfPosition(h.end).line + 1,
|
|
1517
|
+
isExported: isNodeExported(h.node, sourceFile)
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
// RTK Query endpoints live inside a function body (`endpoints: (builder) => ({...})`), so the
|
|
1521
|
+
// general declaration walk above structurally cannot see them — see `findRtkEndpointMatches`.
|
|
1522
|
+
// Folded in here (not called separately by every consumer) so `enumerateFileCandidates` really
|
|
1523
|
+
// is the complete candidate list for a file, RTK or not.
|
|
1524
|
+
for (const rtkNode of detectRtkEndpointNodes(filePath)) {
|
|
1525
|
+
if (!byQualified.has(rtkNode.qualified))
|
|
1526
|
+
results.push(rtkNode);
|
|
1527
|
+
}
|
|
1528
|
+
return results;
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Every OTHER top-level declaration in a file — functions, classes, consts, types, interfaces —
|
|
1532
|
+
* the "what else is here" a raw file read used to be the only way to answer. Reuses
|
|
1533
|
+
* {@link enumerateFileCandidates}'s own declaration walk (`declarationsOverlapping` /
|
|
1534
|
+
* `astBaseType` / `isNodeExported`), but deliberately does NOT call `node.getText()` per hit the
|
|
1535
|
+
* way that function does: `enumerateFileCandidates` materializes each declaration's FULL source
|
|
1536
|
+
* just to build a one-line signature and then discards it, which is an extra copy of potentially
|
|
1537
|
+
* the whole file in string allocations — on every single `get_node_code` call, this is squarely
|
|
1538
|
+
* the wrong trade. A short slice of the already-loaded full text plus a line split gets the same
|
|
1539
|
+
* signature far cheaper. The parse itself is free either way: `getSourceFile` is mtime-cached
|
|
1540
|
+
* ({@link getSourceFile}), and by the time `get_node_code` calls this the file has usually already
|
|
1541
|
+
* been parsed once this same request via `getLiveCode`/`extractLiveCode`.
|
|
1542
|
+
*/
|
|
1543
|
+
function outlineFile(filePath) {
|
|
1544
|
+
if (!isAstParseable(filePath))
|
|
1545
|
+
return [];
|
|
1546
|
+
let sourceFile;
|
|
1547
|
+
try {
|
|
1548
|
+
sourceFile = getSourceFile(filePath);
|
|
1549
|
+
}
|
|
1550
|
+
catch {
|
|
1551
|
+
return [];
|
|
1552
|
+
}
|
|
1553
|
+
const fullText = sourceFile.getFullText();
|
|
1554
|
+
const hits = declarationsOverlapping(sourceFile, [{ start: 0, end: fullText.length }]);
|
|
1555
|
+
// Same "keep the first (outermost) hit per qualified name" rule as enumerateFileCandidates —
|
|
1556
|
+
// declarationsOverlapping walks parent-before-child, so the first hit is always correct.
|
|
1557
|
+
const byQualified = new Map();
|
|
1558
|
+
for (const h of hits) {
|
|
1559
|
+
if (!byQualified.has(h.qualified))
|
|
1560
|
+
byQualified.set(h.qualified, h);
|
|
1561
|
+
}
|
|
1562
|
+
const results = [];
|
|
1563
|
+
for (const h of byQualified.values()) {
|
|
1564
|
+
/* istanbul ignore next -- the `|| null` arm is unreachable: `declarationsOverlapping` takes
|
|
1565
|
+
each hit's start from `node.getStart(sf)`, which skips leading trivia, so the slice always
|
|
1566
|
+
begins at a real token character and its first line is never the empty string. Kept as a
|
|
1567
|
+
type-level guarantee that `signature` is `string | null` rather than `string | undefined`. */
|
|
1568
|
+
const signature = fullText.slice(h.start, h.start + 200).split('\n')[0] || null;
|
|
1569
|
+
results.push({
|
|
1570
|
+
qualified: h.qualified,
|
|
1571
|
+
name: h.name,
|
|
1572
|
+
type: astBaseType(h.node),
|
|
1573
|
+
signature,
|
|
1574
|
+
start_line: sourceFile.getLineAndCharacterOfPosition(h.start).line + 1,
|
|
1575
|
+
end_line: sourceFile.getLineAndCharacterOfPosition(h.end).line + 1,
|
|
1576
|
+
exported: isNodeExported(h.node, sourceFile)
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
// RTK Query endpoints live inside a function body and the general walk above structurally
|
|
1580
|
+
// cannot see them (see detectRtkEndpointNodes) — folded in here so the outline is genuinely
|
|
1581
|
+
// complete, RTK or not, same as enumerateFileCandidates. Its own codeSnapshot is dropped; these
|
|
1582
|
+
// are individual endpoint bodies (small), not a whole-file cost, but the outline never carries
|
|
1583
|
+
// code regardless of source.
|
|
1584
|
+
for (const rtk of detectRtkEndpointNodes(filePath)) {
|
|
1585
|
+
if (byQualified.has(rtk.qualified))
|
|
1586
|
+
continue;
|
|
1587
|
+
results.push({
|
|
1588
|
+
qualified: rtk.qualified,
|
|
1589
|
+
name: rtk.name,
|
|
1590
|
+
type: rtk.type,
|
|
1591
|
+
signature: rtk.signature,
|
|
1592
|
+
start_line: rtk.startLine,
|
|
1593
|
+
end_line: rtk.endLine,
|
|
1594
|
+
exported: rtk.isExported
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
return results;
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* A file's own import list — file-path entry point for {@link getFileImports} (which needs an
|
|
1601
|
+
* already-parsed `ts.SourceFile`). Used by the Phase D curation agent's `get_file_imports` tool:
|
|
1602
|
+
* whether an ambiguous candidate is worth keeping is often decided by whether it's ever imported
|
|
1603
|
+
* anywhere, and the file's OWN imports are the cheap first signal (e.g. a candidate that mirrors
|
|
1604
|
+
* a re-exported name, or that only makes sense alongside a specific imported dependency).
|
|
1605
|
+
*/
|
|
1606
|
+
function listFileImports(filePath) {
|
|
1607
|
+
if (!isAstParseable(filePath))
|
|
1608
|
+
return [];
|
|
1609
|
+
try {
|
|
1610
|
+
return getFileImports(getSourceFile(filePath));
|
|
1611
|
+
}
|
|
1612
|
+
catch {
|
|
1613
|
+
return [];
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1249
1616
|
/** A symbol's current source in `sf`, or null when it isn't there. */
|
|
1250
1617
|
function codeOfSymbol(sf, symbolName) {
|
|
1251
1618
|
const parts = symbolName.split('.');
|
|
@@ -1317,6 +1684,7 @@ function findTouchedSymbols(filePath, ranges, knownSymbols = [], beforeContent)
|
|
|
1317
1684
|
type: astBaseType(h.node),
|
|
1318
1685
|
signature: newCode.split('\n')[0].slice(0, 200),
|
|
1319
1686
|
codeSnapshot: newCode,
|
|
1687
|
+
codeBefore: oldCode,
|
|
1320
1688
|
startLine: sf.getLineAndCharacterOfPosition(h.start).line + 1,
|
|
1321
1689
|
endLine: sf.getLineAndCharacterOfPosition(h.end).line + 1,
|
|
1322
1690
|
isNew: !known
|