@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.
Files changed (80) hide show
  1. package/dist/builtin.js +17 -17
  2. package/dist/{chunk-6I6RXQFG.js → chunk-2BCRQAIW.js} +9 -9
  3. package/dist/{chunk-XRNN44QY.js → chunk-2IRHFHZY.js} +235 -71
  4. package/dist/{chunk-5RBUYFRV.js → chunk-4IMNVQRB.js} +8 -2
  5. package/dist/{chunk-NEYUPZL6.js → chunk-6D3UIQMF.js} +651 -303
  6. package/dist/chunk-BJ5LXLKE.js +351 -0
  7. package/dist/{chunk-C6ZOXNXV.js → chunk-CSS42GRP.js} +2 -2
  8. package/dist/{chunk-SKRMUHTU.js → chunk-HSLDY55L.js} +41 -8
  9. package/dist/{chunk-ENE22I5W.js → chunk-K37AKCTN.js} +884 -362
  10. package/dist/{chunk-V25Q6TQ7.js → chunk-K3VX6V5P.js} +3 -3
  11. package/dist/chunk-OIFY4XNP.js +855 -0
  12. package/dist/{chunk-OUERRFEM.js → chunk-OMQDKZOT.js} +17 -8
  13. package/dist/{chunk-7GOE2ZTZ.js → chunk-OMWC2OGT.js} +2 -2
  14. package/dist/{chunk-IE4KDAOB.js → chunk-OYQE2UNT.js} +3 -3
  15. package/dist/chunk-R3HR3QI3.js +97 -0
  16. package/dist/{chunk-XPVYZ2S6.js → chunk-RYLMSGR5.js} +2 -2
  17. package/dist/{chunk-HYHE4VNA.js → chunk-UMOP3VGK.js} +79 -45
  18. package/dist/{chunk-FVSMTEO3.js → chunk-VMNWQI4V.js} +2 -2
  19. package/dist/{chunk-2UVAIS5O.js → chunk-YPSVPCIE.js} +2 -2
  20. package/dist/{chunk-PWZUHZP7.js → chunk-Z77XMU3U.js} +2 -2
  21. package/dist/codebase-index/ast-invariant-engine.d.ts +19 -2
  22. package/dist/codebase-index/atlas-projection.d.ts +19 -0
  23. package/dist/codebase-index/background-indexer.d.ts +14 -0
  24. package/dist/codebase-index/circuit-breaker.d.ts +7 -0
  25. package/dist/codebase-index/codebase-ast-replace-tool.d.ts +1 -1
  26. package/dist/codebase-index/codebase-impact-analysis-tool.d.ts +6 -0
  27. package/dist/codebase-index/codebase-invariant-check-tool.d.ts +2 -0
  28. package/dist/codebase-index/codebase-targeted-test-tool.d.ts +26 -0
  29. package/dist/codebase-index/dead-code-scan.d.ts +4 -0
  30. package/dist/codebase-index/go-parser.d.ts +23 -3
  31. package/dist/codebase-index/graph-rank-pass.d.ts +10 -0
  32. package/dist/codebase-index/graph-rank.d.ts +8 -0
  33. package/dist/codebase-index/index.d.ts +1 -1
  34. package/dist/codebase-index/index.js +9 -9
  35. package/dist/codebase-index/lsp-kind.d.ts +3 -1
  36. package/dist/codebase-index/module-resolver.d.ts +21 -1
  37. package/dist/codebase-index/parser-batch.d.ts +6 -8
  38. package/dist/codebase-index/parser-output.d.ts +5 -1
  39. package/dist/codebase-index/parser-worker-script.js +4 -4
  40. package/dist/codebase-index/project-server-framing.d.ts +1 -17
  41. package/dist/codebase-index/project-server-watcher.d.ts +6 -0
  42. package/dist/codebase-index/project-server.js +72 -26
  43. package/dist/codebase-index/py-parser.d.ts +4 -4
  44. package/dist/codebase-index/rs-parser.d.ts +7 -0
  45. package/dist/codebase-index/toolchain-scripts.d.ts +64 -0
  46. package/dist/codebase-index/tree-sitter/queries.d.ts +14 -0
  47. package/dist/codebase-index/tree-sitter/util.d.ts +6 -1
  48. package/dist/codebase-index/tree-sitter-parser.d.ts +7 -0
  49. package/dist/codebase-index/worker.js +23 -16
  50. package/dist/codebase-index/writer-refs.d.ts +17 -2
  51. package/dist/codebase-index/writer-search-helpers.d.ts +9 -0
  52. package/dist/codebase-index/writer.d.ts +20 -1
  53. package/dist/edit.js +8 -8
  54. package/dist/{generic-parser-N2PLGBC2.js → generic-parser-IVWLFKMR.js} +2 -2
  55. package/dist/go-parser-LWV3L5V7.js +197 -0
  56. package/dist/index.js +19 -19
  57. package/dist/json-parser-PERV2JHX.js +204 -0
  58. package/dist/pack.js +18 -18
  59. package/dist/parser-dispatch-ZSWCQB6M.js +14 -0
  60. package/dist/patch.js +8 -8
  61. package/dist/{py-parser-4PFZYAJ7.js → py-parser-B6D6RGH7.js} +4 -4
  62. package/dist/read.js +8 -8
  63. package/dist/replace.js +8 -8
  64. package/dist/rs-parser-L7SP4GWJ.js +182 -0
  65. package/dist/test.d.ts +1 -0
  66. package/dist/test.js +1 -1
  67. package/dist/tool-tier.js +19 -19
  68. package/dist/{tree-sitter-parser-GILYQLS4.js → tree-sitter-parser-KU6JF3ZC.js} +6 -4
  69. package/dist/{ts-parser-7W26YB26.js → ts-parser-PML6QIH7.js} +55 -23
  70. package/dist/typecheck.js +1 -1
  71. package/dist/write.js +8 -8
  72. package/dist/{yaml-parser-DFBC5FWH.js → yaml-parser-7BJN544B.js} +41 -6
  73. package/package.json +5 -5
  74. package/dist/chunk-4CE3RKEP.js +0 -390
  75. package/dist/chunk-KHUCKLOR.js +0 -899
  76. package/dist/chunk-ZKC5UNGD.js +0 -167
  77. package/dist/go-parser-Z4H6SR3R.js +0 -474
  78. package/dist/json-parser-IJD77VL4.js +0 -256
  79. package/dist/parser-dispatch-WG4DNBHH.js +0 -14
  80. package/dist/rs-parser-GIWPUA3T.js +0 -81
