@wrongstack/tools 1.0.10 → 1.0.12
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/builtin.js +17 -17
- package/dist/{chunk-6I6RXQFG.js → chunk-2BCRQAIW.js} +9 -9
- package/dist/{chunk-XRNN44QY.js → chunk-2IRHFHZY.js} +235 -71
- package/dist/{chunk-5RBUYFRV.js → chunk-4IMNVQRB.js} +8 -2
- package/dist/{chunk-NEYUPZL6.js → chunk-6D3UIQMF.js} +651 -303
- package/dist/chunk-BJ5LXLKE.js +351 -0
- package/dist/{chunk-C6ZOXNXV.js → chunk-CSS42GRP.js} +2 -2
- package/dist/{chunk-SKRMUHTU.js → chunk-HSLDY55L.js} +41 -8
- package/dist/{chunk-ENE22I5W.js → chunk-K37AKCTN.js} +884 -362
- package/dist/{chunk-V25Q6TQ7.js → chunk-K3VX6V5P.js} +3 -3
- package/dist/chunk-OIFY4XNP.js +855 -0
- package/dist/{chunk-OUERRFEM.js → chunk-OMQDKZOT.js} +17 -8
- package/dist/{chunk-7GOE2ZTZ.js → chunk-OMWC2OGT.js} +2 -2
- package/dist/{chunk-IE4KDAOB.js → chunk-OYQE2UNT.js} +3 -3
- package/dist/chunk-R3HR3QI3.js +97 -0
- package/dist/{chunk-XPVYZ2S6.js → chunk-RYLMSGR5.js} +2 -2
- package/dist/{chunk-HYHE4VNA.js → chunk-UMOP3VGK.js} +79 -45
- package/dist/{chunk-FVSMTEO3.js → chunk-VMNWQI4V.js} +2 -2
- package/dist/{chunk-2UVAIS5O.js → chunk-YPSVPCIE.js} +2 -2
- package/dist/{chunk-PWZUHZP7.js → chunk-Z77XMU3U.js} +2 -2
- package/dist/codebase-index/ast-invariant-engine.d.ts +19 -2
- package/dist/codebase-index/atlas-projection.d.ts +19 -0
- package/dist/codebase-index/background-indexer.d.ts +14 -0
- package/dist/codebase-index/circuit-breaker.d.ts +7 -0
- package/dist/codebase-index/codebase-ast-replace-tool.d.ts +1 -1
- package/dist/codebase-index/codebase-impact-analysis-tool.d.ts +6 -0
- package/dist/codebase-index/codebase-invariant-check-tool.d.ts +2 -0
- package/dist/codebase-index/codebase-targeted-test-tool.d.ts +26 -0
- package/dist/codebase-index/dead-code-scan.d.ts +4 -0
- package/dist/codebase-index/go-parser.d.ts +23 -3
- package/dist/codebase-index/graph-rank-pass.d.ts +10 -0
- package/dist/codebase-index/graph-rank.d.ts +8 -0
- package/dist/codebase-index/index.d.ts +1 -1
- package/dist/codebase-index/index.js +9 -9
- package/dist/codebase-index/lsp-kind.d.ts +3 -1
- package/dist/codebase-index/module-resolver.d.ts +21 -1
- package/dist/codebase-index/parser-batch.d.ts +6 -8
- package/dist/codebase-index/parser-output.d.ts +5 -1
- package/dist/codebase-index/parser-worker-script.js +4 -4
- package/dist/codebase-index/project-server-framing.d.ts +1 -17
- package/dist/codebase-index/project-server-watcher.d.ts +6 -0
- package/dist/codebase-index/project-server.js +72 -26
- package/dist/codebase-index/py-parser.d.ts +4 -4
- package/dist/codebase-index/rs-parser.d.ts +7 -0
- package/dist/codebase-index/toolchain-scripts.d.ts +64 -0
- package/dist/codebase-index/tree-sitter/queries.d.ts +14 -0
- package/dist/codebase-index/tree-sitter/util.d.ts +6 -1
- package/dist/codebase-index/tree-sitter-parser.d.ts +7 -0
- package/dist/codebase-index/worker.js +23 -16
- package/dist/codebase-index/writer-refs.d.ts +17 -2
- package/dist/codebase-index/writer-search-helpers.d.ts +9 -0
- package/dist/codebase-index/writer.d.ts +20 -1
- package/dist/edit.js +8 -8
- package/dist/{generic-parser-N2PLGBC2.js → generic-parser-IVWLFKMR.js} +2 -2
- package/dist/go-parser-LWV3L5V7.js +197 -0
- package/dist/index.js +19 -19
- package/dist/json-parser-PERV2JHX.js +204 -0
- package/dist/pack.js +18 -18
- package/dist/parser-dispatch-ZSWCQB6M.js +14 -0
- package/dist/patch.js +8 -8
- package/dist/{py-parser-4PFZYAJ7.js → py-parser-B6D6RGH7.js} +4 -4
- package/dist/read.js +8 -8
- package/dist/replace.js +8 -8
- package/dist/rs-parser-L7SP4GWJ.js +182 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +1 -1
- package/dist/tool-tier.js +19 -19
- package/dist/{tree-sitter-parser-GILYQLS4.js → tree-sitter-parser-KU6JF3ZC.js} +6 -4
- package/dist/{ts-parser-7W26YB26.js → ts-parser-PML6QIH7.js} +55 -23
- package/dist/typecheck.js +1 -1
- package/dist/write.js +8 -8
- package/dist/{yaml-parser-DFBC5FWH.js → yaml-parser-7BJN544B.js} +41 -6
- package/package.json +5 -5
- package/dist/chunk-4CE3RKEP.js +0 -390
- package/dist/chunk-KHUCKLOR.js +0 -899
- package/dist/chunk-ZKC5UNGD.js +0 -167
- package/dist/go-parser-Z4H6SR3R.js +0 -474
- package/dist/json-parser-IJD77VL4.js +0 -256
- package/dist/parser-dispatch-WG4DNBHH.js +0 -14
- package/dist/rs-parser-GIWPUA3T.js +0 -81
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
parseTreeSitterAst
|
|
3
|
-
|
|
2
|
+
parseTreeSitterAst,
|
|
3
|
+
treeSitterDeclarationName
|
|
4
|
+
} from "./chunk-2IRHFHZY.js";
|
|
4
5
|
import {
|
|
5
6
|
spawnStream
|
|
6
7
|
} from "./chunk-UVYRQKPZ.js";
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
outgoingCallsService,
|
|
16
17
|
runStartupIndex,
|
|
17
18
|
searchCodebaseIndex
|
|
18
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-UMOP3VGK.js";
|
|
19
20
|
import {
|
|
20
21
|
IndexTimeoutError,
|
|
21
22
|
SCHEMA_VERSION,
|
|
@@ -26,7 +27,7 @@ import {
|
|
|
26
27
|
posixIndexPath,
|
|
27
28
|
resolveIndexDir,
|
|
28
29
|
xxhash64String
|
|
29
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-6D3UIQMF.js";
|
|
30
31
|
import {
|
|
31
32
|
EXT_TO_LANG,
|
|
32
33
|
detectLang,
|
|
@@ -49,6 +50,7 @@ async function loadTs() {
|
|
|
49
50
|
}
|
|
50
51
|
return tsModule;
|
|
51
52
|
}
|
|
53
|
+
var TREE_SITTER_RULE_LANGS = /* @__PURE__ */ new Set(["py", "go", "rs"]);
|
|
52
54
|
var PolyglotInvariantEngine = class {
|
|
53
55
|
/**
|
|
54
56
|
* Evaluate whether a code mutation respects backward compatibility invariants.
|
|
@@ -165,6 +167,8 @@ var PolyglotInvariantEngine = class {
|
|
|
165
167
|
return {
|
|
166
168
|
valid: violations.length === 0,
|
|
167
169
|
violations,
|
|
170
|
+
lang,
|
|
171
|
+
supported: origContract.supported && modContract.supported,
|
|
168
172
|
stats: {
|
|
169
173
|
originalSymbols: origContract.functions.size + origContract.interfaces.size,
|
|
170
174
|
modifiedSymbols: modContract.functions.size + modContract.interfaces.size,
|
|
@@ -200,137 +204,206 @@ var PolyglotInvariantEngine = class {
|
|
|
200
204
|
const exports = /* @__PURE__ */ new Set();
|
|
201
205
|
const functions = /* @__PURE__ */ new Map();
|
|
202
206
|
const interfaces = /* @__PURE__ */ new Map();
|
|
207
|
+
function hasModifier(node, kind) {
|
|
208
|
+
if (!ts.canHaveModifiers(node)) return false;
|
|
209
|
+
return ts.getModifiers(node)?.some((m) => m.kind === kind) ?? false;
|
|
210
|
+
}
|
|
203
211
|
function isExportedNode(node) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
212
|
+
return hasModifier(node, ts.SyntaxKind.ExportKeyword);
|
|
213
|
+
}
|
|
214
|
+
function recordExport(node, name) {
|
|
215
|
+
if (!isExportedNode(node)) return false;
|
|
216
|
+
if (hasModifier(node, ts.SyntaxKind.DefaultKeyword)) exports.add("default");
|
|
217
|
+
else if (name) exports.add(name);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
function paramsOf(params) {
|
|
221
|
+
return params.map((p) => ({
|
|
222
|
+
name: p.name.getText(sourceFile),
|
|
223
|
+
isOptional: Boolean(p.questionToken),
|
|
224
|
+
hasDefault: Boolean(p.initializer),
|
|
225
|
+
isRest: Boolean(p.dotDotDotToken),
|
|
226
|
+
type: p.type ? p.type.getText(sourceFile) : void 0
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
function bindingNames(name) {
|
|
230
|
+
if (ts.isIdentifier(name)) return [name.text];
|
|
231
|
+
const names = [];
|
|
232
|
+
for (const element of name.elements) {
|
|
233
|
+
if (!ts.isOmittedExpression(element)) names.push(...bindingNames(element.name));
|
|
209
234
|
}
|
|
210
|
-
return
|
|
235
|
+
return names;
|
|
236
|
+
}
|
|
237
|
+
function isFunctionLike(node) {
|
|
238
|
+
return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node);
|
|
211
239
|
}
|
|
240
|
+
let functionDepth = 0;
|
|
212
241
|
function visit(node) {
|
|
213
|
-
if (ts.isExportDeclaration(node)
|
|
214
|
-
|
|
215
|
-
|
|
242
|
+
if (ts.isExportDeclaration(node)) {
|
|
243
|
+
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
244
|
+
for (const element of node.exportClause.elements) {
|
|
245
|
+
exports.add(element.name.text);
|
|
246
|
+
}
|
|
247
|
+
} else if (node.exportClause && ts.isNamespaceExport(node.exportClause)) {
|
|
248
|
+
exports.add(node.exportClause.name.text);
|
|
249
|
+
} else if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
250
|
+
exports.add(`* from '${node.moduleSpecifier.text}'`);
|
|
216
251
|
}
|
|
217
252
|
}
|
|
218
|
-
if (ts.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
if (ts.isClassDeclaration(node) && node.name) {
|
|
234
|
-
const className = node.name.text;
|
|
235
|
-
const isExp = isExportedNode(node);
|
|
236
|
-
if (isExp) exports.add(className);
|
|
237
|
-
for (const member of node.members) {
|
|
238
|
-
if (ts.isMethodDeclaration(member) && member.name && ts.isIdentifier(member.name)) {
|
|
239
|
-
const methodName = member.name.text;
|
|
240
|
-
const fullMethodName = `${className}.${methodName}`;
|
|
241
|
-
const params = member.parameters.map((p) => {
|
|
242
|
-
const pName = p.name.getText(sourceFile);
|
|
243
|
-
const isOptional = Boolean(p.questionToken);
|
|
244
|
-
const hasDefault = Boolean(p.initializer);
|
|
245
|
-
const isRest = Boolean(p.dotDotDotToken);
|
|
246
|
-
const type = p.type ? p.type.getText(sourceFile) : void 0;
|
|
247
|
-
return { name: pName, isOptional, hasDefault, isRest, type };
|
|
253
|
+
if (ts.isExportAssignment(node)) {
|
|
254
|
+
exports.add(node.isExportEquals ? "export =" : "default");
|
|
255
|
+
}
|
|
256
|
+
if (functionDepth === 0) {
|
|
257
|
+
if (ts.isFunctionDeclaration(node)) {
|
|
258
|
+
const name = node.name?.text;
|
|
259
|
+
const isExp = recordExport(node, name);
|
|
260
|
+
const key = name ?? (isExp ? "default" : void 0);
|
|
261
|
+
if (key) {
|
|
262
|
+
functions.set(key, {
|
|
263
|
+
name: key,
|
|
264
|
+
isExported: isExp,
|
|
265
|
+
params: paramsOf(node.parameters),
|
|
266
|
+
returnType: node.type ? node.type.getText(sourceFile) : void 0
|
|
248
267
|
});
|
|
249
|
-
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (ts.isVariableStatement(node)) {
|
|
271
|
+
const isExp = isExportedNode(node);
|
|
272
|
+
for (const decl of node.declarationList.declarations) {
|
|
273
|
+
const names = bindingNames(decl.name);
|
|
274
|
+
if (isExp) for (const n of names) exports.add(n);
|
|
275
|
+
const init = decl.initializer;
|
|
276
|
+
if (ts.isIdentifier(decl.name) && init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) {
|
|
277
|
+
const name = decl.name.text;
|
|
278
|
+
functions.set(name, {
|
|
279
|
+
name,
|
|
280
|
+
isExported: isExp,
|
|
281
|
+
params: paramsOf(init.parameters),
|
|
282
|
+
returnType: init.type ? init.type.getText(sourceFile) : void 0
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (ts.isClassDeclaration(node)) {
|
|
288
|
+
const className = node.name?.text ?? "default";
|
|
289
|
+
const isExp = recordExport(node, node.name?.text);
|
|
290
|
+
for (const member of node.members) {
|
|
291
|
+
let memberName;
|
|
292
|
+
let params;
|
|
293
|
+
let returnType;
|
|
294
|
+
if (ts.isConstructorDeclaration(member)) {
|
|
295
|
+
memberName = "constructor";
|
|
296
|
+
params = member.parameters;
|
|
297
|
+
} else if (ts.isMethodDeclaration(member) && (ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name))) {
|
|
298
|
+
memberName = member.name.text;
|
|
299
|
+
params = member.parameters;
|
|
300
|
+
returnType = member.type ? member.type.getText(sourceFile) : void 0;
|
|
301
|
+
}
|
|
302
|
+
if (memberName === void 0 || params === void 0) continue;
|
|
303
|
+
const fullMethodName = `${className}.${memberName}`;
|
|
304
|
+
const existing = functions.get(fullMethodName);
|
|
305
|
+
if (existing && !("body" in member && member.body)) continue;
|
|
250
306
|
functions.set(fullMethodName, {
|
|
251
307
|
name: fullMethodName,
|
|
252
308
|
isExported: isExp,
|
|
253
|
-
params,
|
|
309
|
+
params: paramsOf(params),
|
|
254
310
|
returnType
|
|
255
311
|
});
|
|
256
312
|
}
|
|
257
313
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
314
|
+
if (ts.isEnumDeclaration(node)) {
|
|
315
|
+
recordExport(node, node.name.text);
|
|
316
|
+
}
|
|
317
|
+
if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
318
|
+
recordExport(node, node.name.text);
|
|
319
|
+
}
|
|
320
|
+
if (ts.isInterfaceDeclaration(node)) {
|
|
321
|
+
const name = node.name.text;
|
|
322
|
+
const isExp = recordExport(node, name);
|
|
323
|
+
const properties = [];
|
|
324
|
+
const methods = [];
|
|
325
|
+
for (const member of node.members) {
|
|
326
|
+
if (ts.isPropertySignature(member) && member.name) {
|
|
327
|
+
const propName = member.name.getText(sourceFile);
|
|
328
|
+
const isOptional = Boolean(member.questionToken);
|
|
329
|
+
const type = member.type ? member.type.getText(sourceFile) : void 0;
|
|
330
|
+
properties.push({ name: propName, isOptional, type });
|
|
331
|
+
} else if (ts.isMethodSignature(member) && member.name) {
|
|
332
|
+
const methodName = member.name.getText(sourceFile);
|
|
333
|
+
const isOptional = Boolean(member.questionToken);
|
|
334
|
+
const params = member.parameters.map((p) => ({
|
|
335
|
+
name: p.name.getText(sourceFile),
|
|
336
|
+
isOptional: Boolean(p.questionToken),
|
|
337
|
+
hasDefault: Boolean(p.initializer),
|
|
338
|
+
isRest: Boolean(p.dotDotDotToken)
|
|
339
|
+
}));
|
|
340
|
+
const returnType = member.type ? member.type.getText(sourceFile) : void 0;
|
|
341
|
+
methods.push({ name: methodName, isOptional, params, returnType });
|
|
342
|
+
}
|
|
282
343
|
}
|
|
344
|
+
interfaces.set(name, { name, isExported: isExp, properties, methods });
|
|
283
345
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
346
|
+
if (ts.isTypeAliasDeclaration(node)) {
|
|
347
|
+
const name = node.name.text;
|
|
348
|
+
const isExp = recordExport(node, name);
|
|
349
|
+
if (ts.isTypeLiteralNode(node.type)) {
|
|
350
|
+
const properties = [];
|
|
351
|
+
for (const member of node.type.members) {
|
|
352
|
+
if (ts.isPropertySignature(member) && member.name) {
|
|
353
|
+
const propName = member.name.getText(sourceFile);
|
|
354
|
+
const isOptional = Boolean(member.questionToken);
|
|
355
|
+
const type = member.type ? member.type.getText(sourceFile) : void 0;
|
|
356
|
+
properties.push({ name: propName, isOptional, type });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
interfaces.set(name, { name, isExported: isExp, properties, methods: [] });
|
|
297
360
|
}
|
|
298
361
|
}
|
|
299
|
-
interfaces.set(name, { name, isExported: isExp, properties, methods: [] });
|
|
300
362
|
}
|
|
363
|
+
const fn = isFunctionLike(node);
|
|
364
|
+
if (fn) functionDepth++;
|
|
301
365
|
ts.forEachChild(node, visit);
|
|
366
|
+
if (fn) functionDepth--;
|
|
302
367
|
}
|
|
303
368
|
ts.forEachChild(sourceFile, visit);
|
|
304
|
-
return { lang, exports, functions, interfaces };
|
|
369
|
+
return { lang, exports, functions, interfaces, supported: true };
|
|
305
370
|
}
|
|
306
371
|
/**
|
|
307
|
-
* Polyglot extraction (Python, Go, Rust
|
|
372
|
+
* Polyglot extraction (Python, Go, Rust) via Tree-sitter WASM.
|
|
308
373
|
*/
|
|
309
374
|
async extractTreeSitterContract(code, lang) {
|
|
310
375
|
const exports = /* @__PURE__ */ new Set();
|
|
311
376
|
const functions = /* @__PURE__ */ new Map();
|
|
312
377
|
const interfaces = /* @__PURE__ */ new Map();
|
|
378
|
+
if (!TREE_SITTER_RULE_LANGS.has(lang)) {
|
|
379
|
+
return { lang, exports, functions, interfaces, supported: false };
|
|
380
|
+
}
|
|
313
381
|
const parsed = await parseTreeSitterAst({ content: code, lang });
|
|
314
382
|
if (!parsed) {
|
|
315
|
-
return { lang, exports, functions, interfaces };
|
|
383
|
+
return { lang, exports, functions, interfaces, supported: false };
|
|
316
384
|
}
|
|
317
385
|
const { tree, parser } = parsed;
|
|
318
386
|
try {
|
|
319
387
|
let walk2 = function(node) {
|
|
320
388
|
const type = node.type;
|
|
389
|
+
let pushed;
|
|
321
390
|
if (lang === "py") {
|
|
322
391
|
if (type === "function_definition") {
|
|
323
392
|
const nameNode = node.childForFieldName("name");
|
|
324
393
|
if (nameNode) {
|
|
325
394
|
const name = nameNode.text;
|
|
326
|
-
const
|
|
327
|
-
|
|
395
|
+
const qualified = qualify(name);
|
|
396
|
+
const isExported = !insideFunction() && !qualified.split(".").some((s) => s.startsWith("_") && !(s.startsWith("__") && s.endsWith("__")));
|
|
397
|
+
if (isExported) exports.add(qualified);
|
|
328
398
|
const paramsNode = node.childForFieldName("parameters");
|
|
329
399
|
const params = [];
|
|
330
400
|
if (paramsNode) {
|
|
331
401
|
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
|
332
402
|
const param = paramsNode.namedChild(i);
|
|
333
403
|
if (!param) continue;
|
|
404
|
+
if (param.type === "positional_separator" || param.type === "keyword_separator" || param.type === "comment") {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
334
407
|
const pText = param.text;
|
|
335
408
|
const isRest = pText.startsWith("*");
|
|
336
409
|
const hasDefault = param.type === "default_parameter" || param.type === "typed_default_parameter" || pText.includes("=");
|
|
@@ -343,13 +416,18 @@ var PolyglotInvariantEngine = class {
|
|
|
343
416
|
});
|
|
344
417
|
}
|
|
345
418
|
}
|
|
346
|
-
functions.set(
|
|
419
|
+
functions.set(qualified, { name: qualified, isExported, params });
|
|
420
|
+
pushed = { name, isType: false };
|
|
347
421
|
}
|
|
348
422
|
} else if (type === "class_definition") {
|
|
349
423
|
const nameNode = node.childForFieldName("name");
|
|
350
424
|
if (nameNode) {
|
|
351
425
|
const name = nameNode.text;
|
|
352
|
-
|
|
426
|
+
const qualified = qualify(name);
|
|
427
|
+
if (!insideFunction() && !qualified.split(".").some((s) => s.startsWith("_"))) {
|
|
428
|
+
exports.add(qualified);
|
|
429
|
+
}
|
|
430
|
+
pushed = { name, isType: true };
|
|
353
431
|
}
|
|
354
432
|
}
|
|
355
433
|
} else if (lang === "go") {
|
|
@@ -357,23 +435,34 @@ var PolyglotInvariantEngine = class {
|
|
|
357
435
|
const nameNode = node.childForFieldName("name");
|
|
358
436
|
if (nameNode) {
|
|
359
437
|
const name = nameNode.text;
|
|
438
|
+
let key = name;
|
|
439
|
+
if (type === "method_declaration") {
|
|
440
|
+
const receiver = node.childForFieldName("receiver")?.text ?? "";
|
|
441
|
+
const receiverType = /([A-Za-z_]\w*)\s*(?:\[[^\]]*\])?\s*\)\s*$/.exec(
|
|
442
|
+
receiver
|
|
443
|
+
)?.[1];
|
|
444
|
+
if (receiverType) key = `${receiverType}.${name}`;
|
|
445
|
+
}
|
|
360
446
|
const isExported = /^[A-Z]/.test(name);
|
|
361
|
-
if (isExported) exports.add(
|
|
447
|
+
if (isExported) exports.add(key);
|
|
362
448
|
const paramsNode = node.childForFieldName("parameters");
|
|
363
449
|
const params = [];
|
|
364
450
|
if (paramsNode) {
|
|
365
451
|
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
|
366
452
|
const param = paramsNode.namedChild(i);
|
|
367
|
-
if (!param) continue;
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
hasDefault: false,
|
|
372
|
-
|
|
373
|
-
|
|
453
|
+
if (!param || param.type === "comment") continue;
|
|
454
|
+
const isRest = param.type === "variadic_parameter_declaration";
|
|
455
|
+
const names = param.childrenForFieldName("name").filter((n) => n !== null);
|
|
456
|
+
if (names.length === 0) {
|
|
457
|
+
params.push({ name: param.text, isOptional: false, hasDefault: false, isRest });
|
|
458
|
+
} else {
|
|
459
|
+
for (const n of names) {
|
|
460
|
+
params.push({ name: n.text, isOptional: false, hasDefault: false, isRest });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
374
463
|
}
|
|
375
464
|
}
|
|
376
|
-
functions.set(
|
|
465
|
+
functions.set(key, { name: key, isExported, params });
|
|
377
466
|
}
|
|
378
467
|
} else if (type === "type_spec") {
|
|
379
468
|
const nameNode = node.childForFieldName("name");
|
|
@@ -398,18 +487,24 @@ var PolyglotInvariantEngine = class {
|
|
|
398
487
|
}
|
|
399
488
|
}
|
|
400
489
|
} else if (lang === "rs") {
|
|
401
|
-
if (type === "
|
|
490
|
+
if (type === "impl_item") {
|
|
491
|
+
const implType = node.childForFieldName("type")?.text.replace(/<[\s\S]*$/, "");
|
|
492
|
+
if (implType) pushed = { name: implType.trim(), isType: true };
|
|
493
|
+
} else if (type === "function_item") {
|
|
402
494
|
const nameNode = node.childForFieldName("name");
|
|
403
495
|
if (nameNode) {
|
|
404
496
|
const name = nameNode.text;
|
|
405
|
-
const
|
|
406
|
-
|
|
497
|
+
const qualified = qualify(name);
|
|
498
|
+
const isExported = !insideFunction() && node.children.some((c) => c.type === "visibility_modifier");
|
|
499
|
+
if (isExported) exports.add(qualified);
|
|
407
500
|
const paramsNode = node.childForFieldName("parameters");
|
|
408
501
|
const params = [];
|
|
409
502
|
if (paramsNode) {
|
|
410
503
|
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
|
411
504
|
const param = paramsNode.namedChild(i);
|
|
412
|
-
if (!param)
|
|
505
|
+
if (!param || param.type === "attribute_item" || param.type === "line_comment" || param.type === "block_comment") {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
413
508
|
params.push({
|
|
414
509
|
name: param.text,
|
|
415
510
|
isOptional: false,
|
|
@@ -418,20 +513,21 @@ var PolyglotInvariantEngine = class {
|
|
|
418
513
|
});
|
|
419
514
|
}
|
|
420
515
|
}
|
|
421
|
-
functions.set(
|
|
516
|
+
functions.set(qualified, { name: qualified, isExported, params });
|
|
517
|
+
pushed = { name, isType: false };
|
|
422
518
|
}
|
|
423
519
|
} else if (type === "trait_item") {
|
|
424
520
|
const nameNode = node.childForFieldName("name");
|
|
425
521
|
if (nameNode) {
|
|
426
522
|
const name = nameNode.text;
|
|
427
523
|
const isExported = node.children.some((c) => c.type === "visibility_modifier");
|
|
428
|
-
if (isExported) exports.add(name);
|
|
524
|
+
if (isExported) exports.add(qualify(name));
|
|
429
525
|
const methods = [];
|
|
430
526
|
const bodyNode = node.childForFieldName("body");
|
|
431
527
|
if (bodyNode) {
|
|
432
528
|
for (let i = 0; i < bodyNode.namedChildCount; i++) {
|
|
433
529
|
const child = bodyNode.namedChild(i);
|
|
434
|
-
if (child && child.type === "function_item") {
|
|
530
|
+
if (child && (child.type === "function_item" || child.type === "function_signature_item")) {
|
|
435
531
|
const mName = child.childForFieldName("name")?.text;
|
|
436
532
|
const hasDefaultBody = Boolean(child.childForFieldName("body"));
|
|
437
533
|
if (mName) {
|
|
@@ -444,19 +540,30 @@ var PolyglotInvariantEngine = class {
|
|
|
444
540
|
}
|
|
445
541
|
}
|
|
446
542
|
}
|
|
447
|
-
interfaces.set(name, {
|
|
543
|
+
interfaces.set(qualify(name), {
|
|
544
|
+
name: qualify(name),
|
|
545
|
+
isExported,
|
|
546
|
+
properties: [],
|
|
547
|
+
methods
|
|
548
|
+
});
|
|
549
|
+
pushed = { name, isType: true };
|
|
448
550
|
}
|
|
449
551
|
}
|
|
450
552
|
}
|
|
553
|
+
if (pushed) scope.push(pushed);
|
|
451
554
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
452
555
|
const child = node.namedChild(i);
|
|
453
556
|
if (child) walk2(child);
|
|
454
557
|
}
|
|
558
|
+
if (pushed) scope.pop();
|
|
455
559
|
};
|
|
456
560
|
var walk = walk2;
|
|
457
561
|
const root = tree.rootNode;
|
|
562
|
+
const scope = [];
|
|
563
|
+
const qualify = (name) => scope.length > 0 ? `${scope.map((s) => s.name).join(".")}.${name}` : name;
|
|
564
|
+
const insideFunction = () => scope.some((s) => !s.isType);
|
|
458
565
|
walk2(root);
|
|
459
|
-
return { lang, exports, functions, interfaces };
|
|
566
|
+
return { lang, exports, functions, interfaces, supported: true };
|
|
460
567
|
} finally {
|
|
461
568
|
tree.delete();
|
|
462
569
|
parser.delete();
|
|
@@ -560,6 +667,11 @@ async function mutateTsSymbol(content, lang, symbolName, newBody, target = "body
|
|
|
560
667
|
);
|
|
561
668
|
}
|
|
562
669
|
const match = candidates[0];
|
|
670
|
+
if (target === "body" && !match.body) {
|
|
671
|
+
throw new Error(
|
|
672
|
+
`'${match.qualified}' (L${match.line}) is a ${ts.SyntaxKind[match.node.kind]} with no replaceable body. Pass target: "full" with the complete new declaration, or name a function/method inside it.`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
563
675
|
let fullNode = match.node;
|
|
564
676
|
if (ts.isVariableDeclaration(match.node)) {
|
|
565
677
|
const list = match.node.parent;
|
|
@@ -578,6 +690,11 @@ async function mutateTsSymbol(content, lang, symbolName, newBody, target = "body
|
|
|
578
690
|
const baseIndent = indentAt(content, fullNode.getStart(sourceFile));
|
|
579
691
|
formattedReplacement = `{
|
|
580
692
|
${reindent(newBody, `${baseIndent} `)}
|
|
693
|
+
${baseIndent}}`;
|
|
694
|
+
} else if (target === "body" && match.body && !ts.isBlock(match.body) && !ts.isTypeNode(match.body) && /^return\b/.test(formattedReplacement)) {
|
|
695
|
+
const baseIndent = indentAt(content, fullNode.getStart(sourceFile));
|
|
696
|
+
formattedReplacement = `{
|
|
697
|
+
${reindent(newBody, `${baseIndent} `)}
|
|
581
698
|
${baseIndent}}`;
|
|
582
699
|
}
|
|
583
700
|
const updatedContent = content.slice(0, startPos) + formattedReplacement + content.slice(endPos);
|
|
@@ -590,49 +707,113 @@ ${baseIndent}}`;
|
|
|
590
707
|
updatedContent
|
|
591
708
|
};
|
|
592
709
|
}
|
|
710
|
+
var NON_DECLARATION_TYPES = /parameter|argument|declarator$|^field_declaration$|^field$/;
|
|
711
|
+
var TREE_SITTER_BODY_TYPES = [
|
|
712
|
+
"compound_statement",
|
|
713
|
+
"statement_block",
|
|
714
|
+
"block",
|
|
715
|
+
"function_body",
|
|
716
|
+
"body_statement",
|
|
717
|
+
"code_block",
|
|
718
|
+
"do_block"
|
|
719
|
+
];
|
|
720
|
+
function bodyOf(node) {
|
|
721
|
+
return node.childForFieldName("body") ?? node.children.find((c) => c !== null && TREE_SITTER_BODY_TYPES.includes(c.type)) ?? null;
|
|
722
|
+
}
|
|
723
|
+
function indentUnit(bodyText, declIndent, lang) {
|
|
724
|
+
for (const line of bodyText.split("\n").slice(1)) {
|
|
725
|
+
if (!line.trim()) continue;
|
|
726
|
+
const lead = /^[ \t]*/.exec(line)?.[0] ?? "";
|
|
727
|
+
if (lead.startsWith(declIndent) && lead.length > declIndent.length) {
|
|
728
|
+
return lead.slice(declIndent.length);
|
|
729
|
+
}
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
return lang === "go" ? " " : " ";
|
|
733
|
+
}
|
|
593
734
|
async function mutateTreeSitterSymbol(content, lang, symbolName, newBody, target = "body") {
|
|
594
735
|
const parsed = await parseTreeSitterAst({ content, lang });
|
|
595
736
|
if (!parsed) return null;
|
|
596
737
|
const { tree, parser } = parsed;
|
|
597
738
|
try {
|
|
598
739
|
let walk2 = function(node) {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
"
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
740
|
+
let pushed;
|
|
741
|
+
if (node.type === "impl_item") {
|
|
742
|
+
pushed = node.childForFieldName("type")?.text.replace(/<[\s\S]*$/, "").trim() || void 0;
|
|
743
|
+
} else if (!NON_DECLARATION_TYPES.test(node.type)) {
|
|
744
|
+
const name = treeSitterDeclarationName(node);
|
|
745
|
+
if (name !== void 0) {
|
|
746
|
+
let prefix = scope;
|
|
747
|
+
if (node.type === "method_declaration" && lang === "go") {
|
|
748
|
+
const receiver = node.childForFieldName("receiver")?.text ?? "";
|
|
749
|
+
const receiverType = /([A-Za-z_]\w*)\s*(?:\[[^\]]*\])?\s*\)\s*$/.exec(receiver)?.[1];
|
|
750
|
+
if (receiverType) prefix = [...scope, receiverType];
|
|
751
|
+
}
|
|
752
|
+
matches.push({
|
|
753
|
+
node,
|
|
754
|
+
body: bodyOf(node),
|
|
755
|
+
qualified: [...prefix, name].join("."),
|
|
756
|
+
line: node.startPosition.row + 1
|
|
757
|
+
});
|
|
758
|
+
pushed = name;
|
|
759
|
+
}
|
|
615
760
|
}
|
|
761
|
+
if (pushed !== void 0) scope.push(pushed);
|
|
616
762
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
617
763
|
const child = node.namedChild(i);
|
|
618
764
|
if (child) walk2(child);
|
|
619
765
|
}
|
|
766
|
+
if (pushed !== void 0) scope.pop();
|
|
620
767
|
};
|
|
621
768
|
var walk = walk2;
|
|
622
769
|
const root = tree.rootNode;
|
|
623
|
-
|
|
624
|
-
|
|
770
|
+
const matches = [];
|
|
771
|
+
const scope = [];
|
|
625
772
|
walk2(root);
|
|
626
|
-
|
|
627
|
-
|
|
773
|
+
let candidates = matches.filter(
|
|
774
|
+
(m) => m.qualified === symbolName || m.qualified.endsWith(`.${symbolName}`)
|
|
775
|
+
);
|
|
776
|
+
if (candidates.length === 0) return null;
|
|
777
|
+
if (candidates.length > 1 && candidates.every((m) => m.qualified === candidates[0]?.qualified)) {
|
|
778
|
+
const withBody = candidates.filter((m) => m.body !== null);
|
|
779
|
+
if (withBody.length === 1) candidates = withBody;
|
|
780
|
+
}
|
|
781
|
+
if (candidates.length > 1) {
|
|
782
|
+
const listed = candidates.map((m) => `${m.qualified} (L${m.line})`).join(", ");
|
|
783
|
+
const example = candidates.find((m) => m.qualified.includes("."))?.qualified;
|
|
784
|
+
throw new Error(
|
|
785
|
+
`Symbol '${symbolName}' is ambiguous: ${candidates.length} declarations match \u2014 ${listed}. ` + (example ? `Pass a qualified name, e.g. symbol: "${example}".` : "Rename-free disambiguation is impossible here; use the edit tool.")
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
const match = candidates[0];
|
|
789
|
+
if (target === "body" && !match.body) {
|
|
790
|
+
throw new Error(
|
|
791
|
+
`'${match.qualified}' (L${match.line}) is a ${match.node.type} with no replaceable body. Pass target: "full" with the complete new declaration.`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const replaceTarget = target === "full" ? match.node : match.body;
|
|
628
795
|
const startPos = replaceTarget.startIndex;
|
|
629
796
|
const endPos = replaceTarget.endIndex;
|
|
630
797
|
const startLine = replaceTarget.startPosition.row + 1;
|
|
631
798
|
const endLine = replaceTarget.endPosition.row + 1;
|
|
632
799
|
let formattedReplacement = newBody.trim();
|
|
633
|
-
if (target === "body"
|
|
634
|
-
const
|
|
635
|
-
|
|
800
|
+
if (target === "body") {
|
|
801
|
+
const declIndent = indentAt(content, match.node.startIndex);
|
|
802
|
+
const lineStart = content.lastIndexOf("\n", startPos - 1) + 1;
|
|
803
|
+
const lead = content.slice(lineStart, startPos);
|
|
804
|
+
if (replaceTarget.text.startsWith("{")) {
|
|
805
|
+
if (!formattedReplacement.startsWith("{")) {
|
|
806
|
+
const unit2 = indentUnit(replaceTarget.text, declIndent, lang);
|
|
807
|
+
formattedReplacement = `{
|
|
808
|
+
${reindent(newBody, `${declIndent}${unit2}`)}
|
|
809
|
+
${declIndent}}`;
|
|
810
|
+
}
|
|
811
|
+
} else if (/^[ \t]*$/.test(lead)) {
|
|
812
|
+
formattedReplacement = reindent(newBody, lead).slice(lead.length);
|
|
813
|
+
} else {
|
|
814
|
+
formattedReplacement = `
|
|
815
|
+
${reindent(newBody, `${declIndent} `)}`;
|
|
816
|
+
}
|
|
636
817
|
}
|
|
637
818
|
const updatedContent = content.slice(0, startPos) + formattedReplacement + content.slice(endPos);
|
|
638
819
|
const newEndLine = startLine + formattedReplacement.split("\n").length - 1;
|
|
@@ -648,7 +829,7 @@ async function mutateTreeSitterSymbol(content, lang, symbolName, newBody, target
|
|
|
648
829
|
parser.delete();
|
|
649
830
|
}
|
|
650
831
|
}
|
|
651
|
-
var TEST_CALLEES =
|
|
832
|
+
var TEST_CALLEES = "(?:it|test|describe|suite|bench|context|specify)";
|
|
652
833
|
function notFoundMessage(symbol, relPath, content) {
|
|
653
834
|
const base = `Symbol '${symbol}' could not be located in AST of '${relPath}'`;
|
|
654
835
|
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -704,6 +885,7 @@ Pass allowBreakingChanges: true if this breaking change is intended.`
|
|
|
704
885
|
|
|
705
886
|
// src/codebase-index/codebase-ast-replace-tool.ts
|
|
706
887
|
import * as path2 from "node:path";
|
|
888
|
+
import { ToolValidationError } from "@wrongstack/core/types";
|
|
707
889
|
var codebaseAstReplaceTool = {
|
|
708
890
|
name: "codebase-ast-replace",
|
|
709
891
|
category: "Edit",
|
|
@@ -750,6 +932,21 @@ var codebaseAstReplaceTool = {
|
|
|
750
932
|
// to the executor (is_error:false) — the UI showed "ok", the audit log and
|
|
751
933
|
// spans recorded success, and only a model reading the payload noticed.
|
|
752
934
|
async execute(input, ctx) {
|
|
935
|
+
for (const field of ["file", "symbol", "newBody"]) {
|
|
936
|
+
const value = input?.[field];
|
|
937
|
+
if (typeof value !== "string" || field !== "newBody" && !value.trim()) {
|
|
938
|
+
throw new ToolValidationError({
|
|
939
|
+
message: `codebase-ast-replace: ${field} is required and must be a${field === "newBody" ? " string" : " non-empty string"}`,
|
|
940
|
+
field
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (input.target !== void 0 && input.target !== "body" && input.target !== "full") {
|
|
945
|
+
throw new ToolValidationError({
|
|
946
|
+
message: `codebase-ast-replace: target must be "body" or "full", got ${JSON.stringify(input.target)}`,
|
|
947
|
+
field: "target"
|
|
948
|
+
});
|
|
949
|
+
}
|
|
753
950
|
const projectRoot = ctx.projectRoot ?? ctx.cwd ?? process.cwd();
|
|
754
951
|
const file = await safeResolveProjectPath(input.file, ctx);
|
|
755
952
|
const result = await replaceSymbolInFile({ ...input, file }, projectRoot);
|
|
@@ -766,8 +963,12 @@ var codebaseAstReplaceTool = {
|
|
|
766
963
|
|
|
767
964
|
// src/codebase-index/codebase-impact-analysis-tool.ts
|
|
768
965
|
import * as path3 from "node:path";
|
|
966
|
+
import { ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
|
|
769
967
|
import { toErrorMessage } from "@wrongstack/core/utils";
|
|
770
|
-
var TEST_FILE_REGEX = /(?:
|
|
968
|
+
var TEST_FILE_REGEX = /(?:^|[/\\._-])(?:tests?|specs?|mocks?|fixtures?|bench(?:es|marks?)?)(?:[/\\._-]|$)/i;
|
|
969
|
+
function isTestFilePath(relPath) {
|
|
970
|
+
return TEST_FILE_REGEX.test(relPath);
|
|
971
|
+
}
|
|
771
972
|
var codebaseImpactAnalysisTool = {
|
|
772
973
|
name: "codebase-impact-analysis",
|
|
773
974
|
category: "Project",
|
|
@@ -775,6 +976,9 @@ var codebaseImpactAnalysisTool = {
|
|
|
775
976
|
permission: "auto",
|
|
776
977
|
mutating: false,
|
|
777
978
|
capabilities: ["fs.read"],
|
|
979
|
+
// The index host has its own 30s read watchdog; leave headroom so its
|
|
980
|
+
// structured timeout reaches the agent instead of a generic TOOL_TIMEOUT.
|
|
981
|
+
timeoutMs: 35e3,
|
|
778
982
|
description: "Perform a blast radius and impact analysis before modifying or refactoring a function, class, or type. Identifies all production call sites, affected test suites, and breaking change risks across the entire codebase.",
|
|
779
983
|
usageHint: 'CALL BEFORE MODIFYING FUNCTION SIGNATURES, PARAMETERS, OR RETURN TYPES:\n\n- Pass `symbol: "calculateDiscount"` to get the complete blast radius.\n- Groups affected code into production callers vs. test files.\n- Provides a step-by-step action plan of every file and line that must be updated alongside the change.\n- Pair with `codebase-targeted-test` to verify affected test suites afterwards.',
|
|
780
984
|
inputSchema: {
|
|
@@ -801,12 +1005,21 @@ var codebaseImpactAnalysisTool = {
|
|
|
801
1005
|
const indexDir = codebaseIndexDirOverride(ctx);
|
|
802
1006
|
const limit = 200;
|
|
803
1007
|
const transitive = input.transitive ?? true;
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1008
|
+
if (!input?.symbol || typeof input.symbol !== "string" || !input.symbol.trim()) {
|
|
1009
|
+
throw new ToolValidationError2({
|
|
1010
|
+
message: "codebase-impact-analysis: symbol is required and cannot be empty",
|
|
1011
|
+
field: "symbol"
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
const state = getIndexState();
|
|
1015
|
+
if (state.lastError) {
|
|
1016
|
+
const circuit = state.circuit;
|
|
1017
|
+
const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s).` : "Try /codebase-reindex.";
|
|
1018
|
+
throw new Error(`Index build failed: ${state.lastError}. ${retryHint}`);
|
|
1019
|
+
}
|
|
1020
|
+
let serviced;
|
|
808
1021
|
try {
|
|
809
|
-
|
|
1022
|
+
serviced = await incomingCallsService({
|
|
810
1023
|
projectRoot,
|
|
811
1024
|
indexDir,
|
|
812
1025
|
symbol: input.symbol,
|
|
@@ -814,20 +1027,38 @@ var codebaseImpactAnalysisTool = {
|
|
|
814
1027
|
limit,
|
|
815
1028
|
transitive
|
|
816
1029
|
});
|
|
817
|
-
rawSites = serviced.calls;
|
|
818
|
-
symbolFound = serviced.symbolFound;
|
|
819
1030
|
} catch (err) {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1031
|
+
if (err.name === "IndexRefreshInProgressError") {
|
|
1032
|
+
throw new Error(
|
|
1033
|
+
`Index refresh in progress (${state.currentFile}/${state.totalFiles} files); '${input.symbol}' has no cached answer yet \u2014 retry after the completed generation is published.`,
|
|
1034
|
+
{ cause: err }
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
826
1037
|
throw new Error(
|
|
827
|
-
`Index query failed for '${input.symbol}'. Run codebase-index, then retry
|
|
1038
|
+
`Index query failed for '${input.symbol}'. Run codebase-index, then retry. (${toErrorMessage(err)})`,
|
|
1039
|
+
{ cause: err }
|
|
828
1040
|
);
|
|
829
1041
|
}
|
|
1042
|
+
const rawSites = serviced.calls;
|
|
1043
|
+
const { symbolFound, totalMatches, stale } = serviced;
|
|
830
1044
|
if (!symbolFound) {
|
|
1045
|
+
let hasPersistedIndex = state.ready;
|
|
1046
|
+
if (!hasPersistedIndex) {
|
|
1047
|
+
try {
|
|
1048
|
+
const stats = await codebaseIndexStats({ projectRoot, indexDir });
|
|
1049
|
+
hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
|
|
1050
|
+
} catch (err) {
|
|
1051
|
+
throw new Error(
|
|
1052
|
+
`Symbol '${input.symbol}' was not found and the persisted index could not be verified: ${toErrorMessage(err)}. Try /codebase-reindex.`,
|
|
1053
|
+
{ cause: err }
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (!hasPersistedIndex) {
|
|
1058
|
+
throw new Error(
|
|
1059
|
+
"No persisted index data found. Run codebase-index to build it, then retry codebase-impact-analysis."
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
831
1062
|
const reason = `Symbol '${input.symbol}' was not found in the index` + (input.file ? ` for file filter '${input.file}'` : "") + ". Use codebase-search to verify the name.";
|
|
832
1063
|
return {
|
|
833
1064
|
status: "ok",
|
|
@@ -844,14 +1075,18 @@ var codebaseImpactAnalysisTool = {
|
|
|
844
1075
|
}
|
|
845
1076
|
const prodFilesSet = /* @__PURE__ */ new Set();
|
|
846
1077
|
const testFilesSet = /* @__PURE__ */ new Set();
|
|
1078
|
+
const directProdFilesSet = /* @__PURE__ */ new Set();
|
|
1079
|
+
const indirectProdFilesSet = /* @__PURE__ */ new Set();
|
|
847
1080
|
const callSites = [];
|
|
848
1081
|
for (const site of rawSites) {
|
|
849
1082
|
const relPath = path3.relative(projectRoot, site.symbol.file).replace(/\\/g, "/");
|
|
850
|
-
const isTest =
|
|
1083
|
+
const isTest = isTestFilePath(relPath);
|
|
1084
|
+
const indirect = !site.callType;
|
|
851
1085
|
if (isTest) {
|
|
852
1086
|
testFilesSet.add(relPath);
|
|
853
1087
|
} else {
|
|
854
1088
|
prodFilesSet.add(relPath);
|
|
1089
|
+
(indirect ? indirectProdFilesSet : directProdFilesSet).add(relPath);
|
|
855
1090
|
}
|
|
856
1091
|
callSites.push({
|
|
857
1092
|
file: relPath,
|
|
@@ -859,22 +1094,34 @@ var codebaseImpactAnalysisTool = {
|
|
|
859
1094
|
callerName: site.symbol.name,
|
|
860
1095
|
callerKind: site.symbol.kind,
|
|
861
1096
|
callType: site.callType,
|
|
862
|
-
isTest
|
|
1097
|
+
isTest,
|
|
1098
|
+
...indirect ? { indirect: true } : {}
|
|
863
1099
|
});
|
|
864
1100
|
}
|
|
865
1101
|
const prodFiles = [...prodFilesSet];
|
|
866
1102
|
const testFiles = [...testFilesSet];
|
|
867
|
-
const
|
|
1103
|
+
const directProdFiles = [...directProdFilesSet];
|
|
1104
|
+
const indirectOnlyProdFiles = [...indirectProdFilesSet].filter(
|
|
1105
|
+
(file) => !directProdFilesSet.has(file)
|
|
1106
|
+
);
|
|
1107
|
+
const totalCallSites = callSites.filter((site) => !site.indirect).length;
|
|
1108
|
+
const indirectCallers = callSites.length - totalCallSites;
|
|
868
1109
|
let riskLevel = "low";
|
|
869
|
-
if (
|
|
1110
|
+
if (directProdFiles.length > 5 || totalCallSites > 10) {
|
|
870
1111
|
riskLevel = "high";
|
|
871
|
-
} else if (
|
|
1112
|
+
} else if (directProdFiles.length > 1 || totalCallSites > 3) {
|
|
872
1113
|
riskLevel = "medium";
|
|
873
1114
|
}
|
|
1115
|
+
if (riskLevel === "low" && prodFiles.length > 5) riskLevel = "medium";
|
|
874
1116
|
const recommendedActionPlan = [];
|
|
875
|
-
if (
|
|
1117
|
+
if (directProdFiles.length > 0) {
|
|
876
1118
|
recommendedActionPlan.push(
|
|
877
|
-
`Update ${totalCallSites} call site(s) across ${
|
|
1119
|
+
`Update ${totalCallSites} call site(s) across ${directProdFiles.length} production file(s): ${directProdFiles.slice(0, 3).join(", ")}${directProdFiles.length > 3 ? ` +${directProdFiles.length - 3} more` : ""}`
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
if (indirectOnlyProdFiles.length > 0) {
|
|
1123
|
+
recommendedActionPlan.push(
|
|
1124
|
+
`Re-verify ${indirectOnlyProdFiles.length} production file(s) that reach it only indirectly: ${indirectOnlyProdFiles.slice(0, 3).join(", ")}${indirectOnlyProdFiles.length > 3 ? ` +${indirectOnlyProdFiles.length - 3} more` : ""}`
|
|
878
1125
|
);
|
|
879
1126
|
}
|
|
880
1127
|
if (testFiles.length > 0) {
|
|
@@ -886,7 +1133,16 @@ var codebaseImpactAnalysisTool = {
|
|
|
886
1133
|
`No existing test files directly cover '${input.symbol}'. Consider adding a test suite.`
|
|
887
1134
|
);
|
|
888
1135
|
}
|
|
889
|
-
const
|
|
1136
|
+
const capped = totalMatches > callSites.length;
|
|
1137
|
+
if (capped) riskLevel = "high";
|
|
1138
|
+
let summary = `Blast Radius for '${input.symbol}': ${riskLevel.toUpperCase()} RISK (${totalCallSites} call sites in ${directProdFiles.length} prod files, ${testFiles.length} test suites`;
|
|
1139
|
+
summary += indirectCallers > 0 ? `; ${indirectCallers} indirect caller(s)).` : ").";
|
|
1140
|
+
if (capped) {
|
|
1141
|
+
summary += ` Only the first ${callSites.length} of ${totalMatches} callers were analyzed; pass \`file\` to narrow.`;
|
|
1142
|
+
}
|
|
1143
|
+
if (stale) {
|
|
1144
|
+
summary += ` Index refresh in progress; served from the previous generation \u2014 call sites in files being indexed may lag.`;
|
|
1145
|
+
}
|
|
890
1146
|
return {
|
|
891
1147
|
status: "ok",
|
|
892
1148
|
symbol: input.symbol,
|
|
@@ -897,13 +1153,14 @@ var codebaseImpactAnalysisTool = {
|
|
|
897
1153
|
affectedTestFiles: testFiles,
|
|
898
1154
|
callSites,
|
|
899
1155
|
recommendedActionPlan,
|
|
900
|
-
symbolFound: true
|
|
1156
|
+
symbolFound: true,
|
|
1157
|
+
...stale ? { stale: true } : {}
|
|
901
1158
|
};
|
|
902
1159
|
}
|
|
903
1160
|
};
|
|
904
1161
|
|
|
905
1162
|
// src/codebase-index/codebase-incoming-calls-tool.ts
|
|
906
|
-
import { ToolValidationError } from "@wrongstack/core/types";
|
|
1163
|
+
import { ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
|
|
907
1164
|
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
908
1165
|
var codebaseIncomingCallsTool = {
|
|
909
1166
|
name: "codebase-incoming-calls",
|
|
@@ -942,7 +1199,7 @@ var codebaseIncomingCallsTool = {
|
|
|
942
1199
|
},
|
|
943
1200
|
async execute(input, ctx) {
|
|
944
1201
|
if (!input?.symbol || typeof input.symbol !== "string" || !input.symbol.trim()) {
|
|
945
|
-
throw new
|
|
1202
|
+
throw new ToolValidationError3({
|
|
946
1203
|
message: "codebase-incoming-calls: symbol is required and cannot be empty",
|
|
947
1204
|
field: "symbol"
|
|
948
1205
|
});
|
|
@@ -984,7 +1241,7 @@ var codebaseIncomingCallsTool = {
|
|
|
984
1241
|
if (!hasPersistedIndex) {
|
|
985
1242
|
try {
|
|
986
1243
|
const stats = await codebaseIndexStats({
|
|
987
|
-
projectRoot
|
|
1244
|
+
projectRoot,
|
|
988
1245
|
indexDir: codebaseIndexDirOverride(ctx)
|
|
989
1246
|
});
|
|
990
1247
|
hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
|
|
@@ -1015,7 +1272,7 @@ var codebaseIncomingCallsTool = {
|
|
|
1015
1272
|
}
|
|
1016
1273
|
if (ambiguous) {
|
|
1017
1274
|
notes.push(
|
|
1018
|
-
`Symbol "${input.symbol}" exists in multiple files.
|
|
1275
|
+
`Symbol "${input.symbol}" exists in multiple files. Callers resolved to another file were excluded, but callers whose target could not be pinned to a file are still included. Verify with codebase-search before relying on the list.`
|
|
1019
1276
|
);
|
|
1020
1277
|
}
|
|
1021
1278
|
if (stale) {
|
|
@@ -1034,10 +1291,10 @@ var codebaseIncomingCallsTool = {
|
|
|
1034
1291
|
};
|
|
1035
1292
|
|
|
1036
1293
|
// src/codebase-index/codebase-index-tool.ts
|
|
1037
|
-
import { ToolValidationError as
|
|
1294
|
+
import { ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
|
|
1038
1295
|
|
|
1039
1296
|
// src/codebase-index/codebase-search-tool.ts
|
|
1040
|
-
import { ToolValidationError as
|
|
1297
|
+
import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
1041
1298
|
import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
|
|
1042
1299
|
var INDEXABLE_LANG_IDS = [
|
|
1043
1300
|
...new Set(Object.values(EXT_TO_LANG))
|
|
@@ -1115,13 +1372,13 @@ var codebaseSearchTool = {
|
|
|
1115
1372
|
},
|
|
1116
1373
|
async execute(input, ctx, execOpts) {
|
|
1117
1374
|
if (!input?.query || typeof input.query !== "string" || !input.query.trim()) {
|
|
1118
|
-
throw new
|
|
1375
|
+
throw new ToolValidationError4({
|
|
1119
1376
|
message: "codebase-search: query is required and cannot be empty",
|
|
1120
1377
|
field: "query"
|
|
1121
1378
|
});
|
|
1122
1379
|
}
|
|
1123
1380
|
if (input.lspKind !== void 0 && (!Number.isInteger(input.lspKind) || input.lspKind < 1 || input.lspKind > 26)) {
|
|
1124
|
-
throw new
|
|
1381
|
+
throw new ToolValidationError4({
|
|
1125
1382
|
message: "codebase-search: lspKind must be an integer between 1 and 26",
|
|
1126
1383
|
field: "lspKind"
|
|
1127
1384
|
});
|
|
@@ -1225,7 +1482,7 @@ var codebaseIndexTool = {
|
|
|
1225
1482
|
properties: {
|
|
1226
1483
|
force: {
|
|
1227
1484
|
type: "boolean",
|
|
1228
|
-
description: "Force a
|
|
1485
|
+
description: "Force a reindex that ignores mtime/content-hash caches. Without `langs` it clears the index and rebuilds every file; with `langs` it re-parses only those languages and keeps the rest."
|
|
1229
1486
|
},
|
|
1230
1487
|
langs: {
|
|
1231
1488
|
type: "array",
|
|
@@ -1239,7 +1496,7 @@ var codebaseIndexTool = {
|
|
|
1239
1496
|
signal?.throwIfAborted();
|
|
1240
1497
|
if (input.langs) {
|
|
1241
1498
|
if (!Array.isArray(input.langs) || input.langs.length === 0) {
|
|
1242
|
-
throw new
|
|
1499
|
+
throw new ToolValidationError5({
|
|
1243
1500
|
message: "codebase-index: langs cannot be an empty array. Pass at least one valid language or omit langs.",
|
|
1244
1501
|
field: "langs"
|
|
1245
1502
|
});
|
|
@@ -1248,7 +1505,7 @@ var codebaseIndexTool = {
|
|
|
1248
1505
|
(lang) => !INDEXABLE_LANG_IDS.includes(lang)
|
|
1249
1506
|
);
|
|
1250
1507
|
if (unknown.length > 0) {
|
|
1251
|
-
throw new
|
|
1508
|
+
throw new ToolValidationError5({
|
|
1252
1509
|
message: `codebase-index: unknown lang(s) ${unknown.map((l) => `"${l}"`).join(", ")}. Valid ids: ${INDEXABLE_LANG_IDS.join(", ")}.`,
|
|
1253
1510
|
field: "langs"
|
|
1254
1511
|
});
|
|
@@ -1289,7 +1546,8 @@ var codebaseIndexTool = {
|
|
|
1289
1546
|
|
|
1290
1547
|
// src/codebase-index/codebase-invariant-check-tool.ts
|
|
1291
1548
|
import * as fs2 from "node:fs/promises";
|
|
1292
|
-
import { ToolValidationError as
|
|
1549
|
+
import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
|
|
1550
|
+
var KNOWN_LANGS = /* @__PURE__ */ new Set([...Object.values(EXT_TO_LANG), "other"]);
|
|
1293
1551
|
var codebaseInvariantCheckTool = {
|
|
1294
1552
|
name: "codebase-invariant-check",
|
|
1295
1553
|
category: "Inspect",
|
|
@@ -1298,8 +1556,8 @@ var codebaseInvariantCheckTool = {
|
|
|
1298
1556
|
subjectKey: "file",
|
|
1299
1557
|
mutating: false,
|
|
1300
1558
|
capabilities: ["fs.read"],
|
|
1301
|
-
description: "
|
|
1302
|
-
usageHint: "PRE-FLIGHT AST INVARIANT AUDIT:\n\n- Provide `file` and `modifiedCode` (or both `originalCode` and `modifiedCode`).\n- Returns `valid: true`
|
|
1559
|
+
description: "Deterministically check AST backward-compatibility invariants between original code and a candidate mutation for TypeScript/JavaScript, Python, Go and Rust. Detects removed exports (INV-001), mandatory parameter additions (INV-002), interface breaking expansions (INV-003), and incompatible return type changes (INV-004). Other languages return `verified: false`.",
|
|
1560
|
+
usageHint: "PRE-FLIGHT AST INVARIANT AUDIT:\n\n- Provide `file` and `modifiedCode` (or both `originalCode` and `modifiedCode`).\n- Returns `valid: true` when no rule was violated; check `verified` \u2014 it is false for languages without rules.\n- Returns structured `violations[]` with exact rule IDs, offending symbol names, and fix instructions.",
|
|
1303
1561
|
inputSchema: {
|
|
1304
1562
|
type: "object",
|
|
1305
1563
|
properties: {
|
|
@@ -1327,9 +1585,21 @@ var codebaseInvariantCheckTool = {
|
|
|
1327
1585
|
// (is_error:false) whose payload was indistinguishable from "this change
|
|
1328
1586
|
// breaks compatibility", so an unreadable file read as a real violation.
|
|
1329
1587
|
async execute(input, ctx) {
|
|
1588
|
+
if (typeof input?.modifiedCode !== "string") {
|
|
1589
|
+
throw new ToolValidationError6({
|
|
1590
|
+
message: "codebase-invariant-check: modifiedCode must be a string.",
|
|
1591
|
+
field: "modifiedCode"
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
if (input.lang !== void 0 && !KNOWN_LANGS.has(input.lang)) {
|
|
1595
|
+
throw new ToolValidationError6({
|
|
1596
|
+
message: `codebase-invariant-check: unknown lang "${String(input.lang)}". Valid ids: ${[...KNOWN_LANGS].sort().join(", ")}.`,
|
|
1597
|
+
field: "lang"
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1330
1600
|
let originalCode = input.originalCode;
|
|
1331
1601
|
let lang = input.lang;
|
|
1332
|
-
if (
|
|
1602
|
+
if (originalCode === void 0 && input.file) {
|
|
1333
1603
|
const resolved = await safeResolveProjectPath(input.file, ctx);
|
|
1334
1604
|
originalCode = await fs2.readFile(resolved, "utf8");
|
|
1335
1605
|
if (!lang) {
|
|
@@ -1337,7 +1607,7 @@ var codebaseInvariantCheckTool = {
|
|
|
1337
1607
|
}
|
|
1338
1608
|
}
|
|
1339
1609
|
if (originalCode === void 0) {
|
|
1340
|
-
throw new
|
|
1610
|
+
throw new ToolValidationError6({
|
|
1341
1611
|
message: "Neither originalCode nor file was provided for invariant comparison.",
|
|
1342
1612
|
field: "originalCode"
|
|
1343
1613
|
});
|
|
@@ -1348,10 +1618,11 @@ var codebaseInvariantCheckTool = {
|
|
|
1348
1618
|
lang,
|
|
1349
1619
|
filePath: input.file
|
|
1350
1620
|
});
|
|
1351
|
-
const summary = res.valid ? "AST Invariants PASSED.
|
|
1621
|
+
const summary = !res.supported ? `AST Invariants NOT VERIFIED: no invariant rules exist for '${res.lang}' (supported: ts, tsx, js, jsx, py, go, rs). Review compatibility manually.` : res.valid ? "AST Invariants PASSED. No export removal, mandatory parameter/property addition, or incompatible return type change detected." : `AST Invariants FAILED with ${res.violations.length} violation(s):
|
|
1352
1622
|
` + res.violations.map((v) => ` - [${v.ruleId}] ${v.symbolName}: ${v.message}`).join("\n");
|
|
1353
1623
|
return {
|
|
1354
1624
|
valid: res.valid,
|
|
1625
|
+
verified: res.supported,
|
|
1355
1626
|
violations: res.violations,
|
|
1356
1627
|
summary
|
|
1357
1628
|
};
|
|
@@ -1359,7 +1630,7 @@ var codebaseInvariantCheckTool = {
|
|
|
1359
1630
|
};
|
|
1360
1631
|
|
|
1361
1632
|
// src/codebase-index/codebase-outgoing-calls-tool.ts
|
|
1362
|
-
import { ToolValidationError as
|
|
1633
|
+
import { ToolValidationError as ToolValidationError7 } from "@wrongstack/core/types";
|
|
1363
1634
|
import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
|
|
1364
1635
|
var codebaseOutgoingCallsTool = {
|
|
1365
1636
|
name: "codebase-outgoing-calls",
|
|
@@ -1398,7 +1669,7 @@ var codebaseOutgoingCallsTool = {
|
|
|
1398
1669
|
},
|
|
1399
1670
|
async execute(input, ctx) {
|
|
1400
1671
|
if (!input?.symbol || typeof input.symbol !== "string" || !input.symbol.trim()) {
|
|
1401
|
-
throw new
|
|
1672
|
+
throw new ToolValidationError7({
|
|
1402
1673
|
message: "codebase-outgoing-calls: symbol is required and cannot be empty",
|
|
1403
1674
|
field: "symbol"
|
|
1404
1675
|
});
|
|
@@ -1440,7 +1711,7 @@ var codebaseOutgoingCallsTool = {
|
|
|
1440
1711
|
if (!hasPersistedIndex) {
|
|
1441
1712
|
try {
|
|
1442
1713
|
const stats = await codebaseIndexStats({
|
|
1443
|
-
projectRoot
|
|
1714
|
+
projectRoot,
|
|
1444
1715
|
indexDir: codebaseIndexDirOverride(ctx)
|
|
1445
1716
|
});
|
|
1446
1717
|
hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
|
|
@@ -1489,6 +1760,10 @@ var codebaseOutgoingCallsTool = {
|
|
|
1489
1760
|
}
|
|
1490
1761
|
};
|
|
1491
1762
|
|
|
1763
|
+
// src/codebase-index/codebase-repo-map-tool.ts
|
|
1764
|
+
import * as path7 from "node:path";
|
|
1765
|
+
import { ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
|
|
1766
|
+
|
|
1492
1767
|
// src/codebase-index/repo-map.ts
|
|
1493
1768
|
import { existsSync } from "node:fs";
|
|
1494
1769
|
import * as path6 from "node:path";
|
|
@@ -1513,7 +1788,7 @@ async function loadTs3() {
|
|
|
1513
1788
|
}
|
|
1514
1789
|
function applyReplacements(content, replacements) {
|
|
1515
1790
|
if (replacements.length === 0) return content;
|
|
1516
|
-
const sorted = [...replacements].sort((a, b) => a.start - b.start);
|
|
1791
|
+
const sorted = [...replacements].sort((a, b) => a.start - b.start || b.end - a.end);
|
|
1517
1792
|
let result = "";
|
|
1518
1793
|
let lastIndex = 0;
|
|
1519
1794
|
for (const r of sorted) {
|
|
@@ -1541,23 +1816,51 @@ async function extractTsSkeleton(content, lang, opts) {
|
|
|
1541
1816
|
const symbolRanges = [];
|
|
1542
1817
|
let symbolCount = 0;
|
|
1543
1818
|
const includeLineNumbers = opts.includeLineNumbers ?? true;
|
|
1819
|
+
const exportedByName = /* @__PURE__ */ new Set();
|
|
1820
|
+
for (const statement of sourceFile.statements) {
|
|
1821
|
+
if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
1822
|
+
for (const element of statement.exportClause.elements) {
|
|
1823
|
+
exportedByName.add((element.propertyName ?? element.name).text);
|
|
1824
|
+
}
|
|
1825
|
+
} else if (ts.isExportAssignment(statement) && !statement.isExportEquals && ts.isIdentifier(statement.expression)) {
|
|
1826
|
+
exportedByName.add(statement.expression.text);
|
|
1827
|
+
} else if (ts.isExportAssignment(statement) && statement.isExportEquals && ts.isIdentifier(statement.expression)) {
|
|
1828
|
+
exportedByName.add(statement.expression.text);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1544
1831
|
function isExported(node) {
|
|
1545
|
-
if (
|
|
1546
|
-
|
|
1547
|
-
|
|
1832
|
+
if (ts.canHaveModifiers(node)) {
|
|
1833
|
+
const modifiers = ts.getModifiers(node);
|
|
1834
|
+
if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) return true;
|
|
1835
|
+
}
|
|
1836
|
+
if (ts.isVariableStatement(node)) {
|
|
1837
|
+
return node.declarationList.declarations.some(
|
|
1838
|
+
(d) => ts.isIdentifier(d.name) && exportedByName.has(d.name.text)
|
|
1839
|
+
);
|
|
1840
|
+
}
|
|
1841
|
+
const name = node.name;
|
|
1842
|
+
return name !== void 0 && ts.isIdentifier(name) && exportedByName.has(name.text);
|
|
1843
|
+
}
|
|
1844
|
+
function removeDeclaration(node) {
|
|
1845
|
+
replacements.push({ start: node.getStart(sourceFile, true), end: node.getEnd(), text: "" });
|
|
1548
1846
|
}
|
|
1549
1847
|
function visit(node, depth) {
|
|
1550
1848
|
if (opts.maxDepth !== void 0 && depth > opts.maxDepth) {
|
|
1551
1849
|
return;
|
|
1552
1850
|
}
|
|
1851
|
+
if (opts.includeDocs === false) {
|
|
1852
|
+
for (const doc of node.jsDoc ?? []) {
|
|
1853
|
+
replacements.push({ start: doc.getStart(sourceFile), end: doc.getEnd(), text: "" });
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
if (opts.exportsOnly && depth === 0 && (ts.isVariableStatement(node) || ts.isModuleDeclaration(node)) && !isExported(node)) {
|
|
1857
|
+
removeDeclaration(node);
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1553
1860
|
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node)) {
|
|
1554
1861
|
symbolCount++;
|
|
1555
1862
|
if (opts.exportsOnly && depth === 0 && !isExported(node)) {
|
|
1556
|
-
|
|
1557
|
-
start: node.getStart(sourceFile),
|
|
1558
|
-
end: node.getEnd(),
|
|
1559
|
-
text: ""
|
|
1560
|
-
});
|
|
1863
|
+
removeDeclaration(node);
|
|
1561
1864
|
return;
|
|
1562
1865
|
}
|
|
1563
1866
|
const nodeStart = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
@@ -1619,11 +1922,7 @@ async function extractTsSkeleton(content, lang, opts) {
|
|
|
1619
1922
|
} else if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node)) {
|
|
1620
1923
|
symbolCount++;
|
|
1621
1924
|
if (opts.exportsOnly && depth === 0 && !isExported(node)) {
|
|
1622
|
-
|
|
1623
|
-
start: node.getStart(sourceFile),
|
|
1624
|
-
end: node.getEnd(),
|
|
1625
|
-
text: ""
|
|
1626
|
-
});
|
|
1925
|
+
removeDeclaration(node);
|
|
1627
1926
|
return;
|
|
1628
1927
|
}
|
|
1629
1928
|
let name;
|
|
@@ -1675,6 +1974,37 @@ function collapseTsImports(code) {
|
|
|
1675
1974
|
}
|
|
1676
1975
|
return resultLines.join("\n");
|
|
1677
1976
|
}
|
|
1977
|
+
function isPrivateTopLevel(node, lang) {
|
|
1978
|
+
if (lang === "py") {
|
|
1979
|
+
const decl = node.type === "decorated_definition" ? node.childForFieldName("definition") : node;
|
|
1980
|
+
if (decl?.type !== "function_definition" && decl?.type !== "class_definition") return false;
|
|
1981
|
+
return decl.childForFieldName("name")?.text.startsWith("_") ?? false;
|
|
1982
|
+
}
|
|
1983
|
+
if (lang === "go") {
|
|
1984
|
+
let name;
|
|
1985
|
+
if (node.type === "function_declaration" || node.type === "method_declaration") {
|
|
1986
|
+
name = node.childForFieldName("name")?.text;
|
|
1987
|
+
} else if (node.type === "type_declaration" && node.namedChildCount === 1) {
|
|
1988
|
+
name = node.namedChild(0)?.childForFieldName("name")?.text;
|
|
1989
|
+
}
|
|
1990
|
+
return name !== void 0 && !/^[A-Z]/.test(name);
|
|
1991
|
+
}
|
|
1992
|
+
if (lang === "rs") {
|
|
1993
|
+
const items = [
|
|
1994
|
+
"function_item",
|
|
1995
|
+
"struct_item",
|
|
1996
|
+
"enum_item",
|
|
1997
|
+
"trait_item",
|
|
1998
|
+
"type_item",
|
|
1999
|
+
"const_item",
|
|
2000
|
+
"static_item",
|
|
2001
|
+
"mod_item"
|
|
2002
|
+
];
|
|
2003
|
+
if (!items.includes(node.type)) return false;
|
|
2004
|
+
return !node.children.some((c) => c?.type === "visibility_modifier");
|
|
2005
|
+
}
|
|
2006
|
+
return false;
|
|
2007
|
+
}
|
|
1678
2008
|
async function extractTreeSitterSkeleton(content, lang, opts) {
|
|
1679
2009
|
const parsed = await parseTreeSitterAst({ content, lang });
|
|
1680
2010
|
if (!parsed) return null;
|
|
@@ -1684,9 +2014,17 @@ async function extractTreeSitterSkeleton(content, lang, opts) {
|
|
|
1684
2014
|
let symbolCount = 0;
|
|
1685
2015
|
const includeLineNumbers = opts.includeLineNumbers ?? true;
|
|
1686
2016
|
try {
|
|
1687
|
-
let walk2 = function(node) {
|
|
2017
|
+
let walk2 = function(node, depth) {
|
|
1688
2018
|
const type = node.type;
|
|
1689
|
-
if (
|
|
2019
|
+
if (opts.includeDocs === false && /comment$/.test(type)) {
|
|
2020
|
+
replacements.push({ start: node.startIndex, end: node.endIndex, text: "" });
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
if (opts.exportsOnly && depth === 1 && isPrivateTopLevel(node, lang)) {
|
|
2024
|
+
replacements.push({ start: node.startIndex, end: node.endIndex, text: "" });
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
if (type === "function_definition" || type === "function_declaration" || type === "method_declaration" || type === "constructor_declaration" || type === "function_item" || type === "method" || type === "singleton_method" || type === "func_literal") {
|
|
1690
2028
|
symbolCount++;
|
|
1691
2029
|
const nodeStart = node.startPosition.row + 1;
|
|
1692
2030
|
const nodeEnd = node.endPosition.row + 1;
|
|
@@ -1708,11 +2046,27 @@ async function extractTreeSitterSkeleton(content, lang, opts) {
|
|
|
1708
2046
|
bodyEnd = bodyNode.endPosition.row + 1;
|
|
1709
2047
|
const label = includeLineNumbers ? `/* L${bodyStart}-L${bodyEnd} */` : "/* ... */";
|
|
1710
2048
|
if (lang === "py") {
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
2049
|
+
const stub = includeLineNumbers ? `... # L${bodyStart}-L${bodyEnd}` : "...";
|
|
2050
|
+
const first = bodyNode.namedChild(0);
|
|
2051
|
+
const hasDocstring = opts.includeDocs !== false && first?.type === "expression_statement" && first.namedChild(0)?.type === "string";
|
|
2052
|
+
const docLineStart = first ? content.lastIndexOf("\n", first.startIndex - 1) + 1 : 0;
|
|
2053
|
+
const docIndent = first ? content.slice(docLineStart, first.startIndex) : "";
|
|
2054
|
+
if (hasDocstring && first && /^[ \t]*$/.test(docIndent)) {
|
|
2055
|
+
if (bodyNode.namedChildCount > 1) {
|
|
2056
|
+
replacements.push({
|
|
2057
|
+
start: first.endIndex,
|
|
2058
|
+
end: bodyNode.endIndex,
|
|
2059
|
+
text: `
|
|
2060
|
+
${docIndent}${stub}`
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
} else {
|
|
2064
|
+
replacements.push({
|
|
2065
|
+
start: bodyNode.startIndex,
|
|
2066
|
+
end: bodyNode.endIndex,
|
|
2067
|
+
text: stub
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
1716
2070
|
} else if (lang === "ruby") {
|
|
1717
2071
|
replacements.push({
|
|
1718
2072
|
start: bodyNode.startIndex,
|
|
@@ -1729,8 +2083,7 @@ async function extractTreeSitterSkeleton(content, lang, opts) {
|
|
|
1729
2083
|
});
|
|
1730
2084
|
}
|
|
1731
2085
|
}
|
|
1732
|
-
const
|
|
1733
|
-
const symbolName = nameNode ? nameNode.text : void 0;
|
|
2086
|
+
const symbolName = treeSitterDeclarationName(node);
|
|
1734
2087
|
symbolRanges.push({
|
|
1735
2088
|
name: symbolName,
|
|
1736
2089
|
kind: type.includes("function") ? "function" : "method",
|
|
@@ -1752,12 +2105,12 @@ async function extractTreeSitterSkeleton(content, lang, opts) {
|
|
|
1752
2105
|
}
|
|
1753
2106
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1754
2107
|
const child = node.namedChild(i);
|
|
1755
|
-
if (child) walk2(child);
|
|
2108
|
+
if (child) walk2(child, depth + 1);
|
|
1756
2109
|
}
|
|
1757
2110
|
};
|
|
1758
2111
|
var walk = walk2;
|
|
1759
2112
|
const root = tree.rootNode;
|
|
1760
|
-
walk2(root);
|
|
2113
|
+
walk2(root, 0);
|
|
1761
2114
|
const skeleton = applyReplacements(content, replacements);
|
|
1762
2115
|
return { skeleton, symbolCount, symbols: symbolRanges };
|
|
1763
2116
|
} finally {
|
|
@@ -2116,7 +2469,7 @@ async function generateRepoMap(opts) {
|
|
|
2116
2469
|
return b.row.rank - a.row.rank;
|
|
2117
2470
|
}).map(({ row, absolute }) => ({ absolute, relative: relativeOf(row.file) }));
|
|
2118
2471
|
const known = new Set(candidates.map((c) => c.absolute));
|
|
2119
|
-
const missingFocus = focus.filter((file) => !known.has(file)).map((file) => ({ absolute: file, relative: relativeOf(file) }));
|
|
2472
|
+
const missingFocus = focus.filter((file) => !known.has(file) && isIndexablePath(file)).map((file) => ({ absolute: file, relative: relativeOf(file) }));
|
|
2120
2473
|
const bodyBudget = Math.max(0, charBudget - header.length - 2);
|
|
2121
2474
|
const { sections, files } = await renderSkeletonSections(
|
|
2122
2475
|
[...missingFocus, ...candidates],
|
|
@@ -2138,6 +2491,8 @@ async function generateRepoMap(opts) {
|
|
|
2138
2491
|
}
|
|
2139
2492
|
|
|
2140
2493
|
// src/codebase-index/codebase-repo-map-tool.ts
|
|
2494
|
+
var MIN_MAP_TOKENS = 100;
|
|
2495
|
+
var MAX_MAP_TOKENS = 2e4;
|
|
2141
2496
|
var codebaseRepoMapTool = {
|
|
2142
2497
|
name: "codebase-repo-map",
|
|
2143
2498
|
category: "Project",
|
|
@@ -2151,8 +2506,10 @@ var codebaseRepoMapTool = {
|
|
|
2151
2506
|
type: "object",
|
|
2152
2507
|
properties: {
|
|
2153
2508
|
maxTokens: {
|
|
2154
|
-
type: "
|
|
2155
|
-
description:
|
|
2509
|
+
type: "integer",
|
|
2510
|
+
description: `Maximum token budget (approximate) for the map. Defaults to 1200, range ${MIN_MAP_TOKENS}-${MAX_MAP_TOKENS}.`,
|
|
2511
|
+
minimum: MIN_MAP_TOKENS,
|
|
2512
|
+
maximum: MAX_MAP_TOKENS
|
|
2156
2513
|
},
|
|
2157
2514
|
focusFiles: {
|
|
2158
2515
|
type: "array",
|
|
@@ -2166,10 +2523,31 @@ var codebaseRepoMapTool = {
|
|
|
2166
2523
|
// `status: 'error'` payload was recorded as a successful call).
|
|
2167
2524
|
async execute(input, ctx) {
|
|
2168
2525
|
const projectRoot = ctx.projectRoot ?? ctx.cwd ?? process.cwd();
|
|
2526
|
+
const maxTokens = Number.isFinite(input.maxTokens) ? Math.min(Math.max(Math.trunc(input.maxTokens), MIN_MAP_TOKENS), MAX_MAP_TOKENS) : void 0;
|
|
2527
|
+
let focusFiles;
|
|
2528
|
+
if (input.focusFiles !== void 0) {
|
|
2529
|
+
if (!Array.isArray(input.focusFiles)) {
|
|
2530
|
+
throw new ToolValidationError8({
|
|
2531
|
+
message: "codebase-repo-map: focusFiles must be an array of file paths.",
|
|
2532
|
+
field: "focusFiles"
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
focusFiles = [];
|
|
2536
|
+
for (const file of input.focusFiles) {
|
|
2537
|
+
if (typeof file !== "string" || !file.trim()) {
|
|
2538
|
+
throw new ToolValidationError8({
|
|
2539
|
+
message: "codebase-repo-map: focusFiles entries must be non-empty file paths.",
|
|
2540
|
+
field: "focusFiles"
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
await safeResolveProjectPath(file.trim(), ctx);
|
|
2544
|
+
focusFiles.push(path7.resolve(projectRoot, file.trim()));
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2169
2547
|
const result = await generateRepoMap({
|
|
2170
2548
|
projectRoot,
|
|
2171
|
-
maxTokens
|
|
2172
|
-
focusFiles
|
|
2549
|
+
maxTokens,
|
|
2550
|
+
focusFiles,
|
|
2173
2551
|
// Honour a caller-supplied index location so the map reads the same
|
|
2174
2552
|
// index the other codebase-* tools do.
|
|
2175
2553
|
indexDir: codebaseIndexDirOverride(ctx)
|
|
@@ -2214,8 +2592,10 @@ var codebaseSkeletonTool = {
|
|
|
2214
2592
|
description: "Only include exported/public declarations (defaults to false)."
|
|
2215
2593
|
},
|
|
2216
2594
|
maxFiles: {
|
|
2217
|
-
type: "
|
|
2218
|
-
description: "Max files to include if path is a directory (defaults to 20, max 50)."
|
|
2595
|
+
type: "integer",
|
|
2596
|
+
description: "Max files to include if path is a directory (defaults to 20, max 50).",
|
|
2597
|
+
minimum: 1,
|
|
2598
|
+
maximum: 50
|
|
2219
2599
|
}
|
|
2220
2600
|
},
|
|
2221
2601
|
required: ["path"],
|
|
@@ -2239,7 +2619,8 @@ var codebaseSkeletonTool = {
|
|
|
2239
2619
|
exportsOnly: input.exportsOnly ?? false
|
|
2240
2620
|
};
|
|
2241
2621
|
if (stat3.isDirectory()) {
|
|
2242
|
-
const
|
|
2622
|
+
const requested = Number.isFinite(input.maxFiles) ? Math.trunc(input.maxFiles) : 20;
|
|
2623
|
+
const maxFiles = Math.min(Math.max(1, requested), 50);
|
|
2243
2624
|
const dirResult = await extractDirectorySkeleton({
|
|
2244
2625
|
dirPath: targetPath,
|
|
2245
2626
|
maxFiles,
|
|
@@ -2362,25 +2743,38 @@ var codebaseStatsTool = {
|
|
|
2362
2743
|
|
|
2363
2744
|
// src/codebase-index/codebase-targeted-test-tool.ts
|
|
2364
2745
|
import * as fs7 from "node:fs/promises";
|
|
2365
|
-
import * as
|
|
2746
|
+
import * as path8 from "node:path";
|
|
2747
|
+
import { ToolValidationError as ToolValidationError9 } from "@wrongstack/core/types";
|
|
2366
2748
|
import { toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
|
|
2367
|
-
|
|
2749
|
+
function parseTestCounts(output) {
|
|
2750
|
+
const vitestLine = output.match(/^\s*Tests\s+(.+)$/m)?.[1];
|
|
2751
|
+
const scope = vitestLine ?? output;
|
|
2752
|
+
const last = (re) => {
|
|
2753
|
+
let value;
|
|
2754
|
+
for (const m of scope.matchAll(re)) value = Number(m[1]);
|
|
2755
|
+
return value;
|
|
2756
|
+
};
|
|
2757
|
+
const passed = last(/(\d+)\s+passed/gi);
|
|
2758
|
+
const failed = last(/(\d+)\s+failed/gi);
|
|
2759
|
+
if (passed === void 0 && failed === void 0) return null;
|
|
2760
|
+
return { passed: passed ?? 0, failed: failed ?? 0 };
|
|
2761
|
+
}
|
|
2368
2762
|
async function findConventionTestFiles(projectRoot, sourceRelPath) {
|
|
2369
|
-
const parsed =
|
|
2763
|
+
const parsed = path8.parse(sourceRelPath);
|
|
2370
2764
|
const candidates = [
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2765
|
+
path8.join(parsed.dir, `${parsed.name}.test${parsed.ext}`),
|
|
2766
|
+
path8.join(parsed.dir, `${parsed.name}.spec${parsed.ext}`),
|
|
2767
|
+
path8.join(parsed.dir, `${parsed.name}_test${parsed.ext}`),
|
|
2768
|
+
path8.join(parsed.dir, `test_${parsed.name}${parsed.ext}`),
|
|
2769
|
+
path8.join("tests", `${parsed.name}.test${parsed.ext}`),
|
|
2770
|
+
path8.join("tests", `${parsed.name}.spec${parsed.ext}`),
|
|
2771
|
+
path8.join("test", `${parsed.name}.test${parsed.ext}`),
|
|
2772
|
+
path8.join(parsed.dir, "__tests__", `${parsed.name}.test${parsed.ext}`),
|
|
2773
|
+
path8.join(parsed.dir, "__tests__", `${parsed.name}${parsed.ext}`)
|
|
2380
2774
|
];
|
|
2381
2775
|
const found = [];
|
|
2382
2776
|
for (const cand of candidates) {
|
|
2383
|
-
const absPath =
|
|
2777
|
+
const absPath = path8.resolve(projectRoot, cand);
|
|
2384
2778
|
try {
|
|
2385
2779
|
const st = await fs7.stat(absPath);
|
|
2386
2780
|
if (st.isFile()) {
|
|
@@ -2391,6 +2785,35 @@ async function findConventionTestFiles(projectRoot, sourceRelPath) {
|
|
|
2391
2785
|
}
|
|
2392
2786
|
return found;
|
|
2393
2787
|
}
|
|
2788
|
+
var IndexDiscoveryError = class extends Error {
|
|
2789
|
+
name = "IndexDiscoveryError";
|
|
2790
|
+
};
|
|
2791
|
+
async function selectTestRunner(projectRoot, suites) {
|
|
2792
|
+
const runnerOf = (suite) => suite.endsWith("_test.go") ? "go" : suite.endsWith(".py") ? "pytest" : "js";
|
|
2793
|
+
const runners = new Set(suites.map(runnerOf));
|
|
2794
|
+
if (runners.size > 1) {
|
|
2795
|
+
throw new ToolValidationError9({
|
|
2796
|
+
message: `codebase-targeted-test: the suites need different runners (${[...runners].join(", ")}): ${suites.join(", ")}. Run each language's suites in a separate call.`,
|
|
2797
|
+
field: "testFiles"
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
const runner = runners.values().next().value;
|
|
2801
|
+
if (runner === "go") {
|
|
2802
|
+
const packages = [...new Set(suites.map((s) => `./${path8.posix.dirname(s)}`))];
|
|
2803
|
+
return { cmd: "go", args: ["test", ...packages.map((p) => p === "./." ? "." : p)] };
|
|
2804
|
+
}
|
|
2805
|
+
if (runner === "pytest") return { cmd: "pytest", args: [...suites] };
|
|
2806
|
+
return { cmd: "npx", args: [...await jsRunnerArgs(projectRoot), ...suites] };
|
|
2807
|
+
}
|
|
2808
|
+
async function jsRunnerArgs(projectRoot) {
|
|
2809
|
+
try {
|
|
2810
|
+
const manifest = JSON.parse(await fs7.readFile(path8.join(projectRoot, "package.json"), "utf8"));
|
|
2811
|
+
const deps = { ...manifest?.dependencies, ...manifest?.devDependencies };
|
|
2812
|
+
if ("jest" in deps && !("vitest" in deps)) return ["jest"];
|
|
2813
|
+
} catch {
|
|
2814
|
+
}
|
|
2815
|
+
return ["vitest", "run"];
|
|
2816
|
+
}
|
|
2394
2817
|
var codebaseTargetedTestTool = {
|
|
2395
2818
|
name: "codebase-targeted-test",
|
|
2396
2819
|
category: "Code Quality",
|
|
@@ -2423,36 +2846,72 @@ var codebaseTargetedTestTool = {
|
|
|
2423
2846
|
async execute(input, ctx, execOpts) {
|
|
2424
2847
|
const projectRoot = ctx.projectRoot ?? ctx.cwd ?? process.cwd();
|
|
2425
2848
|
const suitesSet = /* @__PURE__ */ new Set();
|
|
2426
|
-
|
|
2427
|
-
if (
|
|
2428
|
-
|
|
2849
|
+
const toProjectRelative = async (raw, field) => {
|
|
2850
|
+
if (typeof raw !== "string" || !raw.trim() || raw.trim().startsWith("-")) {
|
|
2851
|
+
throw new ToolValidationError9({
|
|
2852
|
+
message: `codebase-targeted-test: invalid path ${JSON.stringify(raw)} \u2014 expected a project file path, not a flag.`,
|
|
2853
|
+
field
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
const resolved = await safeResolveProjectPath(raw.trim(), ctx);
|
|
2857
|
+
return path8.relative(projectRoot, resolved).replace(/\\/g, "/");
|
|
2858
|
+
};
|
|
2859
|
+
const explicitSuites = [];
|
|
2860
|
+
if (input.testFiles !== void 0) {
|
|
2861
|
+
if (!Array.isArray(input.testFiles)) {
|
|
2862
|
+
throw new ToolValidationError9({
|
|
2863
|
+
message: "codebase-targeted-test: testFiles must be an array of file paths.",
|
|
2864
|
+
field: "testFiles"
|
|
2865
|
+
});
|
|
2429
2866
|
}
|
|
2430
|
-
|
|
2867
|
+
for (const tf of input.testFiles)
|
|
2868
|
+
explicitSuites.push(await toProjectRelative(tf, "testFiles"));
|
|
2869
|
+
}
|
|
2870
|
+
const sourceFile = input.file !== void 0 ? await toProjectRelative(input.file, "file") : void 0;
|
|
2871
|
+
if (explicitSuites.length === 0 && !input.symbol?.trim() && sourceFile === void 0) {
|
|
2872
|
+
throw new ToolValidationError9({
|
|
2873
|
+
message: "codebase-targeted-test: pass at least one of `symbol`, `file`, or `testFiles`.",
|
|
2874
|
+
field: "symbol"
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2877
|
+
const symbol = input.symbol?.trim();
|
|
2878
|
+
let discoveryError;
|
|
2879
|
+
try {
|
|
2880
|
+
for (const tf of explicitSuites) suitesSet.add(tf);
|
|
2881
|
+
if (symbol) {
|
|
2431
2882
|
try {
|
|
2432
2883
|
const indexDir = codebaseIndexDirOverride(ctx);
|
|
2433
2884
|
const serviced = await incomingCallsService({
|
|
2434
2885
|
projectRoot,
|
|
2435
2886
|
indexDir,
|
|
2436
|
-
symbol
|
|
2887
|
+
symbol,
|
|
2437
2888
|
file: input.file,
|
|
2438
2889
|
limit: 100,
|
|
2439
2890
|
transitive: true
|
|
2440
2891
|
});
|
|
2441
2892
|
for (const site of serviced.calls) {
|
|
2442
|
-
const relPath =
|
|
2443
|
-
if (
|
|
2893
|
+
const relPath = path8.relative(projectRoot, site.symbol.file).replace(/\\/g, "/");
|
|
2894
|
+
if (relPath.startsWith("../") || path8.isAbsolute(relPath)) continue;
|
|
2895
|
+
if (isTestFilePath(relPath)) {
|
|
2444
2896
|
suitesSet.add(relPath);
|
|
2445
2897
|
}
|
|
2446
2898
|
}
|
|
2447
|
-
} catch {
|
|
2899
|
+
} catch (err) {
|
|
2900
|
+
discoveryError = err;
|
|
2448
2901
|
}
|
|
2449
2902
|
}
|
|
2450
|
-
if (
|
|
2451
|
-
const convFiles = await findConventionTestFiles(projectRoot,
|
|
2903
|
+
if (sourceFile !== void 0) {
|
|
2904
|
+
const convFiles = await findConventionTestFiles(projectRoot, sourceFile);
|
|
2452
2905
|
for (const cf of convFiles) suitesSet.add(cf);
|
|
2453
2906
|
}
|
|
2454
2907
|
const discoveredSuites = [...suitesSet];
|
|
2455
2908
|
if (discoveredSuites.length === 0) {
|
|
2909
|
+
if (discoveryError !== void 0) {
|
|
2910
|
+
throw new IndexDiscoveryError(
|
|
2911
|
+
`Could not discover tests for '${symbol}': the codebase index query failed (${toErrorMessage7(discoveryError)}). Run codebase-index, or pass \`file\`/\`testFiles\`.`,
|
|
2912
|
+
{ cause: discoveryError }
|
|
2913
|
+
);
|
|
2914
|
+
}
|
|
2456
2915
|
return {
|
|
2457
2916
|
status: "no_tests_found",
|
|
2458
2917
|
discoveredSuites: [],
|
|
@@ -2464,15 +2923,10 @@ var codebaseTargetedTestTool = {
|
|
|
2464
2923
|
};
|
|
2465
2924
|
}
|
|
2466
2925
|
const start = Date.now();
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
runnerArgs = discoveredSuites;
|
|
2472
|
-
} else if (discoveredSuites.every((s) => s.endsWith("_test.go"))) {
|
|
2473
|
-
runnerCmd = "go";
|
|
2474
|
-
runnerArgs = ["test", ...discoveredSuites];
|
|
2475
|
-
}
|
|
2926
|
+
const { cmd: runnerCmd, args: runnerArgs } = await selectTestRunner(
|
|
2927
|
+
projectRoot,
|
|
2928
|
+
discoveredSuites
|
|
2929
|
+
);
|
|
2476
2930
|
const signal = execOpts?.signal ?? ctx.signal ?? new AbortController().signal;
|
|
2477
2931
|
const gen = spawnStream({
|
|
2478
2932
|
cmd: runnerCmd,
|
|
@@ -2487,16 +2941,15 @@ var codebaseTargetedTestTool = {
|
|
|
2487
2941
|
}
|
|
2488
2942
|
const streamRes = genResult.value;
|
|
2489
2943
|
const durationMs = Date.now() - start;
|
|
2490
|
-
const
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
{ maxBytes: 4e3 }
|
|
2494
|
-
);
|
|
2944
|
+
const rawOutput = `${streamRes.stdout}
|
|
2945
|
+
${streamRes.stderr}`.trim();
|
|
2946
|
+
const combinedOutput = normalizeCommandOutput(rawOutput, { maxBytes: 4e3 });
|
|
2495
2947
|
const isPassed = streamRes.exitCode === 0;
|
|
2496
|
-
const
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
const
|
|
2948
|
+
const counts = parseTestCounts(
|
|
2949
|
+
normalizeCommandOutput(rawOutput, { maxBytes: Number.MAX_SAFE_INTEGER })
|
|
2950
|
+
);
|
|
2951
|
+
const passed = counts ? counts.passed : isPassed ? discoveredSuites.length : 0;
|
|
2952
|
+
const failed = counts ? counts.failed : isPassed ? 0 : 1;
|
|
2500
2953
|
const testsRun = passed + failed;
|
|
2501
2954
|
return {
|
|
2502
2955
|
status: isPassed ? "passed" : "failed",
|
|
@@ -2508,6 +2961,7 @@ ${streamRes.stderr}`.trim(),
|
|
|
2508
2961
|
output: combinedOutput
|
|
2509
2962
|
};
|
|
2510
2963
|
} catch (err) {
|
|
2964
|
+
if (err instanceof IndexDiscoveryError || err instanceof ToolValidationError9) throw err;
|
|
2511
2965
|
const suites = suitesSet.size > 0 ? ` (discovered suites: ${[...suitesSet].join(", ")})` : "";
|
|
2512
2966
|
throw new Error(`Targeted test run failed to execute: ${toErrorMessage7(err)}${suites}`, {
|
|
2513
2967
|
cause: err
|
|
@@ -2518,8 +2972,8 @@ ${streamRes.stderr}`.trim(),
|
|
|
2518
2972
|
|
|
2519
2973
|
// src/codebase-index/dead-code-scan.ts
|
|
2520
2974
|
import * as fs8 from "node:fs";
|
|
2521
|
-
import * as
|
|
2522
|
-
import { ToolValidationError as
|
|
2975
|
+
import * as path9 from "node:path";
|
|
2976
|
+
import { ToolValidationError as ToolValidationError10 } from "@wrongstack/core/types";
|
|
2523
2977
|
var deadCodeScanTool = {
|
|
2524
2978
|
name: "dead-code-scan",
|
|
2525
2979
|
category: "Project",
|
|
@@ -2557,11 +3011,11 @@ var deadCodeScanTool = {
|
|
|
2557
3011
|
userEntryPoints = [];
|
|
2558
3012
|
for (const ep of input.entryPoints) {
|
|
2559
3013
|
const resolved = await safeResolveProjectPath(
|
|
2560
|
-
|
|
3014
|
+
path9.isAbsolute(ep) ? ep : path9.resolve(projectRoot, ep),
|
|
2561
3015
|
ctx
|
|
2562
3016
|
);
|
|
2563
3017
|
if (!fs8.existsSync(resolved)) {
|
|
2564
|
-
throw new
|
|
3018
|
+
throw new ToolValidationError10({
|
|
2565
3019
|
message: `dead-code-scan: entry point not found: "${ep}"`,
|
|
2566
3020
|
field: "entryPoints"
|
|
2567
3021
|
});
|
|
@@ -2595,8 +3049,8 @@ function tryReadJson(filePath) {
|
|
|
2595
3049
|
}
|
|
2596
3050
|
}
|
|
2597
3051
|
function resolveAgainst(base, relative12) {
|
|
2598
|
-
if (
|
|
2599
|
-
return
|
|
3052
|
+
if (path9.isAbsolute(relative12)) return relative12;
|
|
3053
|
+
return path9.resolve(base, relative12);
|
|
2600
3054
|
}
|
|
2601
3055
|
function discoverEntryPoints(projectRoot, userEntryPoints) {
|
|
2602
3056
|
const entries = /* @__PURE__ */ new Set();
|
|
@@ -2606,7 +3060,7 @@ function discoverEntryPoints(projectRoot, userEntryPoints) {
|
|
|
2606
3060
|
if (fs8.existsSync(resolved)) entries.add(resolved);
|
|
2607
3061
|
}
|
|
2608
3062
|
}
|
|
2609
|
-
const rootPkg = tryReadJson(
|
|
3063
|
+
const rootPkg = tryReadJson(path9.join(projectRoot, "package.json"));
|
|
2610
3064
|
if (rootPkg) {
|
|
2611
3065
|
addPkgJsonEntryPoints(projectRoot, rootPkg, entries);
|
|
2612
3066
|
}
|
|
@@ -2620,34 +3074,34 @@ function discoverEntryPoints(projectRoot, userEntryPoints) {
|
|
|
2620
3074
|
workspaces = [];
|
|
2621
3075
|
}
|
|
2622
3076
|
for (const wsDir of workspaces) {
|
|
2623
|
-
const pkgJsonPath =
|
|
3077
|
+
const pkgJsonPath = path9.join(wsDir, "package.json");
|
|
2624
3078
|
const pkg = tryReadJson(pkgJsonPath);
|
|
2625
3079
|
if (pkg) {
|
|
2626
3080
|
addPkgJsonEntryPoints(wsDir, pkg, entries);
|
|
2627
|
-
const convention1 =
|
|
3081
|
+
const convention1 = path9.join(wsDir, "src", "index.ts");
|
|
2628
3082
|
if (fs8.existsSync(convention1)) entries.add(convention1);
|
|
2629
|
-
const convention2 =
|
|
3083
|
+
const convention2 = path9.join(wsDir, "src", "main.ts");
|
|
2630
3084
|
if (fs8.existsSync(convention2)) entries.add(convention2);
|
|
2631
|
-
const convention3 =
|
|
3085
|
+
const convention3 = path9.join(wsDir, "index.ts");
|
|
2632
3086
|
if (fs8.existsSync(convention3)) entries.add(convention3);
|
|
2633
3087
|
}
|
|
2634
3088
|
}
|
|
2635
3089
|
if (!rootPkg || !workspaces.length) {
|
|
2636
3090
|
for (const name of ["src/index.ts", "src/main.ts", "index.ts"]) {
|
|
2637
|
-
const convention =
|
|
3091
|
+
const convention = path9.join(projectRoot, name);
|
|
2638
3092
|
if (fs8.existsSync(convention)) entries.add(convention);
|
|
2639
3093
|
}
|
|
2640
3094
|
}
|
|
2641
3095
|
return [...entries];
|
|
2642
3096
|
}
|
|
2643
3097
|
var BUILD_OUTPUT_DIRS = ["dist", "out", "build", "release"];
|
|
2644
|
-
var BUILD_OUTPUT_DIR_NAMES = BUILD_OUTPUT_DIRS.map((d) => `${
|
|
3098
|
+
var BUILD_OUTPUT_DIR_NAMES = BUILD_OUTPUT_DIRS.map((d) => `${path9.sep}${d}${path9.sep}`);
|
|
2645
3099
|
function trySourceEquivalent(resolved) {
|
|
2646
|
-
resolved = resolved.replace(/[/\\]/g,
|
|
3100
|
+
resolved = resolved.replace(/[/\\]/g, path9.sep);
|
|
2647
3101
|
for (const marker of BUILD_OUTPUT_DIR_NAMES) {
|
|
2648
3102
|
const idx = resolved.indexOf(marker);
|
|
2649
3103
|
if (idx === -1) continue;
|
|
2650
|
-
const base = resolved.replace(marker, `${
|
|
3104
|
+
const base = resolved.replace(marker, `${path9.sep}src${path9.sep}`);
|
|
2651
3105
|
const candidate = base.replace(/\.(js|mjs|cjs)$/, ".ts");
|
|
2652
3106
|
if (candidate !== base && fs8.existsSync(candidate)) {
|
|
2653
3107
|
return candidate;
|
|
@@ -2693,44 +3147,48 @@ function addPkgJsonEntryPoints(pkgDir, pkg, entries) {
|
|
|
2693
3147
|
tryAddEntryPath(pkgDir, pkg[key], entries);
|
|
2694
3148
|
}
|
|
2695
3149
|
}
|
|
2696
|
-
const
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
3150
|
+
const targets = [];
|
|
3151
|
+
collectExportTargets(pkg.exports, targets, 0);
|
|
3152
|
+
for (const target of targets) tryAddEntryPath(pkgDir, target, entries);
|
|
3153
|
+
}
|
|
3154
|
+
function collectExportTargets(value, out, depth) {
|
|
3155
|
+
if (depth > 8) return;
|
|
3156
|
+
if (typeof value === "string") {
|
|
3157
|
+
out.push(value);
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
if (Array.isArray(value)) {
|
|
3161
|
+
for (const item of value) collectExportTargets(item, out, depth + 1);
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
3164
|
+
if (value && typeof value === "object") {
|
|
3165
|
+
for (const item of Object.values(value)) collectExportTargets(item, out, depth + 1);
|
|
2709
3166
|
}
|
|
2710
3167
|
}
|
|
2711
3168
|
function expandGlobPattern(entry, projectRoot) {
|
|
2712
3169
|
const dirs = [];
|
|
2713
3170
|
if (entry.includes("*")) {
|
|
2714
3171
|
const base = entry.replace(/\/\*+$/, "");
|
|
2715
|
-
const baseDir =
|
|
3172
|
+
const baseDir = path9.resolve(projectRoot, base);
|
|
2716
3173
|
try {
|
|
2717
3174
|
const children = fs8.readdirSync(baseDir, { withFileTypes: true });
|
|
2718
3175
|
for (const child of children) {
|
|
2719
3176
|
if (child.isDirectory()) {
|
|
2720
|
-
dirs.push(
|
|
3177
|
+
dirs.push(path9.join(baseDir, child.name));
|
|
2721
3178
|
}
|
|
2722
3179
|
}
|
|
2723
3180
|
} catch {
|
|
2724
3181
|
}
|
|
2725
3182
|
} else {
|
|
2726
|
-
dirs.push(
|
|
3183
|
+
dirs.push(path9.resolve(projectRoot, entry));
|
|
2727
3184
|
}
|
|
2728
3185
|
return dirs;
|
|
2729
3186
|
}
|
|
2730
3187
|
function extractWorkspaceGlobs(pkg, projectRoot) {
|
|
2731
3188
|
const dirs = [];
|
|
2732
|
-
const
|
|
2733
|
-
|
|
3189
|
+
const raw = pkg.workspaces;
|
|
3190
|
+
const workspaces = Array.isArray(raw) ? raw : raw && typeof raw === "object" && Array.isArray(raw.packages) ? raw.packages : void 0;
|
|
3191
|
+
if (workspaces) {
|
|
2734
3192
|
for (const entry of workspaces) {
|
|
2735
3193
|
if (typeof entry === "string") {
|
|
2736
3194
|
dirs.push(...expandGlobPattern(entry, projectRoot));
|
|
@@ -2740,7 +3198,7 @@ function extractWorkspaceGlobs(pkg, projectRoot) {
|
|
|
2740
3198
|
return dirs;
|
|
2741
3199
|
}
|
|
2742
3200
|
function extractPnpmWorkspaceDirs(projectRoot) {
|
|
2743
|
-
const yamlPath =
|
|
3201
|
+
const yamlPath = path9.join(projectRoot, "pnpm-workspace.yaml");
|
|
2744
3202
|
if (!fs8.existsSync(yamlPath)) return [];
|
|
2745
3203
|
try {
|
|
2746
3204
|
const content = fs8.readFileSync(yamlPath, "utf8");
|
|
@@ -2775,10 +3233,17 @@ function extractPnpmWorkspaceDirs(projectRoot) {
|
|
|
2775
3233
|
return [];
|
|
2776
3234
|
}
|
|
2777
3235
|
}
|
|
3236
|
+
var NON_CODE_LANGS = /* @__PURE__ */ new Set(["json", "yaml", "toml", "md"]);
|
|
3237
|
+
var RECEIVER_ONLY_MEMBER_CALL_LANGS = /* @__PURE__ */ new Set([
|
|
3238
|
+
"ts",
|
|
3239
|
+
"tsx",
|
|
3240
|
+
"js",
|
|
3241
|
+
"jsx"
|
|
3242
|
+
]);
|
|
2778
3243
|
function resolveModulePath(importerPath, moduleSpecifier, indexedFiles) {
|
|
2779
3244
|
if (!moduleSpecifier.startsWith(".")) return [];
|
|
2780
|
-
const dir =
|
|
2781
|
-
const base =
|
|
3245
|
+
const dir = path9.dirname(importerPath);
|
|
3246
|
+
const base = path9.resolve(dir, moduleSpecifier);
|
|
2782
3247
|
const results = [];
|
|
2783
3248
|
const stripped = base.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, "");
|
|
2784
3249
|
const skipBase = stripped !== base && /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(base);
|
|
@@ -2790,18 +3255,18 @@ function resolveModulePath(importerPath, moduleSpecifier, indexedFiles) {
|
|
|
2790
3255
|
if (indexedFiles.has(candidate + ".jsx")) results.push(candidate + ".jsx");
|
|
2791
3256
|
if (indexedFiles.has(candidate + ".mjs")) results.push(candidate + ".mjs");
|
|
2792
3257
|
if (indexedFiles.has(candidate + ".cjs")) results.push(candidate + ".cjs");
|
|
2793
|
-
if (indexedFiles.has(
|
|
2794
|
-
results.push(
|
|
2795
|
-
if (indexedFiles.has(
|
|
2796
|
-
results.push(
|
|
2797
|
-
if (indexedFiles.has(
|
|
2798
|
-
results.push(
|
|
2799
|
-
if (indexedFiles.has(
|
|
2800
|
-
results.push(
|
|
2801
|
-
if (indexedFiles.has(
|
|
2802
|
-
results.push(
|
|
2803
|
-
if (indexedFiles.has(
|
|
2804
|
-
results.push(
|
|
3258
|
+
if (indexedFiles.has(path9.join(candidate, "index.ts")))
|
|
3259
|
+
results.push(path9.join(candidate, "index.ts"));
|
|
3260
|
+
if (indexedFiles.has(path9.join(candidate, "index.tsx")))
|
|
3261
|
+
results.push(path9.join(candidate, "index.tsx"));
|
|
3262
|
+
if (indexedFiles.has(path9.join(candidate, "index.js")))
|
|
3263
|
+
results.push(path9.join(candidate, "index.js"));
|
|
3264
|
+
if (indexedFiles.has(path9.join(candidate, "index.jsx")))
|
|
3265
|
+
results.push(path9.join(candidate, "index.jsx"));
|
|
3266
|
+
if (indexedFiles.has(path9.join(candidate, "index.mjs")))
|
|
3267
|
+
results.push(path9.join(candidate, "index.mjs"));
|
|
3268
|
+
if (indexedFiles.has(path9.join(candidate, "index.cjs")))
|
|
3269
|
+
results.push(path9.join(candidate, "index.cjs"));
|
|
2805
3270
|
}
|
|
2806
3271
|
return [...new Set(results)];
|
|
2807
3272
|
}
|
|
@@ -2833,7 +3298,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
|
|
|
2833
3298
|
symbolById.set(s.id, s);
|
|
2834
3299
|
}
|
|
2835
3300
|
const discoveredFiles = discoverEntryPoints(projectRoot, opts.userEntryPoints);
|
|
2836
|
-
const entryFileSet = new Set(discoveredFiles.map((f) =>
|
|
3301
|
+
const entryFileSet = new Set(discoveredFiles.map((f) => path9.resolve(f)));
|
|
2837
3302
|
const indexedFiles = /* @__PURE__ */ new Set();
|
|
2838
3303
|
for (const s of allSymbols) indexedFiles.add(s.file);
|
|
2839
3304
|
for (const fm of store.getAllFileMetas()) indexedFiles.add(fm.file);
|
|
@@ -2904,12 +3369,31 @@ function runDeadCodeScan(projectRoot, opts = {}) {
|
|
|
2904
3369
|
const dead = [];
|
|
2905
3370
|
const symbolsByFile = /* @__PURE__ */ new Map();
|
|
2906
3371
|
const usedFiles = /* @__PURE__ */ new Set();
|
|
3372
|
+
const liveScopesByFile = /* @__PURE__ */ new Map();
|
|
3373
|
+
for (const s of allSymbols) {
|
|
3374
|
+
if (!alive.has(s.id)) continue;
|
|
3375
|
+
const scopePath = s.scope ? `${s.scope}.${s.name}` : s.name;
|
|
3376
|
+
let scopes = liveScopesByFile.get(s.file);
|
|
3377
|
+
if (!scopes) {
|
|
3378
|
+
scopes = /* @__PURE__ */ new Set();
|
|
3379
|
+
liveScopesByFile.set(s.file, scopes);
|
|
3380
|
+
}
|
|
3381
|
+
scopes.add(scopePath);
|
|
3382
|
+
}
|
|
3383
|
+
const isMemberOfLiveDeclaration = (s) => {
|
|
3384
|
+
if (!s.scope) return false;
|
|
3385
|
+
const lang = detectLang(s.file);
|
|
3386
|
+
if (lang === null || !RECEIVER_ONLY_MEMBER_CALL_LANGS.has(lang)) return false;
|
|
3387
|
+
return liveScopesByFile.get(s.file)?.has(s.scope) ?? false;
|
|
3388
|
+
};
|
|
2907
3389
|
for (const s of allSymbols) {
|
|
2908
|
-
if (alive.has(s.id)) {
|
|
3390
|
+
if (alive.has(s.id) || isMemberOfLiveDeclaration(s)) {
|
|
2909
3391
|
usedFiles.add(s.file);
|
|
2910
3392
|
continue;
|
|
2911
3393
|
}
|
|
2912
3394
|
if (s.kind === "parameter") continue;
|
|
3395
|
+
const symbolLang = detectLang(s.file);
|
|
3396
|
+
if (symbolLang !== null && NON_CODE_LANGS.has(symbolLang)) continue;
|
|
2913
3397
|
if (s.kind === "let" || s.kind === "var") {
|
|
2914
3398
|
continue;
|
|
2915
3399
|
}
|
|
@@ -2931,14 +3415,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
|
|
|
2931
3415
|
}
|
|
2932
3416
|
const deadFiles = [];
|
|
2933
3417
|
for (const [file, syms] of symbolsByFile) {
|
|
2934
|
-
|
|
2935
|
-
for (const s of allSymbols) {
|
|
2936
|
-
if (s.file === file && alive.has(s.id)) {
|
|
2937
|
-
anyAlive = true;
|
|
2938
|
-
break;
|
|
2939
|
-
}
|
|
2940
|
-
}
|
|
2941
|
-
if (!anyAlive) {
|
|
3418
|
+
if (!usedFiles.has(file)) {
|
|
2942
3419
|
deadFiles.push({
|
|
2943
3420
|
file,
|
|
2944
3421
|
symbolCount: syms.length,
|
|
@@ -2949,7 +3426,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
|
|
|
2949
3426
|
const deadPackages = [];
|
|
2950
3427
|
const pkgEntries = findPackageEntries(projectRoot);
|
|
2951
3428
|
for (const [pkgName, pkgDir] of pkgEntries) {
|
|
2952
|
-
const pkgFiles = allSymbols.filter((s) => s.file.startsWith(pkgDir +
|
|
3429
|
+
const pkgFiles = allSymbols.filter((s) => s.file.startsWith(pkgDir + path9.sep));
|
|
2953
3430
|
if (pkgFiles.length === 0) continue;
|
|
2954
3431
|
const pkgUsed = pkgFiles.filter((s) => alive.has(s.id));
|
|
2955
3432
|
if (pkgUsed.length === 0) {
|
|
@@ -2985,7 +3462,7 @@ function runDeadCodeScan(projectRoot, opts = {}) {
|
|
|
2985
3462
|
}
|
|
2986
3463
|
function findPackageEntries(projectRoot) {
|
|
2987
3464
|
const pkgMap = /* @__PURE__ */ new Map();
|
|
2988
|
-
const rootPkg = tryReadJson(
|
|
3465
|
+
const rootPkg = tryReadJson(path9.join(projectRoot, "package.json"));
|
|
2989
3466
|
if (rootPkg && typeof rootPkg.name === "string") {
|
|
2990
3467
|
pkgMap.set(rootPkg.name, projectRoot);
|
|
2991
3468
|
}
|
|
@@ -2995,7 +3472,7 @@ function findPackageEntries(projectRoot) {
|
|
|
2995
3472
|
wsDirs = extractPnpmWorkspaceDirs(projectRoot);
|
|
2996
3473
|
}
|
|
2997
3474
|
for (const wsDir of wsDirs) {
|
|
2998
|
-
const wsPkg = tryReadJson(
|
|
3475
|
+
const wsPkg = tryReadJson(path9.join(wsDir, "package.json"));
|
|
2999
3476
|
if (wsPkg && typeof wsPkg.name === "string") {
|
|
3000
3477
|
pkgMap.set(wsPkg.name, wsDir);
|
|
3001
3478
|
}
|
|
@@ -3006,13 +3483,13 @@ function findPackageEntries(projectRoot) {
|
|
|
3006
3483
|
|
|
3007
3484
|
// src/codebase-index/atlas-brief.ts
|
|
3008
3485
|
import { existsSync as existsSync4 } from "node:fs";
|
|
3009
|
-
import * as
|
|
3486
|
+
import * as path11 from "node:path";
|
|
3010
3487
|
|
|
3011
3488
|
// src/codebase-index/atlas-projection.ts
|
|
3012
3489
|
import { createHash } from "node:crypto";
|
|
3013
3490
|
import { existsSync as existsSync3 } from "node:fs";
|
|
3014
3491
|
import * as fs9 from "node:fs/promises";
|
|
3015
|
-
import * as
|
|
3492
|
+
import * as path10 from "node:path";
|
|
3016
3493
|
|
|
3017
3494
|
// src/codebase-index/atlas-export.ts
|
|
3018
3495
|
var EXPORT_PACKAGE_LIMIT = 40;
|
|
@@ -3224,7 +3701,7 @@ ${renderFileRows(document)}
|
|
|
3224
3701
|
}
|
|
3225
3702
|
|
|
3226
3703
|
// src/codebase-index/atlas-projection.ts
|
|
3227
|
-
var ATLAS_DIR =
|
|
3704
|
+
var ATLAS_DIR = path10.join(".wrongstack", "atlas");
|
|
3228
3705
|
var ATLAS_JSON = "atlas.json";
|
|
3229
3706
|
var ATLAS_MARKDOWN = "ATLAS.md";
|
|
3230
3707
|
var ATLAS_MANIFEST = "manifest.json";
|
|
@@ -3237,9 +3714,27 @@ var RANK_PRECISION = 4;
|
|
|
3237
3714
|
function round(value) {
|
|
3238
3715
|
return Number(value.toFixed(RANK_PRECISION));
|
|
3239
3716
|
}
|
|
3717
|
+
function compareText(a, b) {
|
|
3718
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
3719
|
+
}
|
|
3720
|
+
function atlasPackageName(packageLabel, relativePath) {
|
|
3721
|
+
return packageLabel || relativePath.split("/")[0] || "(root)";
|
|
3722
|
+
}
|
|
3723
|
+
function atlasPackageFileCounts(store, relativeOf) {
|
|
3724
|
+
const labels = store.getFilePackages();
|
|
3725
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3726
|
+
for (const meta of store.getAllFileMetas()) {
|
|
3727
|
+
const name = atlasPackageName(labels.get(meta.file), relativeOf(meta.file));
|
|
3728
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
3729
|
+
}
|
|
3730
|
+
return counts;
|
|
3731
|
+
}
|
|
3732
|
+
function markdownCell(value) {
|
|
3733
|
+
return value.replace(/\s+/g, " ").replace(/\|/g, "\\|").trim();
|
|
3734
|
+
}
|
|
3240
3735
|
function relativeFactory(projectRoot) {
|
|
3241
3736
|
return (file) => {
|
|
3242
|
-
const relative12 =
|
|
3737
|
+
const relative12 = path10.relative(projectRoot, file);
|
|
3243
3738
|
if (!relative12 || relative12.startsWith("..")) return posixIndexPath(file);
|
|
3244
3739
|
return posixIndexPath(relative12);
|
|
3245
3740
|
};
|
|
@@ -3274,13 +3769,15 @@ function renderMarkdown(document) {
|
|
|
3274
3769
|
"| --- | ---: | --- | ---: |"
|
|
3275
3770
|
];
|
|
3276
3771
|
for (const pkg of document.packages) {
|
|
3277
|
-
lines.push(
|
|
3772
|
+
lines.push(
|
|
3773
|
+
`| ${markdownCell(pkg.name)} | ${pkg.files} | \`${markdownCell(pkg.hub)}\` | ${pkg.rank.toFixed(4)} |`
|
|
3774
|
+
);
|
|
3278
3775
|
}
|
|
3279
3776
|
const described = document.packages.filter((pkg) => pkg.summary !== void 0);
|
|
3280
3777
|
if (described.length > 0) {
|
|
3281
3778
|
lines.push("", "## Subsystems", "");
|
|
3282
3779
|
for (const pkg of described) {
|
|
3283
|
-
lines.push(`- **${pkg.name}** \u2014 ${pkg.summary}`);
|
|
3780
|
+
lines.push(`- **${markdownCell(pkg.name)}** \u2014 ${markdownCell(pkg.summary ?? "")}`);
|
|
3284
3781
|
}
|
|
3285
3782
|
}
|
|
3286
3783
|
lines.push(
|
|
@@ -3293,9 +3790,9 @@ function renderMarkdown(document) {
|
|
|
3293
3790
|
"| ---: | --- | ---: | ---: | --- |"
|
|
3294
3791
|
);
|
|
3295
3792
|
for (const file of document.files.slice(0, MARKDOWN_FILE_LIMIT)) {
|
|
3296
|
-
const declarations = file.symbols.slice(0, MARKDOWN_SYMBOLS_PER_FILE).map((symbol) => `\`${symbol.name}\``).join(", ");
|
|
3793
|
+
const declarations = file.symbols.slice(0, MARKDOWN_SYMBOLS_PER_FILE).map((symbol) => `\`${markdownCell(symbol.name)}\``).join(", ");
|
|
3297
3794
|
lines.push(
|
|
3298
|
-
`| ${file.rank.toFixed(4)} | \`${file.path}\` | ${file.inDeg} | ${file.outDeg} | ${declarations} |`
|
|
3795
|
+
`| ${file.rank.toFixed(4)} | \`${markdownCell(file.path)}\` | ${file.inDeg} | ${file.outDeg} | ${declarations} |`
|
|
3299
3796
|
);
|
|
3300
3797
|
}
|
|
3301
3798
|
return `${lines.join("\n").trimEnd()}
|
|
@@ -3304,14 +3801,14 @@ function renderMarkdown(document) {
|
|
|
3304
3801
|
function buildAtlas(store, projectRoot) {
|
|
3305
3802
|
const relativeOf = relativeFactory(projectRoot);
|
|
3306
3803
|
const stats = store.getStats();
|
|
3307
|
-
const packageCounts = store
|
|
3804
|
+
const packageCounts = atlasPackageFileCounts(store, relativeOf);
|
|
3308
3805
|
const ranked = store.getRankedFiles(ATLAS_FILE_LIMIT);
|
|
3309
3806
|
const concepts = store.getReadyConceptSummaries();
|
|
3310
3807
|
const subsystemSummaries = new Map(
|
|
3311
3808
|
store.getSubsystems().map((subsystem) => [subsystem.id, subsystem.summary])
|
|
3312
3809
|
);
|
|
3313
3810
|
const packages = /* @__PURE__ */ new Map();
|
|
3314
|
-
const packageNameOf = (row) => row.package
|
|
3811
|
+
const packageNameOf = (row) => atlasPackageName(row.package, relativeOf(row.file));
|
|
3315
3812
|
for (const row of ranked) {
|
|
3316
3813
|
const relative12 = relativeOf(row.file);
|
|
3317
3814
|
const name = packageNameOf(row);
|
|
@@ -3320,7 +3817,7 @@ function buildAtlas(store, projectRoot) {
|
|
|
3320
3817
|
const summary = subsystemSummaries.get(name);
|
|
3321
3818
|
packages.set(name, {
|
|
3322
3819
|
name,
|
|
3323
|
-
files: packageCounts.get(
|
|
3820
|
+
files: packageCounts.get(name) ?? 0,
|
|
3324
3821
|
hub: relative12,
|
|
3325
3822
|
rank: round(row.rank),
|
|
3326
3823
|
// Absent rather than empty: an un-enriched repository must project the
|
|
@@ -3354,14 +3851,12 @@ function buildAtlas(store, projectRoot) {
|
|
|
3354
3851
|
weight: edge.weight,
|
|
3355
3852
|
refType: edge.refType
|
|
3356
3853
|
})).filter((edge) => known.has(edge.from) && known.has(edge.to)).sort(
|
|
3357
|
-
(a, b) => a.from
|
|
3854
|
+
(a, b) => compareText(a.from, b.from) || compareText(a.to, b.to) || compareText(a.refType, b.refType)
|
|
3358
3855
|
);
|
|
3359
3856
|
const document = {
|
|
3360
3857
|
schema: ATLAS_SCHEMA,
|
|
3361
3858
|
counts: { files: stats.totalFiles, symbols: stats.totalSymbols, packages: packages.size },
|
|
3362
|
-
packages: [...packages.values()].sort(
|
|
3363
|
-
(a, b) => b.rank - a.rank || a.name.localeCompare(b.name)
|
|
3364
|
-
),
|
|
3859
|
+
packages: [...packages.values()].sort((a, b) => b.rank - a.rank || compareText(a.name, b.name)),
|
|
3365
3860
|
edges,
|
|
3366
3861
|
files
|
|
3367
3862
|
};
|
|
@@ -3389,7 +3884,7 @@ function stableJson(value) {
|
|
|
3389
3884
|
}
|
|
3390
3885
|
async function writeAtlas(store, projectRoot) {
|
|
3391
3886
|
const projection = buildAtlas(store, projectRoot);
|
|
3392
|
-
const dir =
|
|
3887
|
+
const dir = path10.join(projectRoot, ATLAS_DIR);
|
|
3393
3888
|
await fs9.mkdir(dir, { recursive: true });
|
|
3394
3889
|
const written = [
|
|
3395
3890
|
[ATLAS_JSON, stableJson(projection.document)],
|
|
@@ -3397,7 +3892,7 @@ async function writeAtlas(store, projectRoot) {
|
|
|
3397
3892
|
[ATLAS_MARKDOWN, projection.markdown]
|
|
3398
3893
|
];
|
|
3399
3894
|
for (const [name, body] of written) {
|
|
3400
|
-
await fs9.writeFile(
|
|
3895
|
+
await fs9.writeFile(path10.join(dir, name), body, "utf8");
|
|
3401
3896
|
}
|
|
3402
3897
|
return {
|
|
3403
3898
|
dir: posixIndexPath(ATLAS_DIR),
|
|
@@ -3418,7 +3913,7 @@ async function checkAtlasFreshness(store, projectRoot) {
|
|
|
3418
3913
|
});
|
|
3419
3914
|
let raw;
|
|
3420
3915
|
try {
|
|
3421
|
-
raw = await fs9.readFile(
|
|
3916
|
+
raw = await fs9.readFile(path10.join(projectRoot, ATLAS_DIR, ATLAS_MANIFEST), "utf8");
|
|
3422
3917
|
} catch {
|
|
3423
3918
|
return stale("missing");
|
|
3424
3919
|
}
|
|
@@ -3464,7 +3959,7 @@ async function checkAtlasFreshness(store, projectRoot) {
|
|
|
3464
3959
|
};
|
|
3465
3960
|
}
|
|
3466
3961
|
function hasIndex(projectRoot, indexDir) {
|
|
3467
|
-
return existsSync3(
|
|
3962
|
+
return existsSync3(path10.join(resolveIndexDir(projectRoot, indexDir), "index.db"));
|
|
3468
3963
|
}
|
|
3469
3964
|
async function withStore(projectRoot, indexDir, job) {
|
|
3470
3965
|
if (!hasIndex(projectRoot, indexDir)) return { indexed: false };
|
|
@@ -3486,7 +3981,7 @@ function exportProjectAtlasHtml(projectRoot, opts = {}) {
|
|
|
3486
3981
|
projectRoot,
|
|
3487
3982
|
opts.indexDir,
|
|
3488
3983
|
(store) => renderAtlasHtml(buildAtlas(store, projectRoot).document, {
|
|
3489
|
-
projectName: opts.projectName ??
|
|
3984
|
+
projectName: opts.projectName ?? path10.basename(projectRoot)
|
|
3490
3985
|
})
|
|
3491
3986
|
);
|
|
3492
3987
|
}
|
|
@@ -3502,7 +3997,7 @@ function trim(text) {
|
|
|
3502
3997
|
}
|
|
3503
3998
|
async function buildAtlasBrief(store, projectRoot) {
|
|
3504
3999
|
const relativeOf = (file) => {
|
|
3505
|
-
const relative12 =
|
|
4000
|
+
const relative12 = path11.relative(projectRoot, file);
|
|
3506
4001
|
return posixIndexPath(relative12 && !relative12.startsWith("..") ? relative12 : file);
|
|
3507
4002
|
};
|
|
3508
4003
|
const stats = store.getStats();
|
|
@@ -3516,16 +4011,16 @@ async function buildAtlasBrief(store, projectRoot) {
|
|
|
3516
4011
|
...concept !== void 0 && concept !== "" ? { concept: trim(concept) } : {}
|
|
3517
4012
|
};
|
|
3518
4013
|
});
|
|
3519
|
-
const counts = store
|
|
4014
|
+
const counts = atlasPackageFileCounts(store, relativeOf);
|
|
3520
4015
|
const hubOf = /* @__PURE__ */ new Map();
|
|
3521
4016
|
for (const row of store.getRankedFiles(Math.max(counts.size * 4, BRIEF_HUB_LIMIT))) {
|
|
3522
|
-
const name = row.package
|
|
4017
|
+
const name = atlasPackageName(row.package, relativeOf(row.file));
|
|
3523
4018
|
const current = hubOf.get(name);
|
|
3524
4019
|
if (current === void 0 || row.rank > current.rank) {
|
|
3525
4020
|
hubOf.set(name, { file: relativeOf(row.file), rank: row.rank });
|
|
3526
4021
|
}
|
|
3527
4022
|
}
|
|
3528
|
-
const packages = [...hubOf.entries()].sort((a, b) => b[1].rank - a[1].rank || a[0]
|
|
4023
|
+
const packages = [...hubOf.entries()].sort((a, b) => b[1].rank - a[1].rank || compareText(a[0], b[0])).slice(0, BRIEF_PACKAGE_LIMIT).map(([name, hub]) => ({ name, files: counts.get(name) ?? 0, hub: hub.file }));
|
|
3529
4024
|
const subsystems = store.getSubsystems().filter((subsystem) => subsystem.summary !== "").slice(0, BRIEF_SUBSYSTEM_LIMIT).map((subsystem) => ({ name: subsystem.name, summary: trim(subsystem.summary) }));
|
|
3530
4025
|
const brief = {
|
|
3531
4026
|
ranked: hubs.length > 0,
|
|
@@ -3548,7 +4043,7 @@ async function buildAtlasBrief(store, projectRoot) {
|
|
|
3548
4043
|
return brief;
|
|
3549
4044
|
}
|
|
3550
4045
|
async function buildProjectAtlasBrief(projectRoot, opts = {}) {
|
|
3551
|
-
if (!existsSync4(
|
|
4046
|
+
if (!existsSync4(path11.join(resolveIndexDir(projectRoot, opts.indexDir), "index.db"))) {
|
|
3552
4047
|
return { indexed: false };
|
|
3553
4048
|
}
|
|
3554
4049
|
const store = indexStorePool.acquire(projectRoot, { indexDir: opts.indexDir });
|
|
@@ -3560,12 +4055,15 @@ async function buildProjectAtlasBrief(projectRoot, opts = {}) {
|
|
|
3560
4055
|
}
|
|
3561
4056
|
|
|
3562
4057
|
// src/codebase-index/codebase-context-tool.ts
|
|
3563
|
-
import { ToolValidationError as
|
|
4058
|
+
import { ToolValidationError as ToolValidationError11 } from "@wrongstack/core/types";
|
|
3564
4059
|
import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
|
|
3565
4060
|
var TOOL_TIMEOUT_MS = 35e3;
|
|
3566
4061
|
var VECTOR_HIT_LIMIT = 12;
|
|
3567
4062
|
var VECTOR_MIN_SCORE = 0.25;
|
|
3568
4063
|
var queryEmbedder;
|
|
4064
|
+
function finiteIntOrUndefined(value) {
|
|
4065
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.trunc(value) : void 0;
|
|
4066
|
+
}
|
|
3569
4067
|
function setContextQueryEmbedder(port) {
|
|
3570
4068
|
queryEmbedder = port;
|
|
3571
4069
|
}
|
|
@@ -3605,12 +4103,16 @@ var codebaseContextTool = {
|
|
|
3605
4103
|
description: "What you are trying to do or understand, in plain words."
|
|
3606
4104
|
},
|
|
3607
4105
|
limit: {
|
|
3608
|
-
type: "
|
|
3609
|
-
description: "Number of files to return (default 12, max 50)."
|
|
4106
|
+
type: "integer",
|
|
4107
|
+
description: "Number of files to return (default 12, max 50).",
|
|
4108
|
+
minimum: 1,
|
|
4109
|
+
maximum: 50
|
|
3610
4110
|
},
|
|
3611
4111
|
symbolsPerFile: {
|
|
3612
|
-
type: "
|
|
3613
|
-
description: "Declarations shown per file (default 4, max 20)."
|
|
4112
|
+
type: "integer",
|
|
4113
|
+
description: "Declarations shown per file (default 4, max 20).",
|
|
4114
|
+
minimum: 1,
|
|
4115
|
+
maximum: 20
|
|
3614
4116
|
},
|
|
3615
4117
|
pathPrefix: {
|
|
3616
4118
|
type: "string",
|
|
@@ -3623,7 +4125,7 @@ var codebaseContextTool = {
|
|
|
3623
4125
|
async execute(input, ctx, execOpts) {
|
|
3624
4126
|
const query = typeof input.query === "string" ? input.query.trim() : "";
|
|
3625
4127
|
if (query.length === 0) {
|
|
3626
|
-
throw new
|
|
4128
|
+
throw new ToolValidationError11({
|
|
3627
4129
|
message: "codebase-context requires a non-empty `query`.",
|
|
3628
4130
|
field: "query"
|
|
3629
4131
|
});
|
|
@@ -3641,9 +4143,11 @@ var codebaseContextTool = {
|
|
|
3641
4143
|
indexDir,
|
|
3642
4144
|
query,
|
|
3643
4145
|
...vectorFiles.length > 0 ? { vectorFiles } : {},
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
4146
|
+
// A NaN slipping past the schema survived the server's min/max clamp
|
|
4147
|
+
// and truncated the answer to zero files.
|
|
4148
|
+
limit: finiteIntOrUndefined(input.limit),
|
|
4149
|
+
symbolsPerFile: finiteIntOrUndefined(input.symbolsPerFile),
|
|
4150
|
+
pathPrefix: input.pathPrefix?.replace(/\\/g, "/")
|
|
3647
4151
|
});
|
|
3648
4152
|
} catch (err) {
|
|
3649
4153
|
if (signal?.aborted) throw err;
|
|
@@ -3673,7 +4177,7 @@ var codebaseContextTool = {
|
|
|
3673
4177
|
// src/codebase-index/concept-enrichment.ts
|
|
3674
4178
|
import { existsSync as existsSync5 } from "node:fs";
|
|
3675
4179
|
import * as fs10 from "node:fs/promises";
|
|
3676
|
-
import * as
|
|
4180
|
+
import * as path12 from "node:path";
|
|
3677
4181
|
var MAX_SOURCE_CHARS = 12e3;
|
|
3678
4182
|
var MAX_CRUX_LINES = 12;
|
|
3679
4183
|
var MAX_DECLARATIONS = 24;
|
|
@@ -3692,7 +4196,9 @@ async function enrichConcepts(store, port, relativeOf, options = {}) {
|
|
|
3692
4196
|
const startedAt = Date.now();
|
|
3693
4197
|
const errors = [];
|
|
3694
4198
|
const signal = options.signal;
|
|
3695
|
-
const
|
|
4199
|
+
const requestedConcurrency = Number.isFinite(options.concurrency) ? Math.trunc(options.concurrency) : DEFAULT_CONCURRENCY;
|
|
4200
|
+
const concurrency = Math.max(1, Math.min(requestedConcurrency, 16));
|
|
4201
|
+
const maxFiles = options.maxFiles !== void 0 && Number.isFinite(options.maxFiles) ? Math.max(0, Math.trunc(options.maxFiles)) : void 0;
|
|
3696
4202
|
const pruned = store.pruneOrphanConcepts();
|
|
3697
4203
|
const markedStale = store.markStaleConcepts();
|
|
3698
4204
|
const hashByFile = /* @__PURE__ */ new Map();
|
|
@@ -3707,6 +4213,7 @@ async function enrichConcepts(store, port, relativeOf, options = {}) {
|
|
|
3707
4213
|
const queue = [];
|
|
3708
4214
|
let cached = 0;
|
|
3709
4215
|
for (const file of ordered) {
|
|
4216
|
+
if (maxFiles !== void 0 && queue.length >= maxFiles) break;
|
|
3710
4217
|
const hash = hashByFile.get(file);
|
|
3711
4218
|
if (hash === void 0) continue;
|
|
3712
4219
|
const concept = existing.get(file);
|
|
@@ -3716,7 +4223,6 @@ async function enrichConcepts(store, port, relativeOf, options = {}) {
|
|
|
3716
4223
|
continue;
|
|
3717
4224
|
}
|
|
3718
4225
|
queue.push(file);
|
|
3719
|
-
if (options.maxFiles !== void 0 && queue.length >= options.maxFiles) break;
|
|
3720
4226
|
}
|
|
3721
4227
|
let summarised = 0;
|
|
3722
4228
|
let failed = 0;
|
|
@@ -3787,7 +4293,7 @@ async function summariseOne(store, port, relativeOf, file, hashByFile, existing,
|
|
|
3787
4293
|
const result = await port.describeFile({
|
|
3788
4294
|
file: relativeOf(file),
|
|
3789
4295
|
absolutePath: file,
|
|
3790
|
-
language: "",
|
|
4296
|
+
language: detectLang(file) ?? "",
|
|
3791
4297
|
source: truncated ? source.slice(0, MAX_SOURCE_CHARS) : source,
|
|
3792
4298
|
truncated,
|
|
3793
4299
|
declarations,
|
|
@@ -3816,13 +4322,18 @@ async function deriveSubsystems(store, port, relativeOf, signal) {
|
|
|
3816
4322
|
if (describe === void 0) return 0;
|
|
3817
4323
|
const summaries = store.getReadyConceptSummaries();
|
|
3818
4324
|
if (summaries.size === 0) return 0;
|
|
4325
|
+
const rankOf = store.getFileRankMap();
|
|
4326
|
+
const packageOf = store.getFilePackages();
|
|
4327
|
+
const ordered = [...summaries.keys()].sort(
|
|
4328
|
+
(a, b) => (rankOf.get(b) ?? 0) - (rankOf.get(a) ?? 0) || a.localeCompare(b)
|
|
4329
|
+
);
|
|
3819
4330
|
const byPackage = /* @__PURE__ */ new Map();
|
|
3820
|
-
for (const
|
|
3821
|
-
const summary = summaries.get(
|
|
4331
|
+
for (const file of ordered) {
|
|
4332
|
+
const summary = summaries.get(file);
|
|
3822
4333
|
if (summary === void 0) continue;
|
|
3823
|
-
const name =
|
|
4334
|
+
const name = packageOf.get(file) || relativeOf(file).split("/")[0] || "(root)";
|
|
3824
4335
|
const bucket = byPackage.get(name);
|
|
3825
|
-
const entry = { file: relativeOf(
|
|
4336
|
+
const entry = { file: relativeOf(file), summary, rank: rankOf.get(file) ?? 0 };
|
|
3826
4337
|
if (bucket === void 0) byPackage.set(name, [entry]);
|
|
3827
4338
|
else if (bucket.length < SUBSYSTEM_FILE_SAMPLE) bucket.push(entry);
|
|
3828
4339
|
}
|
|
@@ -3855,7 +4366,7 @@ async function deriveSubsystems(store, port, relativeOf, signal) {
|
|
|
3855
4366
|
}
|
|
3856
4367
|
async function enrichProjectConcepts(projectRoot, port, options = {}) {
|
|
3857
4368
|
const { indexDir, ...rest } = options;
|
|
3858
|
-
if (!existsSync5(
|
|
4369
|
+
if (!existsSync5(path12.join(resolveIndexDir(projectRoot, indexDir), "index.db"))) {
|
|
3859
4370
|
return { indexed: false };
|
|
3860
4371
|
}
|
|
3861
4372
|
const store = indexStorePool.acquire(projectRoot, { indexDir });
|
|
@@ -3864,7 +4375,7 @@ async function enrichProjectConcepts(projectRoot, port, options = {}) {
|
|
|
3864
4375
|
store,
|
|
3865
4376
|
port,
|
|
3866
4377
|
(file) => {
|
|
3867
|
-
const relative12 =
|
|
4378
|
+
const relative12 = path12.relative(projectRoot, file);
|
|
3868
4379
|
const chosen = relative12 && !relative12.startsWith("..") ? relative12 : file;
|
|
3869
4380
|
return posixIndexPath(chosen);
|
|
3870
4381
|
},
|
|
@@ -3877,7 +4388,7 @@ async function enrichProjectConcepts(projectRoot, port, options = {}) {
|
|
|
3877
4388
|
|
|
3878
4389
|
// src/codebase-index/embedding-pass.ts
|
|
3879
4390
|
import { existsSync as existsSync6 } from "node:fs";
|
|
3880
|
-
import * as
|
|
4391
|
+
import * as path13 from "node:path";
|
|
3881
4392
|
var DEFAULT_BATCH_SIZE = 16;
|
|
3882
4393
|
var MAX_EMBED_CHARS = 2e3;
|
|
3883
4394
|
var FALLBACK_DECLARATIONS = 12;
|
|
@@ -3901,7 +4412,9 @@ async function embedFiles(store, port, relativeOf, options = {}) {
|
|
|
3901
4412
|
const startedAt = Date.now();
|
|
3902
4413
|
const errors = [];
|
|
3903
4414
|
const signal = options.signal;
|
|
3904
|
-
const
|
|
4415
|
+
const requestedBatch = Number.isFinite(options.batchSize) ? Math.trunc(options.batchSize) : DEFAULT_BATCH_SIZE;
|
|
4416
|
+
const batchSize = Math.max(1, Math.min(requestedBatch, 128));
|
|
4417
|
+
const maxFiles = options.maxFiles !== void 0 && Number.isFinite(options.maxFiles) ? Math.max(0, Math.trunc(options.maxFiles)) : void 0;
|
|
3905
4418
|
const providerChanged = store.reconcileVectorProvider(port.id);
|
|
3906
4419
|
const pruned = store.pruneOrphanFileVectors();
|
|
3907
4420
|
const stored = store.getFileVectorStates(port.id);
|
|
@@ -3913,6 +4426,7 @@ async function embedFiles(store, port, relativeOf, options = {}) {
|
|
|
3913
4426
|
const queue = [];
|
|
3914
4427
|
let cached = 0;
|
|
3915
4428
|
for (const file of ordered) {
|
|
4429
|
+
if (maxFiles !== void 0 && queue.length >= maxFiles) break;
|
|
3916
4430
|
const declarations = summaries.has(file) ? [] : store.getFileSymbols(file, FALLBACK_DECLARATIONS).map((symbol) => symbol.name);
|
|
3917
4431
|
const { text, fromDeclarations: fromDeclarations2 } = embedTextFor(
|
|
3918
4432
|
relativeOf(file),
|
|
@@ -3926,7 +4440,6 @@ async function embedFiles(store, port, relativeOf, options = {}) {
|
|
|
3926
4440
|
continue;
|
|
3927
4441
|
}
|
|
3928
4442
|
queue.push({ file, text, hash, fromDeclarations: fromDeclarations2 });
|
|
3929
|
-
if (options.maxFiles !== void 0 && queue.length >= options.maxFiles) break;
|
|
3930
4443
|
}
|
|
3931
4444
|
let embedded = 0;
|
|
3932
4445
|
let fromDeclarations = 0;
|
|
@@ -3937,15 +4450,24 @@ async function embedFiles(store, port, relativeOf, options = {}) {
|
|
|
3937
4450
|
try {
|
|
3938
4451
|
const vectors = await port.embed(batch.map((entry) => entry.text));
|
|
3939
4452
|
const rows = [];
|
|
4453
|
+
let rejected = 0;
|
|
3940
4454
|
for (let i = 0; i < batch.length; i++) {
|
|
3941
4455
|
const entry = batch[i];
|
|
3942
4456
|
const vector = vectors[i];
|
|
3943
|
-
if (vector === void 0 || vector.length !== port.dimensions)
|
|
4457
|
+
if (vector === void 0 || vector.length !== port.dimensions || !vector.every(Number.isFinite)) {
|
|
4458
|
+
rejected++;
|
|
4459
|
+
continue;
|
|
4460
|
+
}
|
|
3944
4461
|
rows.push({ file: entry.file, vector, sourceHash: entry.hash, provider: port.id });
|
|
3945
4462
|
if (entry.fromDeclarations) fromDeclarations++;
|
|
3946
4463
|
}
|
|
3947
4464
|
store.upsertFileVectors(rows);
|
|
3948
4465
|
embedded += rows.length;
|
|
4466
|
+
if (rejected > 0 && errors.length < 20) {
|
|
4467
|
+
errors.push(
|
|
4468
|
+
`batch at ${start}: dropped ${rejected} vector(s) that were missing, the wrong width, or non-finite`
|
|
4469
|
+
);
|
|
4470
|
+
}
|
|
3949
4471
|
} catch (error) {
|
|
3950
4472
|
if (errors.length < 20) {
|
|
3951
4473
|
errors.push(`batch at ${start}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -3967,7 +4489,7 @@ async function embedFiles(store, port, relativeOf, options = {}) {
|
|
|
3967
4489
|
}
|
|
3968
4490
|
async function embedProjectFiles(projectRoot, port, options = {}) {
|
|
3969
4491
|
const { indexDir, ...rest } = options;
|
|
3970
|
-
if (!existsSync6(
|
|
4492
|
+
if (!existsSync6(path13.join(resolveIndexDir(projectRoot, indexDir), "index.db"))) {
|
|
3971
4493
|
return { indexed: false };
|
|
3972
4494
|
}
|
|
3973
4495
|
const store = indexStorePool.acquire(projectRoot, { indexDir });
|
|
@@ -3976,7 +4498,7 @@ async function embedProjectFiles(projectRoot, port, options = {}) {
|
|
|
3976
4498
|
store,
|
|
3977
4499
|
port,
|
|
3978
4500
|
(file) => {
|
|
3979
|
-
const relative12 =
|
|
4501
|
+
const relative12 = path13.relative(projectRoot, file);
|
|
3980
4502
|
return posixIndexPath(relative12 && !relative12.startsWith("..") ? relative12 : file);
|
|
3981
4503
|
},
|
|
3982
4504
|
rest
|
|
@@ -4040,4 +4562,4 @@ export {
|
|
|
4040
4562
|
embedFiles,
|
|
4041
4563
|
embedProjectFiles
|
|
4042
4564
|
};
|
|
4043
|
-
//# sourceMappingURL=chunk-
|
|
4565
|
+
//# sourceMappingURL=chunk-K37AKCTN.js.map
|