brainclaw 1.24.0 → 1.26.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/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli/register-code-map.js +9 -2
- package/dist/commands/code-map.js +120 -6
- package/dist/commands/mcp-catalog.js +46 -0
- package/dist/commands/mcp.js +58 -6
- package/dist/commands/session-start.js +84 -13
- package/dist/core/bootstrap.js +28 -4
- package/dist/core/code-map/aggregate.js +36 -31
- package/dist/core/code-map/backend.js +162 -5
- package/dist/core/code-map/core.js +1 -0
- package/dist/core/code-map/export.js +212 -0
- package/dist/core/code-map/finalizer.js +57 -2
- package/dist/core/code-map/freshness.js +81 -15
- package/dist/core/code-map/impact.js +409 -0
- package/dist/core/code-map/indexes.js +64 -3
- package/dist/core/code-map/lang/python/index.js +4 -2
- package/dist/core/code-map/lang/query-runtime.js +2 -0
- package/dist/core/code-map/lang/typescript/config.js +271 -0
- package/dist/core/code-map/lang/typescript/index.js +24 -6
- package/dist/core/code-map/lang/usages.js +333 -0
- package/dist/core/code-map/memory-reader.js +15 -0
- package/dist/core/code-map/query.js +285 -71
- package/dist/core/code-map/refresh.js +0 -0
- package/dist/core/code-map/resolve.js +28 -2
- package/dist/core/code-map/store.js +1 -0
- package/dist/core/code-map/types.js +70 -9
- package/dist/core/code-map/vocabulary.js +6 -0
- package/dist/core/code-map/work-section.js +12 -14
- package/dist/core/context-diff.js +17 -3
- package/dist/core/entity-operations.js +14 -2
- package/dist/core/federation-pull.js +151 -3
- package/dist/core/federation-push.js +16 -3
- package/dist/core/hint-aging.js +4 -1
- package/dist/core/identity.js +69 -17
- package/dist/core/io.js +27 -0
- package/dist/core/project-discovery.js +7 -1
- package/dist/core/protocol-tool-policy.js +3 -0
- package/dist/core/runtime.js +23 -0
- package/dist/core/worktree.js +89 -2
- package/dist/facts.js +15 -12
- package/dist/facts.json +14 -11
- package/docs/cli.md +8 -0
- package/docs/code-map.md +60 -28
- package/docs/integrations/mcp.md +5 -2
- package/docs/mcp-schema-changelog.md +11 -1
- package/package.json +1 -1
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, explainable Code Map impact analysis.
|
|
3
|
+
*
|
|
4
|
+
* This module deliberately traverses only the persisted P1c resolution graph
|
|
5
|
+
* through P1d's reverse ResolutionIndex. It neither reparses files nor infers
|
|
6
|
+
* import edges from a naming convention. Naming is limited to a clearly marked,
|
|
7
|
+
* low-confidence test suggestion after resolved test imports have been reported.
|
|
8
|
+
*/
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileId } from './ids.js';
|
|
11
|
+
import { makeFreshnessBadge } from './freshness.js';
|
|
12
|
+
import { readManifest, readResolutionIndex, readShard, readSymbolsIndex, } from './store.js';
|
|
13
|
+
import { deriveBadge, isTestPath, makeLazyChecker, newAccumulator, validateStoreEntry, } from './query.js';
|
|
14
|
+
/** Direct results and each optional transitive layer are independently bounded. */
|
|
15
|
+
export const IMPACT_DEPENDENT_CAP = 100;
|
|
16
|
+
/** A depth of one is direct only; transitives require an explicit depth of two or more. */
|
|
17
|
+
export const IMPACT_MAX_DEPTH = 4;
|
|
18
|
+
export const IMPACT_NAMING_SUGGESTION_CONFIDENCE = 0.25;
|
|
19
|
+
function normalizeIdentifier(value) {
|
|
20
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
21
|
+
}
|
|
22
|
+
function looksLikePathTarget(target) {
|
|
23
|
+
return /[\\/]/.test(target) || /\.(?:[cm]?[jt]sx?|py|php|java|go|rs|cs|rb|c|cc|cpp|cxx|h|hpp)$/i.test(target);
|
|
24
|
+
}
|
|
25
|
+
function normalizePathTarget(target, projectRoot) {
|
|
26
|
+
const root = path.resolve(projectRoot);
|
|
27
|
+
const absolute = path.resolve(root, target);
|
|
28
|
+
const relative = path.relative(root, absolute);
|
|
29
|
+
if (!relative || relative === '.' || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return relative.replace(/\\/g, '/');
|
|
33
|
+
}
|
|
34
|
+
function entriesForToken(index, target) {
|
|
35
|
+
const normalizedTarget = normalizeIdentifier(target);
|
|
36
|
+
if (!normalizedTarget)
|
|
37
|
+
return [];
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
const candidates = [];
|
|
40
|
+
for (const entries of Object.values(index.entries)) {
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (seen.has(entry.node_id))
|
|
43
|
+
continue;
|
|
44
|
+
const normalizedName = normalizeIdentifier(entry.name);
|
|
45
|
+
if (normalizedName === normalizedTarget || normalizedName.includes(normalizedTarget)) {
|
|
46
|
+
seen.add(entry.node_id);
|
|
47
|
+
candidates.push(entry);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const exact = candidates.filter((entry) => normalizeIdentifier(entry.name) === normalizedTarget);
|
|
52
|
+
return (exact.length > 0 ? exact : candidates).sort((a, b) => a.path.localeCompare(b.path) || a.name.localeCompare(b.name) || a.node_id.localeCompare(b.node_id));
|
|
53
|
+
}
|
|
54
|
+
function entriesForPath(index, target) {
|
|
55
|
+
const normalizedTarget = target.replace(/\\/g, '/');
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
const entries = [];
|
|
58
|
+
for (const bucket of Object.values(index.entries)) {
|
|
59
|
+
for (const entry of bucket) {
|
|
60
|
+
const candidatePath = entry.path.replace(/\\/g, '/');
|
|
61
|
+
if ((candidatePath === normalizedTarget || candidatePath.endsWith(`/${normalizedTarget}`)) && !seen.has(entry.node_id)) {
|
|
62
|
+
seen.add(entry.node_id);
|
|
63
|
+
entries.push(entry);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return entries.sort((a, b) => a.path.localeCompare(b.path) || a.name.localeCompare(b.name));
|
|
68
|
+
}
|
|
69
|
+
function asDefinition(entry) {
|
|
70
|
+
return {
|
|
71
|
+
node_id: entry.node_id,
|
|
72
|
+
name: entry.name,
|
|
73
|
+
kind: 'symbol',
|
|
74
|
+
subtype: entry.subtype ?? null,
|
|
75
|
+
path: entry.path,
|
|
76
|
+
file_id: entry.file_id,
|
|
77
|
+
span: null,
|
|
78
|
+
confidence: entry.score_hint,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function fallbackReasons(entry, kind) {
|
|
82
|
+
const indexed = entry.reasons.filter((reason) => reason.kind === kind);
|
|
83
|
+
if (indexed.length > 0)
|
|
84
|
+
return indexed;
|
|
85
|
+
// Existing P1d indexes (written before P3) still have a compact aggregate.
|
|
86
|
+
// Preserve their factual resolution evidence rather than manufacturing a guess.
|
|
87
|
+
return [{
|
|
88
|
+
kind,
|
|
89
|
+
...(entry.module ? { module: entry.module } : {}),
|
|
90
|
+
imported: entry.imported,
|
|
91
|
+
...(typeof entry.confidence === 'number' ? { confidence: entry.confidence } : {}),
|
|
92
|
+
}];
|
|
93
|
+
}
|
|
94
|
+
function causeKey(cause) {
|
|
95
|
+
return [
|
|
96
|
+
cause.kind,
|
|
97
|
+
cause.module ?? '',
|
|
98
|
+
cause.imported.join('\u0000'),
|
|
99
|
+
String(cause.confidence ?? ''),
|
|
100
|
+
String(cause.source_line ?? ''),
|
|
101
|
+
cause.target.kind,
|
|
102
|
+
cause.target.path,
|
|
103
|
+
cause.target.node_id ?? '',
|
|
104
|
+
cause.caller?.node_id ?? '',
|
|
105
|
+
].join('\u0001');
|
|
106
|
+
}
|
|
107
|
+
function compareCause(a, b) {
|
|
108
|
+
return a.kind.localeCompare(b.kind)
|
|
109
|
+
|| a.target.path.localeCompare(b.target.path)
|
|
110
|
+
|| (a.target.node_id ?? '').localeCompare(b.target.node_id ?? '')
|
|
111
|
+
|| (a.caller?.node_id ?? '').localeCompare(b.caller?.node_id ?? '')
|
|
112
|
+
|| (a.module ?? '').localeCompare(b.module ?? '')
|
|
113
|
+
|| (a.source_line ?? -1) - (b.source_line ?? -1)
|
|
114
|
+
|| a.imported.join('\u0000').localeCompare(b.imported.join('\u0000'))
|
|
115
|
+
|| (a.confidence ?? -1) - (b.confidence ?? -1);
|
|
116
|
+
}
|
|
117
|
+
function addRelation(rows, entry, depth, target, kind) {
|
|
118
|
+
const current = rows.get(entry.path) ?? {
|
|
119
|
+
path: entry.path,
|
|
120
|
+
file_id: entry.file_id,
|
|
121
|
+
depth,
|
|
122
|
+
causes: [],
|
|
123
|
+
causeKeys: new Set(),
|
|
124
|
+
};
|
|
125
|
+
current.depth = Math.min(current.depth, depth);
|
|
126
|
+
for (const reason of fallbackReasons(entry, kind)) {
|
|
127
|
+
const cause = {
|
|
128
|
+
kind: reason.kind,
|
|
129
|
+
...(reason.module ? { module: reason.module } : {}),
|
|
130
|
+
imported: [...reason.imported],
|
|
131
|
+
...(typeof reason.confidence === 'number' ? { confidence: reason.confidence } : {}),
|
|
132
|
+
...(reason.source_line !== undefined ? { source_line: reason.source_line } : {}),
|
|
133
|
+
target,
|
|
134
|
+
};
|
|
135
|
+
const key = causeKey(cause);
|
|
136
|
+
if (!current.causeKeys.has(key)) {
|
|
137
|
+
current.causeKeys.add(key);
|
|
138
|
+
current.causes.push(cause);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
current.causes.sort(compareCause);
|
|
142
|
+
rows.set(entry.path, current);
|
|
143
|
+
}
|
|
144
|
+
function addUsageRelation(rows, entry, depth, target) {
|
|
145
|
+
const current = rows.get(entry.path) ?? {
|
|
146
|
+
path: entry.path,
|
|
147
|
+
file_id: entry.file_id,
|
|
148
|
+
depth,
|
|
149
|
+
causes: [],
|
|
150
|
+
causeKeys: new Set(),
|
|
151
|
+
};
|
|
152
|
+
current.depth = Math.min(current.depth, depth);
|
|
153
|
+
for (const reason of entry.reasons) {
|
|
154
|
+
const cause = {
|
|
155
|
+
kind: reason.kind,
|
|
156
|
+
imported: [],
|
|
157
|
+
confidence: reason.confidence,
|
|
158
|
+
...(reason.source_line !== undefined ? { source_line: reason.source_line } : {}),
|
|
159
|
+
caller: { node_id: reason.caller_node_id },
|
|
160
|
+
target,
|
|
161
|
+
};
|
|
162
|
+
const key = causeKey(cause);
|
|
163
|
+
if (!current.causeKeys.has(key)) {
|
|
164
|
+
current.causeKeys.add(key);
|
|
165
|
+
current.causes.push(cause);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
current.causes.sort(compareCause);
|
|
169
|
+
rows.set(entry.path, current);
|
|
170
|
+
}
|
|
171
|
+
function publicRelation(row) {
|
|
172
|
+
return {
|
|
173
|
+
path: row.path,
|
|
174
|
+
file_id: row.file_id,
|
|
175
|
+
depth: row.depth,
|
|
176
|
+
causes: row.causes,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function relationConfidence(row) {
|
|
180
|
+
return row.causes.reduce((best, cause) => Math.max(best, cause.confidence ?? 0), 0);
|
|
181
|
+
}
|
|
182
|
+
function testStem(filePath) {
|
|
183
|
+
const base = filePath.replace(/\\/g, '/').split('/').pop() ?? filePath;
|
|
184
|
+
return normalizeIdentifier(base
|
|
185
|
+
.replace(/\.[^.]+$/, '')
|
|
186
|
+
.replace(/(?:[._-](?:test|spec)|(?:test|spec)s?)$/i, '')
|
|
187
|
+
.replace(/^(?:test|spec)[_-]/i, ''));
|
|
188
|
+
}
|
|
189
|
+
function clampDepth(depth) {
|
|
190
|
+
if (depth === undefined || !Number.isFinite(depth))
|
|
191
|
+
return 1;
|
|
192
|
+
return Math.min(Math.max(Math.floor(depth), 1), IMPACT_MAX_DEPTH);
|
|
193
|
+
}
|
|
194
|
+
function clampLimit(limit) {
|
|
195
|
+
if (limit === undefined || !Number.isFinite(limit))
|
|
196
|
+
return IMPACT_DEPENDENT_CAP;
|
|
197
|
+
return Math.min(Math.max(Math.floor(limit), 0), IMPACT_DEPENDENT_CAP);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Read a bounded blast radius from existing resolved imports. A direct relation
|
|
201
|
+
* is distance 1. Supplying depth=2 (or more) opts into transitively importing
|
|
202
|
+
* files; depth is clamped to {@link IMPACT_MAX_DEPTH}.
|
|
203
|
+
*/
|
|
204
|
+
export function impact(target, options, ctx) {
|
|
205
|
+
const symbolsIndex = readSymbolsIndex(ctx.cwd, ctx.preferredDirName);
|
|
206
|
+
const manifest = readManifest(ctx.cwd, ctx.preferredDirName);
|
|
207
|
+
const maxDepth = clampDepth(options?.depth);
|
|
208
|
+
const limit = clampLimit(options?.limit);
|
|
209
|
+
const checker = makeLazyChecker();
|
|
210
|
+
const acc = newAccumulator();
|
|
211
|
+
const empty = (freshness) => ({
|
|
212
|
+
target,
|
|
213
|
+
definition: { match_kind: 'none', entries: [] },
|
|
214
|
+
direct_dependents: [],
|
|
215
|
+
transitive_dependents: [],
|
|
216
|
+
tests_for: [],
|
|
217
|
+
risk: {
|
|
218
|
+
score: 0,
|
|
219
|
+
formula: 'direct_dependents + transitive_dependents',
|
|
220
|
+
counters: {
|
|
221
|
+
definitions: 0,
|
|
222
|
+
direct_dependents: 0,
|
|
223
|
+
transitive_dependents: 0,
|
|
224
|
+
resolved_test_files: 0,
|
|
225
|
+
suggested_test_files: 0,
|
|
226
|
+
max_depth_returned: 0,
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
limits: {
|
|
230
|
+
max_depth: maxDepth,
|
|
231
|
+
max_dependents_per_section: limit,
|
|
232
|
+
direct_truncated: false,
|
|
233
|
+
transitive_truncated: false,
|
|
234
|
+
},
|
|
235
|
+
freshness_badge: freshness,
|
|
236
|
+
});
|
|
237
|
+
if (!symbolsIndex || !manifest) {
|
|
238
|
+
return empty(makeFreshnessBadge('missing_index', { extra: { hint: 'run refresh' } }));
|
|
239
|
+
}
|
|
240
|
+
let matchKind = 'none';
|
|
241
|
+
let rawDefinitions = [];
|
|
242
|
+
if (looksLikePathTarget(target)) {
|
|
243
|
+
const safePath = normalizePathTarget(target, manifest.project_root);
|
|
244
|
+
if (safePath) {
|
|
245
|
+
const symbols = entriesForPath(symbolsIndex, safePath);
|
|
246
|
+
rawDefinitions = symbols.map(asDefinition);
|
|
247
|
+
if (rawDefinitions.length > 0) {
|
|
248
|
+
matchKind = 'path';
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
const shard = readShard(fileId(manifest.project_id, safePath), ctx.cwd, ctx.preferredDirName);
|
|
252
|
+
const fileNode = shard?.nodes.find((node) => node.kind === 'file');
|
|
253
|
+
if (shard && fileNode) {
|
|
254
|
+
rawDefinitions = [{
|
|
255
|
+
node_id: fileNode.id,
|
|
256
|
+
name: fileNode.name,
|
|
257
|
+
kind: 'file',
|
|
258
|
+
subtype: null,
|
|
259
|
+
path: shard.path,
|
|
260
|
+
file_id: shard.file_id,
|
|
261
|
+
span: null,
|
|
262
|
+
confidence: fileNode.confidence,
|
|
263
|
+
}];
|
|
264
|
+
matchKind = 'path';
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
const symbols = entriesForToken(symbolsIndex, target);
|
|
271
|
+
rawDefinitions = symbols.map(asDefinition);
|
|
272
|
+
if (rawDefinitions.length > 0) {
|
|
273
|
+
matchKind = rawDefinitions.every((entry) => normalizeIdentifier(entry.name) === normalizeIdentifier(target)) ? 'exact' : 'fuzzy';
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const definitions = rawDefinitions
|
|
277
|
+
.filter((entry) => validateStoreEntry({ path: entry.path, file_id: entry.file_id }, checker, acc, ctx.cwd, ctx.preferredDirName))
|
|
278
|
+
.map((entry) => {
|
|
279
|
+
// SymbolIndexEntry intentionally stores only a ranking hint. The shard is
|
|
280
|
+
// the authoritative persisted source for the definition span/confidence.
|
|
281
|
+
const node = readShard(entry.file_id, ctx.cwd, ctx.preferredDirName)?.nodes.find((candidate) => candidate.id === entry.node_id);
|
|
282
|
+
return node ? { ...entry, span: node.span ?? null, confidence: node.confidence } : entry;
|
|
283
|
+
});
|
|
284
|
+
const definitionByNodeId = new Map(definitions.filter((entry) => entry.kind === 'symbol').map((entry) => [entry.node_id, entry]));
|
|
285
|
+
const definitionPaths = new Set(definitions.map((entry) => entry.path));
|
|
286
|
+
const resolution = readResolutionIndex(ctx.cwd, ctx.preferredDirName);
|
|
287
|
+
const directRows = new Map();
|
|
288
|
+
if (resolution) {
|
|
289
|
+
for (const definition of definitionByNodeId.values()) {
|
|
290
|
+
const target = { kind: 'symbol', path: definition.path, node_id: definition.node_id, name: definition.name };
|
|
291
|
+
for (const dependent of resolution.dependents_by_symbol[definition.node_id] ?? []) {
|
|
292
|
+
addRelation(directRows, dependent, 1, target, 'imports_symbol');
|
|
293
|
+
}
|
|
294
|
+
for (const dependent of resolution.usages_by_symbol[definition.node_id] ?? []) {
|
|
295
|
+
addUsageRelation(directRows, dependent, 1, target);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
for (const definitionPath of definitionPaths) {
|
|
299
|
+
for (const dependent of resolution.dependents_by_file[definitionPath] ?? []) {
|
|
300
|
+
addRelation(directRows, dependent, 1, { kind: 'file', path: definitionPath }, 'resolves_to');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const sortedDirect = [...directRows.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
305
|
+
const directCandidates = sortedDirect.slice(0, limit);
|
|
306
|
+
const direct = directCandidates
|
|
307
|
+
.filter((row) => validateStoreEntry(row, checker, acc, ctx.cwd, ctx.preferredDirName))
|
|
308
|
+
.map(publicRelation);
|
|
309
|
+
const transitive = [];
|
|
310
|
+
let transitiveTruncated = false;
|
|
311
|
+
if (resolution && maxDepth > 1 && limit > 0) {
|
|
312
|
+
const visited = new Set([...definitionPaths, ...direct.map((row) => row.path)]);
|
|
313
|
+
const queue = direct.map((row) => ({ path: row.path, depth: 1 }));
|
|
314
|
+
for (let offset = 0; offset < queue.length; offset++) {
|
|
315
|
+
const current = queue[offset];
|
|
316
|
+
if (current.depth >= maxDepth)
|
|
317
|
+
continue;
|
|
318
|
+
for (const dependent of resolution.dependents_by_file[current.path] ?? []) {
|
|
319
|
+
if (visited.has(dependent.path))
|
|
320
|
+
continue;
|
|
321
|
+
visited.add(dependent.path);
|
|
322
|
+
if (transitive.length >= limit) {
|
|
323
|
+
transitiveTruncated = true;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const rowMap = new Map();
|
|
327
|
+
addRelation(rowMap, dependent, current.depth + 1, { kind: 'file', path: current.path }, 'resolves_to');
|
|
328
|
+
const row = rowMap.get(dependent.path);
|
|
329
|
+
if (!validateStoreEntry(row, checker, acc, ctx.cwd, ctx.preferredDirName))
|
|
330
|
+
continue;
|
|
331
|
+
const output = publicRelation(row);
|
|
332
|
+
transitive.push(output);
|
|
333
|
+
queue.push({ path: output.path, depth: output.depth });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
transitive.sort((a, b) => a.depth - b.depth || a.path.localeCompare(b.path));
|
|
338
|
+
const confirmedTests = [...direct, ...transitive]
|
|
339
|
+
.filter((row) => isTestPath(row.path))
|
|
340
|
+
.map((row) => ({
|
|
341
|
+
path: row.path,
|
|
342
|
+
file_id: row.file_id,
|
|
343
|
+
relation: 'resolved_import',
|
|
344
|
+
confidence: relationConfidence(row),
|
|
345
|
+
depth: row.depth,
|
|
346
|
+
causes: row.causes,
|
|
347
|
+
reason: `resolved import at graph depth ${row.depth}`,
|
|
348
|
+
}));
|
|
349
|
+
const confirmedPaths = new Set(confirmedTests.map((test) => test.path));
|
|
350
|
+
const targetNames = new Set([
|
|
351
|
+
...definitions.map((definition) => normalizeIdentifier(definition.name)),
|
|
352
|
+
...definitions.map((definition) => testStem(definition.path)),
|
|
353
|
+
].filter(Boolean));
|
|
354
|
+
const suggestions = new Map();
|
|
355
|
+
if (targetNames.size > 0) {
|
|
356
|
+
const seenFiles = new Map();
|
|
357
|
+
for (const entries of Object.values(symbolsIndex.entries)) {
|
|
358
|
+
for (const entry of entries)
|
|
359
|
+
if (!seenFiles.has(entry.path))
|
|
360
|
+
seenFiles.set(entry.path, entry.file_id);
|
|
361
|
+
}
|
|
362
|
+
for (const [testPath, testFileId] of seenFiles) {
|
|
363
|
+
if (suggestions.size >= limit || confirmedPaths.has(testPath) || !isTestPath(testPath))
|
|
364
|
+
continue;
|
|
365
|
+
if (!targetNames.has(testStem(testPath)))
|
|
366
|
+
continue;
|
|
367
|
+
if (!validateStoreEntry({ path: testPath, file_id: testFileId }, checker, acc, ctx.cwd, ctx.preferredDirName))
|
|
368
|
+
continue;
|
|
369
|
+
suggestions.set(testPath, {
|
|
370
|
+
path: testPath,
|
|
371
|
+
file_id: testFileId,
|
|
372
|
+
relation: 'naming_convention_suggestion',
|
|
373
|
+
confidence: IMPACT_NAMING_SUGGESTION_CONFIDENCE,
|
|
374
|
+
reason: 'filename convention matches the target; no resolved import proves this relationship',
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const testsFor = [...confirmedTests, ...suggestions.values()].sort((a, b) => a.relation.localeCompare(b.relation) || a.path.localeCompare(b.path));
|
|
379
|
+
const maxDepthReturned = Math.max(0, ...direct.map((row) => row.depth), ...transitive.map((row) => row.depth));
|
|
380
|
+
const risk = {
|
|
381
|
+
score: direct.length + transitive.length,
|
|
382
|
+
formula: 'direct_dependents + transitive_dependents',
|
|
383
|
+
counters: {
|
|
384
|
+
definitions: definitions.length,
|
|
385
|
+
direct_dependents: direct.length,
|
|
386
|
+
transitive_dependents: transitive.length,
|
|
387
|
+
resolved_test_files: confirmedTests.length,
|
|
388
|
+
suggested_test_files: suggestions.size,
|
|
389
|
+
max_depth_returned: maxDepthReturned,
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
const freshnessBadge = deriveBadge(manifest.freshness.status, acc, checker.exhausted, definitions.length > 0 || direct.length > 0 || transitive.length > 0, definitions.length === 0);
|
|
393
|
+
return {
|
|
394
|
+
target,
|
|
395
|
+
definition: { match_kind: definitions.length > 0 ? matchKind : 'none', entries: definitions },
|
|
396
|
+
direct_dependents: direct,
|
|
397
|
+
transitive_dependents: transitive,
|
|
398
|
+
tests_for: testsFor,
|
|
399
|
+
risk,
|
|
400
|
+
limits: {
|
|
401
|
+
max_depth: maxDepth,
|
|
402
|
+
max_dependents_per_section: limit,
|
|
403
|
+
direct_truncated: sortedDirect.length > limit,
|
|
404
|
+
transitive_truncated: transitiveTruncated,
|
|
405
|
+
},
|
|
406
|
+
freshness_badge: freshnessBadge,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
//# sourceMappingURL=impact.js.map
|
|
@@ -130,7 +130,30 @@ export function buildResolutionIndex(projectId, shards) {
|
|
|
130
130
|
// target key -> (importer path -> merged entry)
|
|
131
131
|
const byFile = new Map();
|
|
132
132
|
const bySymbol = new Map();
|
|
133
|
-
const
|
|
133
|
+
const byUsageSymbol = new Map();
|
|
134
|
+
const addUsage = (targetSymbolId, importerPath, importerFileId, edge) => {
|
|
135
|
+
const perImporter = byUsageSymbol.get(targetSymbolId) ?? new Map();
|
|
136
|
+
const entry = perImporter.get(importerPath) ?? { path: importerPath, file_id: importerFileId, reasons: [] };
|
|
137
|
+
const reason = {
|
|
138
|
+
kind: edge.kind,
|
|
139
|
+
caller_node_id: edge.from,
|
|
140
|
+
confidence: edge.confidence,
|
|
141
|
+
...(edge.source?.line !== undefined ? { source_line: edge.source.line } : {}),
|
|
142
|
+
};
|
|
143
|
+
if (!entry.reasons.some((existing) => existing.kind === reason.kind
|
|
144
|
+
&& existing.caller_node_id === reason.caller_node_id
|
|
145
|
+
&& existing.confidence === reason.confidence
|
|
146
|
+
&& existing.source_line === reason.source_line)) {
|
|
147
|
+
entry.reasons.push(reason);
|
|
148
|
+
entry.reasons.sort((a, b) => a.kind.localeCompare(b.kind)
|
|
149
|
+
|| a.caller_node_id.localeCompare(b.caller_node_id)
|
|
150
|
+
|| (a.source_line ?? -1) - (b.source_line ?? -1)
|
|
151
|
+
|| a.confidence - b.confidence);
|
|
152
|
+
}
|
|
153
|
+
perImporter.set(importerPath, entry);
|
|
154
|
+
byUsageSymbol.set(targetSymbolId, perImporter);
|
|
155
|
+
};
|
|
156
|
+
const addDependent = (bucket, targetKey, importerPath, importerFileId, module, imported, confidence, kind, sourceLine) => {
|
|
134
157
|
const perImporter = bucket.get(targetKey) ?? new Map();
|
|
135
158
|
const prev = perImporter.get(importerPath);
|
|
136
159
|
if (!prev) {
|
|
@@ -140,6 +163,7 @@ export function buildResolutionIndex(projectId, shards) {
|
|
|
140
163
|
module,
|
|
141
164
|
imported: [...new Set(imported)].sort(),
|
|
142
165
|
confidence,
|
|
166
|
+
reasons: [],
|
|
143
167
|
});
|
|
144
168
|
}
|
|
145
169
|
else {
|
|
@@ -154,6 +178,29 @@ export function buildResolutionIndex(projectId, shards) {
|
|
|
154
178
|
prev.confidence = typeof prev.confidence === 'number' ? Math.max(prev.confidence, confidence) : confidence;
|
|
155
179
|
}
|
|
156
180
|
}
|
|
181
|
+
const current = perImporter.get(importerPath);
|
|
182
|
+
const reason = {
|
|
183
|
+
kind,
|
|
184
|
+
...(module ? { module } : {}),
|
|
185
|
+
imported: [...new Set(imported)].sort(),
|
|
186
|
+
...(typeof confidence === 'number' ? { confidence } : {}),
|
|
187
|
+
...(sourceLine !== undefined ? { source_line: sourceLine } : {}),
|
|
188
|
+
};
|
|
189
|
+
// Multiple module nodes can resolve to one target from one importer. Keep
|
|
190
|
+
// every concrete cause, deduping an identical extractor edge defensively.
|
|
191
|
+
if (!current.reasons.some((existing) => existing.kind === reason.kind
|
|
192
|
+
&& existing.module === reason.module
|
|
193
|
+
&& existing.confidence === reason.confidence
|
|
194
|
+
&& existing.source_line === reason.source_line
|
|
195
|
+
&& existing.imported.length === reason.imported.length
|
|
196
|
+
&& existing.imported.every((name, index) => name === reason.imported[index]))) {
|
|
197
|
+
current.reasons.push(reason);
|
|
198
|
+
current.reasons.sort((a, b) => a.kind.localeCompare(b.kind)
|
|
199
|
+
|| (a.module ?? '').localeCompare(b.module ?? '')
|
|
200
|
+
|| (a.source_line ?? -1) - (b.source_line ?? -1)
|
|
201
|
+
|| a.imported.join('\0').localeCompare(b.imported.join('\0'))
|
|
202
|
+
|| (a.confidence ?? -1) - (b.confidence ?? -1));
|
|
203
|
+
}
|
|
157
204
|
bucket.set(targetKey, perImporter);
|
|
158
205
|
};
|
|
159
206
|
const ordered = [...shards].sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -165,6 +212,12 @@ export function buildResolutionIndex(projectId, shards) {
|
|
|
165
212
|
moduleById.set(n.id, { name: n.name, imported: n.imported_names ?? [] });
|
|
166
213
|
}
|
|
167
214
|
for (const e of shard.edges) {
|
|
215
|
+
if (e.kind === 'calls' || e.kind === 'references') {
|
|
216
|
+
// `possible_textual_match` remains deliberately absent: it is a hint on
|
|
217
|
+
// the shard, never an impact dependency.
|
|
218
|
+
addUsage(e.to, shard.path, shard.file_id, { ...e, kind: e.kind });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
168
221
|
if (e.kind !== 'resolves_to' && e.kind !== 'imports_symbol')
|
|
169
222
|
continue;
|
|
170
223
|
const mod = moduleById.get(e.from);
|
|
@@ -172,10 +225,10 @@ export function buildResolutionIndex(projectId, shards) {
|
|
|
172
225
|
const targetPath = fileNodeIdToPath.get(e.to);
|
|
173
226
|
if (!targetPath)
|
|
174
227
|
continue; // target id not an indexed file (defensive)
|
|
175
|
-
addDependent(byFile, targetPath, shard.path, shard.file_id, mod?.name, mod?.imported ?? [], e.confidence);
|
|
228
|
+
addDependent(byFile, targetPath, shard.path, shard.file_id, mod?.name, mod?.imported ?? [], e.confidence, 'resolves_to', e.source?.line);
|
|
176
229
|
}
|
|
177
230
|
else {
|
|
178
|
-
addDependent(bySymbol, e.to, shard.path, shard.file_id, mod?.name, mod?.imported ?? [], e.confidence);
|
|
231
|
+
addDependent(bySymbol, e.to, shard.path, shard.file_id, mod?.name, mod?.imported ?? [], e.confidence, 'imports_symbol', e.source?.line);
|
|
179
232
|
}
|
|
180
233
|
}
|
|
181
234
|
}
|
|
@@ -186,12 +239,20 @@ export function buildResolutionIndex(projectId, shards) {
|
|
|
186
239
|
}
|
|
187
240
|
return out;
|
|
188
241
|
};
|
|
242
|
+
const finalizeUsages = (bucket) => {
|
|
243
|
+
const out = Object.create(null);
|
|
244
|
+
for (const key of [...bucket.keys()].sort()) {
|
|
245
|
+
out[key] = [...bucket.get(key).values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
};
|
|
189
249
|
return {
|
|
190
250
|
schema_version: CODE_MAP_SCHEMA_VERSION,
|
|
191
251
|
project_id: projectId,
|
|
192
252
|
updated_at: new Date().toISOString(),
|
|
193
253
|
dependents_by_file: finalize(byFile),
|
|
194
254
|
dependents_by_symbol: finalize(bySymbol),
|
|
255
|
+
usages_by_symbol: finalizeUsages(byUsageSymbol),
|
|
195
256
|
};
|
|
196
257
|
}
|
|
197
258
|
//# sourceMappingURL=indexes.js.map
|
|
@@ -30,6 +30,7 @@ import path from 'node:path';
|
|
|
30
30
|
import { fileURLToPath } from 'node:url';
|
|
31
31
|
import { loadGrammarWasm, grammarHashForWasm } from '../../wasm-loader.js';
|
|
32
32
|
import { extractWithQueries } from '../query-runtime.js';
|
|
33
|
+
import { extractLexicalUsages } from '../usages.js';
|
|
33
34
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
34
35
|
/** The python grammar .wasm: dist basename + node_modules devDep fallback spec. */
|
|
35
36
|
const PY_WASM_BASENAME = 'tree-sitter-python.wasm';
|
|
@@ -106,7 +107,7 @@ const queries = {
|
|
|
106
107
|
};
|
|
107
108
|
const vocabulary = {
|
|
108
109
|
nodeSubtypes: ['function', 'method', 'class', 'variable', 'constant', 'property'],
|
|
109
|
-
edgeKinds: ['contains', 'defines', 'imports'],
|
|
110
|
+
edgeKinds: ['contains', 'defines', 'imports', 'calls', 'references', 'possible_textual_match'],
|
|
110
111
|
captureMap: queries.captureMap,
|
|
111
112
|
};
|
|
112
113
|
const capabilities = {
|
|
@@ -284,7 +285,8 @@ export class PythonProvider {
|
|
|
284
285
|
}
|
|
285
286
|
return d;
|
|
286
287
|
});
|
|
287
|
-
|
|
288
|
+
const tree = draft.attributes?.__tree;
|
|
289
|
+
return { ...draft, definitions, usages: extractLexicalUsages(tree?.rootNode, definitions, 'python') };
|
|
288
290
|
}
|
|
289
291
|
/**
|
|
290
292
|
* P1c file-level import resolution (T3). Returns at most one resolution per import:
|
|
@@ -121,6 +121,7 @@ export async function extractWithQueries(input) {
|
|
|
121
121
|
imports: [],
|
|
122
122
|
exports: [],
|
|
123
123
|
tests: [],
|
|
124
|
+
usages: [],
|
|
124
125
|
facts,
|
|
125
126
|
attributes: { parseStatus, __tree: tree },
|
|
126
127
|
});
|
|
@@ -367,6 +368,7 @@ export async function extractWithQueries(input) {
|
|
|
367
368
|
imports,
|
|
368
369
|
exports,
|
|
369
370
|
tests: [],
|
|
371
|
+
usages: [],
|
|
370
372
|
facts,
|
|
371
373
|
attributes: { parseStatus, __tree: tree },
|
|
372
374
|
};
|