@@ -1,6 +1,7 @@
1
1
  import {
2
- parseTreeSitterAst
3
- } from "./chunk-XRNN44QY.js";
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-HYHE4VNA.js";
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-NEYUPZL6.js";
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
- if (ts.canHaveModifiers(node)) {
205
- const modifiers = ts.getModifiers(node);
206
- if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
207
- return true;
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 false;
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) && node.exportClause && ts.isNamedExports(node.exportClause)) {
214
- for (const element of node.exportClause.elements) {
215
- exports.add(element.name.text);
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.isFunctionDeclaration(node) && node.name) {
219
- const name = node.name.text;
220
- const isExp = isExportedNode(node);
221
- if (isExp) exports.add(name);
222
- const params = node.parameters.map((p) => {
223
- const pName = p.name.getText(sourceFile);
224
- const isOptional = Boolean(p.questionToken);
225
- const hasDefault = Boolean(p.initializer);
226
- const isRest = Boolean(p.dotDotDotToken);
227
- const type = p.type ? p.type.getText(sourceFile) : void 0;
228
- return { name: pName, isOptional, hasDefault, isRest, type };
229
- });
230
- const returnType = node.type ? node.type.getText(sourceFile) : void 0;
231
- functions.set(name, { name, isExported: isExp, params, returnType });
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
- const returnType = member.type ? member.type.getText(sourceFile) : void 0;
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
- if (ts.isInterfaceDeclaration(node) && node.name) {
260
- const name = node.name.text;
261
- const isExp = isExportedNode(node);
262
- if (isExp) exports.add(name);
263
- const properties = [];
264
- const methods = [];
265
- for (const member of node.members) {
266
- if (ts.isPropertySignature(member) && member.name) {
267
- const propName = member.name.getText(sourceFile);
268
- const isOptional = Boolean(member.questionToken);
269
- const type = member.type ? member.type.getText(sourceFile) : void 0;
270
- properties.push({ name: propName, isOptional, type });
271
- } else if (ts.isMethodSignature(member) && member.name) {
272
- const methodName = member.name.getText(sourceFile);
273
- const isOptional = Boolean(member.questionToken);
274
- const params = member.parameters.map((p) => ({
275
- name: p.name.getText(sourceFile),
276
- isOptional: Boolean(p.questionToken),
277
- hasDefault: Boolean(p.initializer),
278
- isRest: Boolean(p.dotDotDotToken)
279
- }));
280
- const returnType = member.type ? member.type.getText(sourceFile) : void 0;
281
- methods.push({ name: methodName, isOptional, params, returnType });
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
- interfaces.set(name, { name, isExported: isExp, properties, methods });
285
- }
286
- if (ts.isTypeAliasDeclaration(node) && node.name && ts.isTypeLiteralNode(node.type)) {
287
- const name = node.name.text;
288
- const isExp = isExportedNode(node);
289
- if (isExp) exports.add(name);
290
- const properties = [];
291
- for (const member of node.type.members) {
292
- if (ts.isPropertySignature(member) && member.name) {
293
- const propName = member.name.getText(sourceFile);
294
- const isOptional = Boolean(member.questionToken);
295
- const type = member.type ? member.type.getText(sourceFile) : void 0;
296
- properties.push({ name: propName, isOptional, type });
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, Java, etc.) via Tree-sitter WASM.
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 isExported = !name.startsWith("_");
327
- if (isExported) exports.add(name);
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(name, { name, isExported, params });
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
- if (!name.startsWith("_")) exports.add(name);
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(name);
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
- params.push({
369
- name: param.text,
370
- isOptional: false,
371
- hasDefault: false,
372
- isRest: param.type === "variadic_parameter_declaration"
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(name, { name, isExported, params });
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 === "function_item") {
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 isExported = node.children.some((c) => c.type === "visibility_modifier");
406
- if (isExported) exports.add(name);
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) continue;
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(name, { name, isExported, params });
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, { name, isExported, properties: [], methods });
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
- if (targetNode) return;
600
- const nameNode = node.childForFieldName("name") ?? node.childForFieldName("declarator");
601
- if (nameNode && nameNode.text === symbolName) {
602
- targetNode = node;
603
- bodyNode = node.childForFieldName("body") ?? node.children.find(
604
- (c) => [
605
- "compound_statement",
606
- "statement_block",
607
- "block",
608
- "function_body",
609
- "body_statement",
610
- "code_block",
611
- "do_block"
612
- ].includes(c.type)
613
- ) ?? null;
614
- return;
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
- let targetNode = null;
624
- let bodyNode = null;
770
+ const matches = [];
771
+ const scope = [];
625
772
  walk2(root);
626
- if (!targetNode) return null;
627
- const replaceTarget = target === "full" || !bodyNode ? targetNode : bodyNode;
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" && lang === "py") {
634
- const indent = " ";
635
- formattedReplacement = formattedReplacement.split("\n").map((l) => l.trim() ? `${indent}${l}` : "").join("\n");
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 = String.raw`(?:it|test|describe|suite|bench|context|specify)`;
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 = /(?:test|spec|mock|fixture|bench)[s]?[/\\._]/i;
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
- let rawSites = [];
805
- let symbolFound = true;
806
- let indexUnavailable = false;
807
- let indexError;
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
- const serviced = await incomingCallsService({
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
- symbolFound = false;
821
- indexUnavailable = true;
822
- indexError = toErrorMessage(err);
823
- }
824
- if (indexUnavailable) {
825
- const detail = indexError ? ` (${indexError})` : "";
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.${detail}`
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 = TEST_FILE_REGEX.test(relPath);
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 totalCallSites = callSites.length;
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 (prodFiles.length > 5 || totalCallSites > 10) {
1110
+ if (directProdFiles.length > 5 || totalCallSites > 10) {
870
1111
  riskLevel = "high";
871
- } else if (prodFiles.length > 1 || totalCallSites > 3) {
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 (prodFiles.length > 0) {
1117
+ if (directProdFiles.length > 0) {
876
1118
  recommendedActionPlan.push(
877
- `Update ${totalCallSites} call site(s) across ${prodFiles.length} production file(s): ${prodFiles.slice(0, 3).join(", ")}${prodFiles.length > 3 ? ` +${prodFiles.length - 3} more` : ""}`
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 summary = `Blast Radius for '${input.symbol}': ${riskLevel.toUpperCase()} RISK (${totalCallSites} call sites in ${prodFiles.length} prod files, ${testFiles.length} test suites).`;
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 ToolValidationError({
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: ctx.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. Results include callers of all same-named symbols. Use codebase-search to find the exact file and pass it as \`file\`.`
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 ToolValidationError3 } from "@wrongstack/core/types";
1294
+ import { ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
1038
1295
 
1039
1296
  // src/codebase-index/codebase-search-tool.ts
1040
- import { ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
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 ToolValidationError2({
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 ToolValidationError2({
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 full reindex \u2014 clears the index first and reindexes all files."
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 ToolValidationError3({
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 ToolValidationError3({
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 ToolValidationError4 } from "@wrongstack/core/types";
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: "Mathematically verify AST backward-compatibility invariants between original code and candidate mutations across TypeScript, Python, Go, Rust, Java, etc. Detects missing exports (INV-001), mandatory parameter additions (INV-002), interface breaking expansions (INV-003), and return type changes (INV-004) with zero hallucinations.",
1302
- usageHint: "PRE-FLIGHT AST INVARIANT AUDIT:\n\n- Provide `file` and `modifiedCode` (or both `originalCode` and `modifiedCode`).\n- Returns `valid: true` if changes are strictly backward compatible.\n- Returns structured `violations[]` with exact rule IDs, offending symbol names, and fix instructions.",
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 (!originalCode && input.file) {
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 ToolValidationError4({
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. Mutation is 100% backward compatible." : `AST Invariants FAILED with ${res.violations.length} violation(s):
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 ToolValidationError5 } from "@wrongstack/core/types";
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 ToolValidationError5({
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: ctx.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 (!ts.canHaveModifiers(node)) return false;
1546
- const modifiers = ts.getModifiers(node);
1547
- return modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
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
- replacements.push({
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
- replacements.push({
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 (type === "function_definition" || type === "function_declaration" || type === "method_declaration" || type === "function_item" || type === "method" || type === "singleton_method" || type === "func_literal") {
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
- replacements.push({
1712
- start: bodyNode.startIndex,
1713
- end: bodyNode.endIndex,
1714
- text: includeLineNumbers ? `... # L${bodyStart}-L${bodyEnd}` : "..."
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 nameNode = node.childForFieldName("name") ?? node.childForFieldName("declarator");
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: "number",
2155
- description: "Maximum token budget (approximate) for the map. Defaults to 1200."
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: input.maxTokens,
2172
- focusFiles: input.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: "number",
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 maxFiles = Math.min(Math.max(1, input.maxFiles ?? 20), 50);
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 path7 from "node:path";
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
- var TEST_FILE_REGEX2 = /(?:test|spec|mock|fixture|bench)[s]?[/\\._]/i;
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 = path7.parse(sourceRelPath);
2763
+ const parsed = path8.parse(sourceRelPath);
2370
2764
  const candidates = [
2371
- path7.join(parsed.dir, `${parsed.name}.test${parsed.ext}`),
2372
- path7.join(parsed.dir, `${parsed.name}.spec${parsed.ext}`),
2373
- path7.join(parsed.dir, `${parsed.name}_test${parsed.ext}`),
2374
- path7.join(parsed.dir, `test_${parsed.name}${parsed.ext}`),
2375
- path7.join("tests", `${parsed.name}.test${parsed.ext}`),
2376
- path7.join("tests", `${parsed.name}.spec${parsed.ext}`),
2377
- path7.join("test", `${parsed.name}.test${parsed.ext}`),
2378
- path7.join(parsed.dir, "__tests__", `${parsed.name}.test${parsed.ext}`),
2379
- path7.join(parsed.dir, "__tests__", `${parsed.name}${parsed.ext}`)
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 = path7.resolve(projectRoot, cand);
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
- try {
2427
- if (input.testFiles) {
2428
- for (const tf of input.testFiles) suitesSet.add(tf.replace(/\\/g, "/"));
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
- if (input.symbol) {
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: input.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 = path7.relative(projectRoot, site.symbol.file).replace(/\\/g, "/");
2443
- if (TEST_FILE_REGEX2.test(relPath)) {
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 (input.file) {
2451
- const convFiles = await findConventionTestFiles(projectRoot, input.file);
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
- let runnerCmd = "npx";
2468
- let runnerArgs = ["vitest", "run", ...discoveredSuites];
2469
- if (discoveredSuites.every((s) => s.endsWith(".py"))) {
2470
- runnerCmd = "pytest";
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 combinedOutput = normalizeCommandOutput(
2491
- `${streamRes.stdout}
2492
- ${streamRes.stderr}`.trim(),
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 passMatch = combinedOutput.match(/(\d+)\s+passed/i);
2497
- const failMatch = combinedOutput.match(/(\d+)\s+failed/i);
2498
- const passed = passMatch ? parseInt(passMatch[1] ?? "0", 10) : isPassed ? discoveredSuites.length : 0;
2499
- const failed = failMatch ? parseInt(failMatch[1] ?? "0", 10) : isPassed ? 0 : 1;
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 path8 from "node:path";
2522
- import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
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
- path8.isAbsolute(ep) ? ep : path8.resolve(projectRoot, ep),
3014
+ path9.isAbsolute(ep) ? ep : path9.resolve(projectRoot, ep),
2561
3015
  ctx
2562
3016
  );
2563
3017
  if (!fs8.existsSync(resolved)) {
2564
- throw new ToolValidationError6({
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 (path8.isAbsolute(relative12)) return relative12;
2599
- return path8.resolve(base, relative12);
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(path8.join(projectRoot, "package.json"));
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 = path8.join(wsDir, "package.json");
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 = path8.join(wsDir, "src", "index.ts");
3081
+ const convention1 = path9.join(wsDir, "src", "index.ts");
2628
3082
  if (fs8.existsSync(convention1)) entries.add(convention1);
2629
- const convention2 = path8.join(wsDir, "src", "main.ts");
3083
+ const convention2 = path9.join(wsDir, "src", "main.ts");
2630
3084
  if (fs8.existsSync(convention2)) entries.add(convention2);
2631
- const convention3 = path8.join(wsDir, "index.ts");
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 = path8.join(projectRoot, name);
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) => `${path8.sep}${d}${path8.sep}`);
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, path8.sep);
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, `${path8.sep}src${path8.sep}`);
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 exports_ = pkg.exports;
2697
- if (exports_ && typeof exports_ === "object") {
2698
- for (const value of Object.values(exports_)) {
2699
- if (typeof value === "string") {
2700
- tryAddEntryPath(pkgDir, value, entries);
2701
- } else if (value && typeof value === "object") {
2702
- for (const nested of Object.values(value)) {
2703
- if (typeof nested === "string") {
2704
- tryAddEntryPath(pkgDir, nested, entries);
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 = path8.resolve(projectRoot, base);
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(path8.join(baseDir, child.name));
3177
+ dirs.push(path9.join(baseDir, child.name));
2721
3178
  }
2722
3179
  }
2723
3180
  } catch {
2724
3181
  }
2725
3182
  } else {
2726
- dirs.push(path8.resolve(projectRoot, entry));
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 workspaces = pkg.workspaces;
2733
- if (Array.isArray(workspaces)) {
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 = path8.join(projectRoot, "pnpm-workspace.yaml");
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 = path8.dirname(importerPath);
2781
- const base = path8.resolve(dir, moduleSpecifier);
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(path8.join(candidate, "index.ts")))
2794
- results.push(path8.join(candidate, "index.ts"));
2795
- if (indexedFiles.has(path8.join(candidate, "index.tsx")))
2796
- results.push(path8.join(candidate, "index.tsx"));
2797
- if (indexedFiles.has(path8.join(candidate, "index.js")))
2798
- results.push(path8.join(candidate, "index.js"));
2799
- if (indexedFiles.has(path8.join(candidate, "index.jsx")))
2800
- results.push(path8.join(candidate, "index.jsx"));
2801
- if (indexedFiles.has(path8.join(candidate, "index.mjs")))
2802
- results.push(path8.join(candidate, "index.mjs"));
2803
- if (indexedFiles.has(path8.join(candidate, "index.cjs")))
2804
- results.push(path8.join(candidate, "index.cjs"));
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) => path8.resolve(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
- let anyAlive = false;
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 + path8.sep));
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(path8.join(projectRoot, "package.json"));
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(path8.join(wsDir, "package.json"));
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 path10 from "node:path";
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 path9 from "node:path";
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 = path9.join(".wrongstack", "atlas");
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 = path9.relative(projectRoot, file);
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(`| ${pkg.name} | ${pkg.files} | \`${pkg.hub}\` | ${pkg.rank.toFixed(4)} |`);
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.getPackageFileCounts();
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 || relativeOf(row.file).split("/")[0] || "(root)";
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(row.package) ?? 0,
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.localeCompare(b.from) || a.to.localeCompare(b.to) || a.refType.localeCompare(b.refType)
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 = path9.join(projectRoot, ATLAS_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(path9.join(dir, name), body, "utf8");
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(path9.join(projectRoot, ATLAS_DIR, ATLAS_MANIFEST), "utf8");
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(path9.join(resolveIndexDir(projectRoot, indexDir), "index.db"));
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 ?? path9.basename(projectRoot)
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 = path10.relative(projectRoot, file);
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.getPackageFileCounts();
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 || relativeOf(row.file).split("/")[0] || "(root)";
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].localeCompare(b[0])).slice(0, BRIEF_PACKAGE_LIMIT).map(([name, hub]) => ({ name, files: counts.get(name) ?? 0, hub: hub.file }));
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(path10.join(resolveIndexDir(projectRoot, opts.indexDir), "index.db"))) {
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 ToolValidationError7 } from "@wrongstack/core/types";
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: "number",
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: "number",
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 ToolValidationError7({
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
- limit: input.limit,
3645
- symbolsPerFile: input.symbolsPerFile,
3646
- pathPrefix: input.pathPrefix
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 path11 from "node:path";
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 concurrency = Math.max(1, Math.min(options.concurrency ?? DEFAULT_CONCURRENCY, 16));
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 row of store.getRankedFiles(summaries.size * 2)) {
3821
- const summary = summaries.get(row.file);
4331
+ for (const file of ordered) {
4332
+ const summary = summaries.get(file);
3822
4333
  if (summary === void 0) continue;
3823
- const name = row.package || relativeOf(row.file).split("/")[0] || "(root)";
4334
+ const name = packageOf.get(file) || relativeOf(file).split("/")[0] || "(root)";
3824
4335
  const bucket = byPackage.get(name);
3825
- const entry = { file: relativeOf(row.file), summary, rank: row.rank };
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(path11.join(resolveIndexDir(projectRoot, indexDir), "index.db"))) {
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 = path11.relative(projectRoot, file);
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 path12 from "node:path";
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 batchSize = Math.max(1, Math.min(options.batchSize ?? DEFAULT_BATCH_SIZE, 128));
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) continue;
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(path12.join(resolveIndexDir(projectRoot, indexDir), "index.db"))) {
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 = path12.relative(projectRoot, file);
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-ENE22I5W.js.map
4565
+ //# sourceMappingURL=chunk-K37AKCTN.js.map