@wrongstack/tools 0.313.0 → 0.316.1

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 (182) hide show
  1. package/dist/_edit-match.d.ts +3 -2
  2. package/dist/_output-spool.d.ts +4 -3
  3. package/dist/_shell-pick.d.ts +2 -1
  4. package/dist/_spawn-stream.d.ts +2 -1
  5. package/dist/_syntax-check.d.ts +2 -1
  6. package/dist/audit.js +10 -4055
  7. package/dist/bash-kill-guard.d.ts +3 -6
  8. package/dist/bash.js +9 -2308
  9. package/dist/batch-tool-use.js +2 -187
  10. package/dist/browser/index.js +25 -1339
  11. package/dist/browser/network-guard-proxy.d.ts +2 -1
  12. package/dist/builtin.js +69 -33949
  13. package/dist/chunk-22B7GE5T.js +260 -0
  14. package/dist/chunk-2VSZC6QM.js +113 -0
  15. package/dist/chunk-3PVYQFRQ.js +472 -0
  16. package/dist/chunk-42BHI7TN.js +337 -0
  17. package/dist/chunk-4BHUSMNL.js +104 -0
  18. package/dist/chunk-4I4LWFOO.js +431 -0
  19. package/dist/chunk-4UESMQDP.js +122 -0
  20. package/dist/chunk-553IQUMO.js +478 -0
  21. package/dist/chunk-5H4GSJAI.js +223 -0
  22. package/dist/chunk-6RNIFLG4.js +305 -0
  23. package/dist/chunk-7EY5DCRO.js +434 -0
  24. package/dist/chunk-A6BRWIXN.js +317 -0
  25. package/dist/chunk-AHHDWNXC.js +200 -0
  26. package/dist/chunk-BSLVR56S.js +199 -0
  27. package/dist/chunk-BTSFTGIL.js +7 -0
  28. package/dist/chunk-D33S4RQG.js +222 -0
  29. package/dist/chunk-DA4KNE2H.js +4412 -0
  30. package/dist/chunk-DR7JOQTC.js +520 -0
  31. package/dist/chunk-EEOFQDT6.js +898 -0
  32. package/dist/chunk-EJWSCQ6D.js +137 -0
  33. package/dist/chunk-EMRGN4KM.js +170 -0
  34. package/dist/chunk-FQZOHLG3.js +89 -0
  35. package/dist/chunk-GOIZNNOA.js +221 -0
  36. package/dist/chunk-HFXSNKCV.js +196 -0
  37. package/dist/chunk-HYX2TZ7O.js +1272 -0
  38. package/dist/chunk-I64BS3VC.js +15 -0
  39. package/dist/chunk-IVWV2UM6.js +955 -0
  40. package/dist/chunk-IXMNTNW5.js +238 -0
  41. package/dist/chunk-IYK7XGDZ.js +892 -0
  42. package/dist/chunk-J5SKD6LN.js +235 -0
  43. package/dist/chunk-JNNVRHMV.js +2111 -0
  44. package/dist/chunk-JOHXDF3D.js +295 -0
  45. package/dist/chunk-JRJJX3L5.js +424 -0
  46. package/dist/chunk-K3WNSHUF.js +1371 -0
  47. package/dist/chunk-L25CVUVR.js +2830 -0
  48. package/dist/chunk-LIJQMYYX.js +59 -0
  49. package/dist/chunk-LWKUTMD5.js +197 -0
  50. package/dist/chunk-MQX2KXVW.js +54 -0
  51. package/dist/chunk-MZZN7VCD.js +359 -0
  52. package/dist/chunk-NBDZMMG2.js +169 -0
  53. package/dist/chunk-NKJW5GFO.js +366 -0
  54. package/dist/chunk-NKNOBUBU.js +522 -0
  55. package/dist/chunk-O4MBV4AX.js +134 -0
  56. package/dist/chunk-OUBDL3WH.js +3515 -0
  57. package/dist/chunk-PQKK5JH7.js +101 -0
  58. package/dist/chunk-PY2IOS7C.js +1000 -0
  59. package/dist/chunk-Q5JPPCZC.js +12 -0
  60. package/dist/chunk-QBV725YG.js +128 -0
  61. package/dist/chunk-QZQSPUBF.js +315 -0
  62. package/dist/chunk-RGISBZBD.js +231 -0
  63. package/dist/chunk-RJZQWEMS.js +84 -0
  64. package/dist/chunk-RXISBFR5.js +315 -0
  65. package/dist/chunk-S5CCLZL3.js +99 -0
  66. package/dist/chunk-SDLVH5RZ.js +178 -0
  67. package/dist/chunk-T2F4S7U4.js +1723 -0
  68. package/dist/chunk-TEBQ4VPZ.js +163 -0
  69. package/dist/chunk-U4D5S3VQ.js +21 -0
  70. package/dist/chunk-U4VAATKP.js +193 -0
  71. package/dist/chunk-UCYQTZ7F.js +113 -0
  72. package/dist/chunk-UKU2ZX4C.js +203 -0
  73. package/dist/chunk-UNSKG6DC.js +159 -0
  74. package/dist/chunk-V43EQ6VR.js +68 -0
  75. package/dist/chunk-VB3MJGCV.js +610 -0
  76. package/dist/chunk-VFKPWSOX.js +154 -0
  77. package/dist/chunk-VJNHFZ7J.js +190 -0
  78. package/dist/chunk-VXECAJJ7.js +1091 -0
  79. package/dist/chunk-WHIIIREL.js +223 -0
  80. package/dist/chunk-Z6DAKQ3U.js +1 -0
  81. package/dist/chunk-ZJQ4IGXK.js +474 -0
  82. package/dist/chunk-ZLTFLC25.js +385 -0
  83. package/dist/circuit-breaker.js +3 -191
  84. package/dist/codebase-index/ast-invariant-engine.d.ts +5 -4
  85. package/dist/codebase-index/binary-frame.d.ts +0 -2
  86. package/dist/codebase-index/bm25.d.ts +1 -1
  87. package/dist/codebase-index/circuit-breaker.d.ts +2 -1
  88. package/dist/codebase-index/codebase-ast-replace-tool.d.ts +3 -2
  89. package/dist/codebase-index/codebase-impact-analysis-tool.d.ts +4 -3
  90. package/dist/codebase-index/codebase-invariant-check-tool.d.ts +3 -2
  91. package/dist/codebase-index/codebase-repo-map-tool.d.ts +3 -2
  92. package/dist/codebase-index/codebase-skeleton-tool.d.ts +3 -2
  93. package/dist/codebase-index/codebase-targeted-test-tool.d.ts +3 -2
  94. package/dist/codebase-index/content-hash.d.ts +0 -2
  95. package/dist/codebase-index/import-extractor.d.ts +0 -13
  96. package/dist/codebase-index/index-service.d.ts +1 -1
  97. package/dist/codebase-index/index.js +84 -13688
  98. package/dist/codebase-index/languages.d.ts +2 -1
  99. package/dist/codebase-index/module-roots.d.ts +3 -2
  100. package/dist/codebase-index/parser-batch.d.ts +0 -5
  101. package/dist/codebase-index/parser-dispatch.d.ts +2 -1
  102. package/dist/codebase-index/parser-output.d.ts +4 -3
  103. package/dist/codebase-index/parser-worker-pool.d.ts +0 -2
  104. package/dist/codebase-index/parser-worker-script.d.ts +1 -16
  105. package/dist/codebase-index/parser-worker-script.js +8 -3995
  106. package/dist/codebase-index/project-server-client-state.d.ts +2 -2
  107. package/dist/codebase-index/project-server-endpoint.d.ts +0 -15
  108. package/dist/codebase-index/project-server.js +60 -8548
  109. package/dist/codebase-index/tree-sitter/visitor.d.ts +2 -1
  110. package/dist/codebase-index/tree-sitter-parser.d.ts +0 -2
  111. package/dist/codebase-index/ts-parser.d.ts +1 -1
  112. package/dist/codebase-index/vector-search.d.ts +2 -1
  113. package/dist/codebase-index/wal-maintenance.d.ts +3 -2
  114. package/dist/codebase-index/worker.js +20 -8416
  115. package/dist/codebase-index/writer-admin.d.ts +0 -1
  116. package/dist/codebase-index/writer-search.d.ts +0 -9
  117. package/dist/design.js +2 -299
  118. package/dist/diff.js +4 -256
  119. package/dist/document.js +4 -222
  120. package/dist/e2e.js +5 -476
  121. package/dist/edit.js +14 -10200
  122. package/dist/exec.js +16 -3131
  123. package/dist/fetch.js +8 -398
  124. package/dist/format.js +10 -3750
  125. package/dist/generic-parser-VUXDIAOZ.js +13 -0
  126. package/dist/git.js +4 -380
  127. package/dist/glob.js +6 -280
  128. package/dist/go-parser-JXYKJHRC.js +469 -0
  129. package/dist/grep.js +7 -604
  130. package/dist/index.js +318 -35387
  131. package/dist/install.js +10 -4091
  132. package/dist/json-parser-IBHZ4BZC.js +213 -0
  133. package/dist/json.js +4 -665
  134. package/dist/kanban-contract-actions.d.ts +1 -1
  135. package/dist/kanban-detail-actions.d.ts +2 -1
  136. package/dist/kanban-presence.d.ts +2 -1
  137. package/dist/kanban-serializer.d.ts +0 -8
  138. package/dist/kanban-split-task-handler.d.ts +2 -2
  139. package/dist/kanban-tool-types.d.ts +2 -1
  140. package/dist/kanban.js +3 -2096
  141. package/dist/languages/diagnostics.d.ts +2 -1
  142. package/dist/languages/index.js +23 -4664
  143. package/dist/languages/legacy-bridge.d.ts +3 -2
  144. package/dist/languages/types.d.ts +2 -1
  145. package/dist/legacy-bridge-7M6QLUQN.js +18 -0
  146. package/dist/lint.js +10 -3706
  147. package/dist/logs.js +4 -287
  148. package/dist/memory.js +6 -215
  149. package/dist/mode.js +3 -99
  150. package/dist/outdated.js +4 -5170
  151. package/dist/pack.js +65 -33897
  152. package/dist/parser-dispatch-QH4XPGG6.js +14 -0
  153. package/dist/patch.js +13 -9935
  154. package/dist/plan.js +5 -3286
  155. package/dist/process-registry.js +8 -651
  156. package/dist/ps-slash.js +12 -1439
  157. package/dist/pwsh.d.ts +0 -1
  158. package/dist/py-parser-IS4HKYMF.js +16 -0
  159. package/dist/read.js +13 -9816
  160. package/dist/replace.js +15 -10014
  161. package/dist/rs-parser-XHUUO2KZ.js +81 -0
  162. package/dist/scaffold.js +4 -253
  163. package/dist/search.js +5 -585
  164. package/dist/session-kanban-sync.d.ts +0 -6
  165. package/dist/session-kanban.js +30 -972
  166. package/dist/skill.js +2 -215
  167. package/dist/spawn-background.d.ts +1 -1
  168. package/dist/task.js +5 -3410
  169. package/dist/test.js +10 -3773
  170. package/dist/todo.js +4 -2891
  171. package/dist/tool-help.js +3 -123
  172. package/dist/tool-search.js +3 -79
  173. package/dist/tool-tier.js +68 -33984
  174. package/dist/tool-use.js +2 -93
  175. package/dist/tree-sitter-parser-AIERW3YI.js +15 -0
  176. package/dist/tree.js +4 -288
  177. package/dist/ts-parser-RCTYKSVI.js +232 -0
  178. package/dist/typecheck.js +10 -3753
  179. package/dist/win32.js +6 -60
  180. package/dist/write.js +14 -9882
  181. package/dist/yaml-parser-4SAXV3PL.js +157 -0
  182. package/package.json +5 -5
@@ -0,0 +1,2830 @@
1
+ import {
2
+ parseTreeSitterAst
3
+ } from "./chunk-EEOFQDT6.js";
4
+ import {
5
+ spawnStream
6
+ } from "./chunk-22B7GE5T.js";
7
+ import {
8
+ codebaseIndexStats,
9
+ enqueueReindex,
10
+ getIndexState,
11
+ incomingCallsService,
12
+ isIndexing,
13
+ outgoingCallsService,
14
+ runStartupIndex,
15
+ searchCodebaseIndex
16
+ } from "./chunk-HYX2TZ7O.js";
17
+ import {
18
+ IndexTimeoutError,
19
+ SCHEMA_VERSION,
20
+ codebaseIndexDirOverride,
21
+ indexCircuitBreaker,
22
+ indexStorePool,
23
+ resolveIndexDir
24
+ } from "./chunk-DA4KNE2H.js";
25
+ import {
26
+ EXT_TO_LANG,
27
+ detectLang,
28
+ isIndexablePath
29
+ } from "./chunk-TEBQ4VPZ.js";
30
+ import {
31
+ normalizeCommandOutput
32
+ } from "./chunk-D33S4RQG.js";
33
+
34
+ // src/codebase-index/codebase-ast-replace-tool.ts
35
+ import * as path2 from "node:path";
36
+ import { toErrorMessage } from "@wrongstack/core/utils";
37
+
38
+ // src/codebase-index/ast-symbol-mutator.ts
39
+ import * as fs from "node:fs/promises";
40
+ import * as path from "node:path";
41
+ import { atomicWrite } from "@wrongstack/core/utils";
42
+
43
+ // src/codebase-index/ast-invariant-engine.ts
44
+ var tsModule = null;
45
+ async function loadTs() {
46
+ if (!tsModule) {
47
+ const mod = await import("@typescript/typescript6");
48
+ tsModule = mod.default ?? mod;
49
+ }
50
+ return tsModule;
51
+ }
52
+ var PolyglotInvariantEngine = class {
53
+ /**
54
+ * Evaluate whether a code mutation respects backward compatibility invariants.
55
+ */
56
+ async evaluate(opts) {
57
+ const startMs = Date.now();
58
+ const detected = opts.filePath ? detectLang(opts.filePath) : null;
59
+ const lang = opts.lang ?? detected ?? "ts";
60
+ const [origContract, modContract] = await Promise.all([
61
+ this.extractContract(opts.originalCode, lang),
62
+ this.extractContract(opts.modifiedCode, lang)
63
+ ]);
64
+ const violations = [];
65
+ for (const exp of origContract.exports) {
66
+ if (!modContract.exports.has(exp)) {
67
+ violations.push({
68
+ ruleId: "INV-001",
69
+ symbolName: exp,
70
+ lang,
71
+ severity: "CRITICAL",
72
+ message: `Public/exported symbol '${exp}' was deleted or renamed in ${lang.toUpperCase()}. Dependent modules or consumers will break.`
73
+ });
74
+ }
75
+ }
76
+ for (const [name, origFunc] of origContract.functions) {
77
+ const modFunc = modContract.functions.get(name);
78
+ if (!modFunc) continue;
79
+ const origRequired = origFunc.params.filter(
80
+ (p) => !p.isOptional && !p.hasDefault && !p.isRest
81
+ );
82
+ const modRequired = modFunc.params.filter((p) => !p.isOptional && !p.hasDefault && !p.isRest);
83
+ if (lang === "go" || lang === "rs") {
84
+ if (modFunc.params.length > origFunc.params.length) {
85
+ const addedParam = modFunc.params[origFunc.params.length];
86
+ violations.push({
87
+ ruleId: "INV-002",
88
+ symbolName: name,
89
+ lang,
90
+ severity: "CRITICAL",
91
+ message: `Function/method '${name}' in ${lang.toUpperCase()} had new parameter ('${addedParam?.name ?? "param"}') added. In ${lang.toUpperCase()}, signatures cannot take optional parameters without breaking all existing callers. Introduce a new function or options struct instead.`,
92
+ details: {
93
+ addedParameter: addedParam?.name
94
+ }
95
+ });
96
+ }
97
+ } else if (modRequired.length > origRequired.length) {
98
+ const addedReq = modFunc.params.find(
99
+ (p, i) => !p.isOptional && !p.hasDefault && !p.isRest && i >= origRequired.length
100
+ ) ?? modRequired[modRequired.length - 1];
101
+ violations.push({
102
+ ruleId: "INV-002",
103
+ symbolName: name,
104
+ lang,
105
+ severity: "CRITICAL",
106
+ message: `Function '${name}' had new mandatory parameter ('${addedReq?.name}') added. For backward compatibility, make it optional (e.g. '?', default value '= value', or '*args/**kwargs').`,
107
+ details: {
108
+ addedParameter: addedReq?.name
109
+ }
110
+ });
111
+ }
112
+ if (origFunc.returnType && modFunc.returnType && origFunc.returnType !== modFunc.returnType) {
113
+ const isBreakingAsync = origFunc.returnType.includes("Promise<") && !modFunc.returnType.includes("Promise<");
114
+ const isBreakingResult = origFunc.returnType.includes("Result<") && !modFunc.returnType.includes("Result<");
115
+ if (isBreakingAsync || isBreakingResult) {
116
+ violations.push({
117
+ ruleId: "INV-004",
118
+ symbolName: name,
119
+ lang,
120
+ severity: "CRITICAL",
121
+ message: `Return type of '${name}' changed incompatibly from '${origFunc.returnType}' to '${modFunc.returnType}'. Callers awaiting or unpacking results will fail.`,
122
+ details: {
123
+ originalSignature: origFunc.returnType,
124
+ modifiedSignature: modFunc.returnType
125
+ }
126
+ });
127
+ }
128
+ }
129
+ }
130
+ for (const [name, origIface] of origContract.interfaces) {
131
+ const modIface = modContract.interfaces.get(name);
132
+ if (!modIface) continue;
133
+ const origPropNames = new Set(origIface.properties.map((p) => p.name));
134
+ for (const prop of modIface.properties) {
135
+ if (!origPropNames.has(prop.name) && !prop.isOptional && !prop.hasDefault) {
136
+ violations.push({
137
+ ruleId: "INV-003",
138
+ symbolName: `${name}.${prop.name}`,
139
+ lang,
140
+ severity: "CRITICAL",
141
+ message: `Interface/model '${name}' had new mandatory property ('${prop.name}') added. All implementations and mock objects will break. Mark it optional ('${prop.name}?') or provide a default value.`,
142
+ details: {
143
+ addedProperty: prop.name
144
+ }
145
+ });
146
+ }
147
+ }
148
+ const origMethodNames = new Set(origIface.methods.map((m) => m.name));
149
+ for (const method of modIface.methods) {
150
+ if (!origMethodNames.has(method.name) && !method.isOptional) {
151
+ violations.push({
152
+ ruleId: "INV-003",
153
+ symbolName: `${name}.${method.name}()`,
154
+ lang,
155
+ severity: "CRITICAL",
156
+ message: `Interface/trait '${name}' had new mandatory method ('${method.name}') added. Existing implementors will fail to compile.`,
157
+ details: {
158
+ addedProperty: method.name
159
+ }
160
+ });
161
+ }
162
+ }
163
+ }
164
+ const durationMs = Date.now() - startMs;
165
+ return {
166
+ valid: violations.length === 0,
167
+ violations,
168
+ stats: {
169
+ originalSymbols: origContract.functions.size + origContract.interfaces.size,
170
+ modifiedSymbols: modContract.functions.size + modContract.interfaces.size,
171
+ durationMs
172
+ }
173
+ };
174
+ }
175
+ /**
176
+ * Extract high-level contract signatures from source code.
177
+ */
178
+ async extractContract(code, lang) {
179
+ if (["ts", "tsx", "js", "jsx"].includes(lang)) {
180
+ try {
181
+ return await this.extractTsContract(code, lang);
182
+ } catch {
183
+ }
184
+ }
185
+ return await this.extractTreeSitterContract(code, lang);
186
+ }
187
+ /**
188
+ * TypeScript & JavaScript extraction via TS Compiler API.
189
+ */
190
+ async extractTsContract(code, lang) {
191
+ const ts = await loadTs();
192
+ const scriptKind = lang === "tsx" ? ts.ScriptKind.TSX : lang === "jsx" ? ts.ScriptKind.JSX : lang === "js" ? ts.ScriptKind.JS : ts.ScriptKind.TS;
193
+ const sourceFile = ts.createSourceFile(
194
+ lang === "tsx" ? "contract.tsx" : "contract.ts",
195
+ code,
196
+ ts.ScriptTarget.Latest,
197
+ true,
198
+ scriptKind
199
+ );
200
+ const exports = /* @__PURE__ */ new Set();
201
+ const functions = /* @__PURE__ */ new Map();
202
+ const interfaces = /* @__PURE__ */ new Map();
203
+ 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
+ }
209
+ }
210
+ return false;
211
+ }
212
+ 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);
216
+ }
217
+ }
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 };
248
+ });
249
+ const returnType = member.type ? member.type.getText(sourceFile) : void 0;
250
+ functions.set(fullMethodName, {
251
+ name: fullMethodName,
252
+ isExported: isExp,
253
+ params,
254
+ returnType
255
+ });
256
+ functions.set(methodName, {
257
+ name: methodName,
258
+ isExported: isExp,
259
+ params,
260
+ returnType
261
+ });
262
+ }
263
+ }
264
+ }
265
+ if (ts.isInterfaceDeclaration(node) && node.name) {
266
+ const name = node.name.text;
267
+ const isExp = isExportedNode(node);
268
+ if (isExp) exports.add(name);
269
+ const properties = [];
270
+ const methods = [];
271
+ for (const member of node.members) {
272
+ if (ts.isPropertySignature(member) && member.name) {
273
+ const propName = member.name.getText(sourceFile);
274
+ const isOptional = Boolean(member.questionToken);
275
+ const type = member.type ? member.type.getText(sourceFile) : void 0;
276
+ properties.push({ name: propName, isOptional, type });
277
+ } else if (ts.isMethodSignature(member) && member.name) {
278
+ const methodName = member.name.getText(sourceFile);
279
+ const isOptional = Boolean(member.questionToken);
280
+ const params = member.parameters.map((p) => ({
281
+ name: p.name.getText(sourceFile),
282
+ isOptional: Boolean(p.questionToken),
283
+ hasDefault: Boolean(p.initializer),
284
+ isRest: Boolean(p.dotDotDotToken)
285
+ }));
286
+ const returnType = member.type ? member.type.getText(sourceFile) : void 0;
287
+ methods.push({ name: methodName, isOptional, params, returnType });
288
+ }
289
+ }
290
+ interfaces.set(name, { name, isExported: isExp, properties, methods });
291
+ }
292
+ if (ts.isTypeAliasDeclaration(node) && node.name && ts.isTypeLiteralNode(node.type)) {
293
+ const name = node.name.text;
294
+ const isExp = isExportedNode(node);
295
+ if (isExp) exports.add(name);
296
+ const properties = [];
297
+ for (const member of node.type.members) {
298
+ if (ts.isPropertySignature(member) && member.name) {
299
+ const propName = member.name.getText(sourceFile);
300
+ const isOptional = Boolean(member.questionToken);
301
+ const type = member.type ? member.type.getText(sourceFile) : void 0;
302
+ properties.push({ name: propName, isOptional, type });
303
+ }
304
+ }
305
+ interfaces.set(name, { name, isExported: isExp, properties, methods: [] });
306
+ }
307
+ ts.forEachChild(node, visit);
308
+ }
309
+ ts.forEachChild(sourceFile, visit);
310
+ return { lang, exports, functions, interfaces };
311
+ }
312
+ /**
313
+ * Polyglot extraction (Python, Go, Rust, Java, etc.) via Tree-sitter WASM.
314
+ */
315
+ async extractTreeSitterContract(code, lang) {
316
+ const exports = /* @__PURE__ */ new Set();
317
+ const functions = /* @__PURE__ */ new Map();
318
+ const interfaces = /* @__PURE__ */ new Map();
319
+ const parsed = await parseTreeSitterAst({ content: code, lang });
320
+ if (!parsed) {
321
+ return { lang, exports, functions, interfaces };
322
+ }
323
+ const { tree, parser } = parsed;
324
+ try {
325
+ let walk2 = function(node) {
326
+ const type = node.type;
327
+ if (lang === "py") {
328
+ if (type === "function_definition") {
329
+ const nameNode = node.childForFieldName("name");
330
+ if (nameNode) {
331
+ const name = nameNode.text;
332
+ const isExported = !name.startsWith("_");
333
+ if (isExported) exports.add(name);
334
+ const paramsNode = node.childForFieldName("parameters");
335
+ const params = [];
336
+ if (paramsNode) {
337
+ for (let i = 0; i < paramsNode.namedChildCount; i++) {
338
+ const param = paramsNode.namedChild(i);
339
+ if (!param) continue;
340
+ const pText = param.text;
341
+ const isRest = pText.startsWith("*");
342
+ const hasDefault = param.type === "default_parameter" || param.type === "typed_default_parameter" || pText.includes("=");
343
+ const idNode = param.childForFieldName("name") ?? param;
344
+ params.push({
345
+ name: idNode.text.split(":")[0]?.trim() ?? idNode.text,
346
+ isOptional: hasDefault || isRest,
347
+ hasDefault,
348
+ isRest
349
+ });
350
+ }
351
+ }
352
+ functions.set(name, { name, isExported, params });
353
+ }
354
+ } else if (type === "class_definition") {
355
+ const nameNode = node.childForFieldName("name");
356
+ if (nameNode) {
357
+ const name = nameNode.text;
358
+ if (!name.startsWith("_")) exports.add(name);
359
+ }
360
+ }
361
+ } else if (lang === "go") {
362
+ if (type === "function_declaration" || type === "method_declaration") {
363
+ const nameNode = node.childForFieldName("name");
364
+ if (nameNode) {
365
+ const name = nameNode.text;
366
+ const isExported = /^[A-Z]/.test(name);
367
+ if (isExported) exports.add(name);
368
+ const paramsNode = node.childForFieldName("parameters");
369
+ const params = [];
370
+ if (paramsNode) {
371
+ for (let i = 0; i < paramsNode.namedChildCount; i++) {
372
+ const param = paramsNode.namedChild(i);
373
+ if (!param) continue;
374
+ params.push({
375
+ name: param.text,
376
+ isOptional: false,
377
+ hasDefault: false,
378
+ isRest: param.type === "variadic_parameter_declaration"
379
+ });
380
+ }
381
+ }
382
+ functions.set(name, { name, isExported, params });
383
+ }
384
+ } else if (type === "type_spec") {
385
+ const nameNode = node.childForFieldName("name");
386
+ const typeNode = node.childForFieldName("type");
387
+ if (nameNode) {
388
+ const name = nameNode.text;
389
+ const isExported = /^[A-Z]/.test(name);
390
+ if (isExported) exports.add(name);
391
+ if (typeNode && typeNode.type === "interface_type") {
392
+ const methods = [];
393
+ for (let i = 0; i < typeNode.namedChildCount; i++) {
394
+ const elem = typeNode.namedChild(i);
395
+ if (elem && (elem.type === "method_spec" || elem.type === "method_elem")) {
396
+ const mName = elem.childForFieldName("name")?.text ?? elem.text.split("(")[0]?.trim() ?? "";
397
+ if (mName) {
398
+ methods.push({ name: mName, isOptional: false, params: [] });
399
+ }
400
+ }
401
+ }
402
+ interfaces.set(name, { name, isExported, properties: [], methods });
403
+ }
404
+ }
405
+ }
406
+ } else if (lang === "rs") {
407
+ if (type === "function_item") {
408
+ const nameNode = node.childForFieldName("name");
409
+ if (nameNode) {
410
+ const name = nameNode.text;
411
+ const isExported = node.children.some((c) => c.type === "visibility_modifier");
412
+ if (isExported) exports.add(name);
413
+ const paramsNode = node.childForFieldName("parameters");
414
+ const params = [];
415
+ if (paramsNode) {
416
+ for (let i = 0; i < paramsNode.namedChildCount; i++) {
417
+ const param = paramsNode.namedChild(i);
418
+ if (!param) continue;
419
+ params.push({
420
+ name: param.text,
421
+ isOptional: false,
422
+ hasDefault: false,
423
+ isRest: false
424
+ });
425
+ }
426
+ }
427
+ functions.set(name, { name, isExported, params });
428
+ }
429
+ } else if (type === "trait_item") {
430
+ const nameNode = node.childForFieldName("name");
431
+ if (nameNode) {
432
+ const name = nameNode.text;
433
+ const isExported = node.children.some((c) => c.type === "visibility_modifier");
434
+ if (isExported) exports.add(name);
435
+ const methods = [];
436
+ const bodyNode = node.childForFieldName("body");
437
+ if (bodyNode) {
438
+ for (let i = 0; i < bodyNode.namedChildCount; i++) {
439
+ const child = bodyNode.namedChild(i);
440
+ if (child && child.type === "function_item") {
441
+ const mName = child.childForFieldName("name")?.text;
442
+ const hasDefaultBody = Boolean(child.childForFieldName("body"));
443
+ if (mName) {
444
+ methods.push({
445
+ name: mName,
446
+ isOptional: hasDefaultBody,
447
+ params: []
448
+ });
449
+ }
450
+ }
451
+ }
452
+ }
453
+ interfaces.set(name, { name, isExported, properties: [], methods });
454
+ }
455
+ }
456
+ }
457
+ for (let i = 0; i < node.namedChildCount; i++) {
458
+ const child = node.namedChild(i);
459
+ if (child) walk2(child);
460
+ }
461
+ };
462
+ var walk = walk2;
463
+ const root = tree.rootNode;
464
+ walk2(root);
465
+ return { lang, exports, functions, interfaces };
466
+ } finally {
467
+ tree.delete();
468
+ parser.delete();
469
+ }
470
+ }
471
+ };
472
+ var polyglotInvariantEngine = new PolyglotInvariantEngine();
473
+
474
+ // src/codebase-index/ast-symbol-mutator.ts
475
+ var tsModule2 = null;
476
+ async function loadTs2() {
477
+ if (!tsModule2) {
478
+ const mod = await import("@typescript/typescript6");
479
+ tsModule2 = mod.default ?? mod;
480
+ }
481
+ return tsModule2;
482
+ }
483
+ async function mutateTsSymbol(content, lang, symbolName, newBody, target = "body") {
484
+ const ts = await loadTs2();
485
+ const scriptKind = lang === "tsx" ? ts.ScriptKind.TSX : lang === "jsx" ? ts.ScriptKind.JSX : lang === "js" ? ts.ScriptKind.JS : ts.ScriptKind.TS;
486
+ const sourceFile = ts.createSourceFile(
487
+ lang === "tsx" ? "temp.tsx" : "temp.ts",
488
+ content,
489
+ ts.ScriptTarget.Latest,
490
+ true,
491
+ scriptKind
492
+ );
493
+ let targetNode = null;
494
+ let bodyNode = null;
495
+ function visit(node) {
496
+ if (targetNode) return;
497
+ if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
498
+ let name;
499
+ if ("name" in node && node.name && ts.isIdentifier(node.name)) {
500
+ name = node.name.text;
501
+ }
502
+ if (name === symbolName) {
503
+ targetNode = node;
504
+ if ("body" in node && node.body) {
505
+ bodyNode = node.body;
506
+ }
507
+ return;
508
+ }
509
+ } else if (ts.isVariableStatement(node)) {
510
+ for (const decl of node.declarationList.declarations) {
511
+ if (ts.isIdentifier(decl.name) && decl.name.text === symbolName) {
512
+ targetNode = decl;
513
+ if (decl.initializer && (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))) {
514
+ bodyNode = decl.initializer.body;
515
+ }
516
+ return;
517
+ }
518
+ }
519
+ }
520
+ ts.forEachChild(node, visit);
521
+ }
522
+ ts.forEachChild(sourceFile, visit);
523
+ if (!targetNode) return null;
524
+ const replaceTarget = target === "full" || !bodyNode ? targetNode : bodyNode;
525
+ const startPos = replaceTarget.getStart(sourceFile);
526
+ const endPos = replaceTarget.getEnd();
527
+ const startLine = sourceFile.getLineAndCharacterOfPosition(startPos).line + 1;
528
+ const endLine = sourceFile.getLineAndCharacterOfPosition(endPos).line + 1;
529
+ let formattedReplacement = newBody.trim();
530
+ if (target === "body" && bodyNode && ts.isBlock(bodyNode)) {
531
+ if (!formattedReplacement.startsWith("{")) {
532
+ formattedReplacement = `{
533
+ ${formattedReplacement.split("\n").join("\n ")}
534
+ }`;
535
+ }
536
+ }
537
+ const updatedContent = content.slice(0, startPos) + formattedReplacement + content.slice(endPos);
538
+ const newEndLine = startLine + formattedReplacement.split("\n").length - 1;
539
+ return {
540
+ file: "",
541
+ symbol: symbolName,
542
+ originalRange: { startLine, endLine },
543
+ newRange: { startLine, endLine: newEndLine },
544
+ updatedContent
545
+ };
546
+ }
547
+ async function mutateTreeSitterSymbol(content, lang, symbolName, newBody, target = "body") {
548
+ const parsed = await parseTreeSitterAst({ content, lang });
549
+ if (!parsed) return null;
550
+ const { tree, parser } = parsed;
551
+ try {
552
+ let walk2 = function(node) {
553
+ if (targetNode) return;
554
+ const nameNode = node.childForFieldName("name") ?? node.childForFieldName("declarator");
555
+ if (nameNode && nameNode.text === symbolName) {
556
+ targetNode = node;
557
+ bodyNode = node.childForFieldName("body") ?? node.children.find(
558
+ (c) => [
559
+ "compound_statement",
560
+ "statement_block",
561
+ "block",
562
+ "function_body",
563
+ "body_statement",
564
+ "code_block",
565
+ "do_block"
566
+ ].includes(c.type)
567
+ ) ?? null;
568
+ return;
569
+ }
570
+ for (let i = 0; i < node.namedChildCount; i++) {
571
+ const child = node.namedChild(i);
572
+ if (child) walk2(child);
573
+ }
574
+ };
575
+ var walk = walk2;
576
+ const root = tree.rootNode;
577
+ let targetNode = null;
578
+ let bodyNode = null;
579
+ walk2(root);
580
+ if (!targetNode) return null;
581
+ const replaceTarget = target === "full" || !bodyNode ? targetNode : bodyNode;
582
+ const startPos = replaceTarget.startIndex;
583
+ const endPos = replaceTarget.endIndex;
584
+ const startLine = replaceTarget.startPosition.row + 1;
585
+ const endLine = replaceTarget.endPosition.row + 1;
586
+ let formattedReplacement = newBody.trim();
587
+ if (target === "body" && lang === "py") {
588
+ const indent = " ";
589
+ formattedReplacement = formattedReplacement.split("\n").map((l) => l.trim() ? `${indent}${l}` : "").join("\n");
590
+ }
591
+ const updatedContent = content.slice(0, startPos) + formattedReplacement + content.slice(endPos);
592
+ const newEndLine = startLine + formattedReplacement.split("\n").length - 1;
593
+ return {
594
+ file: "",
595
+ symbol: symbolName,
596
+ originalRange: { startLine, endLine },
597
+ newRange: { startLine, endLine: newEndLine },
598
+ updatedContent
599
+ };
600
+ } finally {
601
+ tree.delete();
602
+ parser.delete();
603
+ }
604
+ }
605
+ async function replaceSymbolInFile(opts, projectRoot) {
606
+ const resolved = path.isAbsolute(opts.file) ? opts.file : path.resolve(projectRoot, opts.file);
607
+ const content = await fs.readFile(resolved, "utf8");
608
+ const lang = detectLang(resolved) ?? "other";
609
+ let result = null;
610
+ if (["ts", "tsx", "js", "jsx"].includes(lang)) {
611
+ result = await mutateTsSymbol(content, lang, opts.symbol, opts.newBody, opts.target);
612
+ } else {
613
+ result = await mutateTreeSitterSymbol(content, lang, opts.symbol, opts.newBody, opts.target);
614
+ }
615
+ if (!result) {
616
+ throw new Error(
617
+ `Symbol '${opts.symbol}' could not be located in AST of '${path.relative(projectRoot, resolved)}'`
618
+ );
619
+ }
620
+ result.file = resolved;
621
+ if (opts.checkInvariants !== false) {
622
+ const inv = await polyglotInvariantEngine.evaluate({
623
+ originalCode: content,
624
+ modifiedCode: result.updatedContent,
625
+ lang,
626
+ filePath: resolved
627
+ });
628
+ if (!inv.valid && !opts.allowBreakingChanges) {
629
+ const summary = inv.violations.map((v) => `[${v.ruleId}] ${v.message}`).join("\n");
630
+ throw new Error(
631
+ `AST Invariant Violation: Mutating '${opts.symbol}' introduces breaking changes:
632
+ ${summary}
633
+ Pass allowBreakingChanges: true if this breaking change is intended.`
634
+ );
635
+ }
636
+ result.violations = inv.violations;
637
+ }
638
+ await atomicWrite(resolved, result.updatedContent);
639
+ try {
640
+ enqueueReindex({ projectRoot, files: [resolved] });
641
+ } catch {
642
+ }
643
+ return result;
644
+ }
645
+
646
+ // src/codebase-index/codebase-ast-replace-tool.ts
647
+ var codebaseAstReplaceTool = {
648
+ name: "codebase-ast-replace",
649
+ category: "Edit",
650
+ icon: "edit",
651
+ permission: "confirm",
652
+ subjectKey: "file",
653
+ mutating: true,
654
+ capabilities: ["fs.write", "fs.read"],
655
+ description: "Surgically replace the implementation body or full definition of a function, method, class, or type alias using AST parsing. Guarantees zero string-matching errors, regex escape bugs, or whitespace indentation mismatches. Automatically verifies AST backward-compatibility invariants (no mandatory param addition, no export deletion, no interface breaking).",
656
+ usageHint: 'SURGICAL AST-BASED CODE EDITING:\n\n- Use when updating an existing function, method, or class without worrying about context lines.\n- Pass `symbol: "calculateTotal"` and `newBody: "return items.reduce(...);"`.\n- Default `target: "body"` replaces only the inner `{ ... }` block while preserving the signature and JSDoc.\n- Use `target: "full"` if you also need to modify the function parameters or return type.\n- Setting `allowBreakingChanges: true` bypasses the invariant engine if breaking changes are deliberate.',
657
+ inputSchema: {
658
+ type: "object",
659
+ properties: {
660
+ file: {
661
+ type: "string",
662
+ description: "Target file path (relative to project root or absolute)."
663
+ },
664
+ symbol: {
665
+ type: "string",
666
+ description: "Name of the function, method, class, interface, or variable to replace."
667
+ },
668
+ newBody: {
669
+ type: "string",
670
+ description: 'The new body implementation (or complete declaration if target="full").'
671
+ },
672
+ target: {
673
+ type: "string",
674
+ enum: ["body", "full"],
675
+ description: "Whether to replace only the inner body ('body', default) or the entire symbol declaration ('full')."
676
+ },
677
+ checkInvariants: {
678
+ type: "boolean",
679
+ description: "Whether to check AST backward-compatibility invariants before write. Defaults to true."
680
+ },
681
+ allowBreakingChanges: {
682
+ type: "boolean",
683
+ description: "Explicitly allow breaking changes (e.g. adding mandatory parameter, removing export) if intentional."
684
+ }
685
+ },
686
+ required: ["file", "symbol", "newBody"],
687
+ additionalProperties: false
688
+ },
689
+ async execute(input, ctx) {
690
+ try {
691
+ const projectRoot = ctx.projectRoot;
692
+ const result = await replaceSymbolInFile(input, projectRoot);
693
+ return {
694
+ status: "ok",
695
+ file: path2.relative(projectRoot, result.file).replace(/\\/g, "/"),
696
+ symbol: result.symbol,
697
+ originalRange: result.originalRange,
698
+ newRange: result.newRange
699
+ };
700
+ } catch (err) {
701
+ return {
702
+ status: "error",
703
+ file: input.file,
704
+ symbol: input.symbol,
705
+ error: toErrorMessage(err)
706
+ };
707
+ }
708
+ }
709
+ };
710
+
711
+ // src/codebase-index/codebase-invariant-check-tool.ts
712
+ import * as fs2 from "node:fs/promises";
713
+ import * as path3 from "node:path";
714
+ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
715
+ var codebaseInvariantCheckTool = {
716
+ name: "codebase-invariant-check",
717
+ category: "Inspect",
718
+ icon: "index",
719
+ permission: "auto",
720
+ subjectKey: "file",
721
+ mutating: false,
722
+ capabilities: ["fs.read"],
723
+ 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.",
724
+ 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.",
725
+ inputSchema: {
726
+ type: "object",
727
+ properties: {
728
+ file: {
729
+ type: "string",
730
+ description: "Path to existing source file to compare against candidate modifications."
731
+ },
732
+ originalCode: {
733
+ type: "string",
734
+ description: "Original source code string (if not reading from an existing file)."
735
+ },
736
+ modifiedCode: {
737
+ type: "string",
738
+ description: "The candidate modified code string to validate."
739
+ },
740
+ lang: {
741
+ type: "string",
742
+ description: "Language hint (ts, py, go, rs, etc.). Inferred from file path if omitted."
743
+ }
744
+ },
745
+ required: ["modifiedCode"],
746
+ additionalProperties: false
747
+ },
748
+ async execute(input, ctx) {
749
+ try {
750
+ let originalCode = input.originalCode;
751
+ let lang = input.lang;
752
+ if (!originalCode && input.file) {
753
+ const resolved = path3.isAbsolute(input.file) ? input.file : path3.resolve(ctx.projectRoot, input.file);
754
+ originalCode = await fs2.readFile(resolved, "utf8");
755
+ if (!lang) {
756
+ lang = detectLang(resolved) ?? "ts";
757
+ }
758
+ }
759
+ if (originalCode === void 0) {
760
+ return {
761
+ valid: false,
762
+ violations: [],
763
+ summary: "Neither originalCode nor file was provided for invariant comparison.",
764
+ error: "Missing originalCode or file path."
765
+ };
766
+ }
767
+ const res = await polyglotInvariantEngine.evaluate({
768
+ originalCode,
769
+ modifiedCode: input.modifiedCode,
770
+ lang,
771
+ filePath: input.file
772
+ });
773
+ const summary = res.valid ? "AST Invariants PASSED. Mutation is 100% backward compatible." : `AST Invariants FAILED with ${res.violations.length} violation(s):
774
+ ` + res.violations.map((v) => ` - [${v.ruleId}] ${v.symbolName}: ${v.message}`).join("\n");
775
+ return {
776
+ valid: res.valid,
777
+ violations: res.violations,
778
+ summary
779
+ };
780
+ } catch (err) {
781
+ return {
782
+ valid: false,
783
+ violations: [],
784
+ summary: `Error during invariant evaluation: ${toErrorMessage2(err)}`,
785
+ error: toErrorMessage2(err)
786
+ };
787
+ }
788
+ }
789
+ };
790
+
791
+ // src/codebase-index/codebase-impact-analysis-tool.ts
792
+ import * as path4 from "node:path";
793
+ import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
794
+ var TEST_FILE_REGEX = /(?:test|spec|mock|fixture|bench)[s]?[/\\._]/i;
795
+ var codebaseImpactAnalysisTool = {
796
+ name: "codebase-impact-analysis",
797
+ category: "Project",
798
+ icon: "index",
799
+ permission: "auto",
800
+ mutating: false,
801
+ capabilities: ["fs.read"],
802
+ 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.",
803
+ 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.',
804
+ inputSchema: {
805
+ type: "object",
806
+ properties: {
807
+ symbol: {
808
+ type: "string",
809
+ description: "The function/method/class/type name to analyze."
810
+ },
811
+ file: {
812
+ type: "string",
813
+ description: "Optional file path to disambiguate when multiple symbols share the same name."
814
+ },
815
+ transitive: {
816
+ type: "boolean",
817
+ description: "Whether to traverse transitive callers (callers of callers). Defaults to true."
818
+ }
819
+ },
820
+ required: ["symbol"],
821
+ additionalProperties: false
822
+ },
823
+ async execute(input, ctx) {
824
+ try {
825
+ const projectRoot = ctx.projectRoot;
826
+ const indexDir = codebaseIndexDirOverride(ctx);
827
+ const limit = 200;
828
+ const transitive = input.transitive ?? true;
829
+ let rawSites = [];
830
+ let symbolFound = true;
831
+ let indexUnavailable = false;
832
+ let indexError;
833
+ try {
834
+ const serviced = await incomingCallsService({
835
+ projectRoot,
836
+ indexDir,
837
+ symbol: input.symbol,
838
+ file: input.file,
839
+ limit,
840
+ transitive
841
+ });
842
+ rawSites = serviced.calls;
843
+ symbolFound = serviced.symbolFound;
844
+ } catch (err) {
845
+ symbolFound = false;
846
+ indexUnavailable = true;
847
+ indexError = toErrorMessage3(err);
848
+ }
849
+ if (indexUnavailable) {
850
+ const detail = indexError ? ` (${indexError})` : "";
851
+ const reason = `Index query failed for '${input.symbol}'. Run codebase-index, then retry.${detail}`;
852
+ return {
853
+ status: "error",
854
+ symbol: input.symbol,
855
+ riskLevel: "low",
856
+ summary: reason,
857
+ totalCallSites: 0,
858
+ affectedProductionFiles: [],
859
+ affectedTestFiles: [],
860
+ callSites: [],
861
+ recommendedActionPlan: [reason],
862
+ symbolFound: false,
863
+ error: reason
864
+ };
865
+ }
866
+ if (!symbolFound) {
867
+ 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.";
868
+ return {
869
+ status: "ok",
870
+ symbol: input.symbol,
871
+ riskLevel: "low",
872
+ summary: reason,
873
+ totalCallSites: 0,
874
+ affectedProductionFiles: [],
875
+ affectedTestFiles: [],
876
+ callSites: [],
877
+ recommendedActionPlan: [reason],
878
+ symbolFound: false
879
+ };
880
+ }
881
+ const prodFilesSet = /* @__PURE__ */ new Set();
882
+ const testFilesSet = /* @__PURE__ */ new Set();
883
+ const callSites = [];
884
+ for (const site of rawSites) {
885
+ const relPath = path4.relative(projectRoot, site.symbol.file).replace(/\\/g, "/");
886
+ const isTest = TEST_FILE_REGEX.test(relPath);
887
+ if (isTest) {
888
+ testFilesSet.add(relPath);
889
+ } else {
890
+ prodFilesSet.add(relPath);
891
+ }
892
+ callSites.push({
893
+ file: relPath,
894
+ line: site.line,
895
+ callerName: site.symbol.name,
896
+ callerKind: site.symbol.kind,
897
+ callType: site.callType,
898
+ isTest
899
+ });
900
+ }
901
+ const prodFiles = [...prodFilesSet];
902
+ const testFiles = [...testFilesSet];
903
+ const totalCallSites = callSites.length;
904
+ let riskLevel = "low";
905
+ if (prodFiles.length > 5 || totalCallSites > 10) {
906
+ riskLevel = "high";
907
+ } else if (prodFiles.length > 1 || totalCallSites > 3) {
908
+ riskLevel = "medium";
909
+ }
910
+ const recommendedActionPlan = [];
911
+ if (prodFiles.length > 0) {
912
+ recommendedActionPlan.push(
913
+ `Update ${totalCallSites} call site(s) across ${prodFiles.length} production file(s): ${prodFiles.slice(0, 3).join(", ")}${prodFiles.length > 3 ? ` +${prodFiles.length - 3} more` : ""}`
914
+ );
915
+ }
916
+ if (testFiles.length > 0) {
917
+ recommendedActionPlan.push(
918
+ `Run targeted tests in ${testFiles.length} test suite(s): ${testFiles.join(", ")}`
919
+ );
920
+ } else {
921
+ recommendedActionPlan.push(
922
+ `No existing test files directly cover '${input.symbol}'. Consider adding a test suite.`
923
+ );
924
+ }
925
+ const summary = `Blast Radius for '${input.symbol}': ${riskLevel.toUpperCase()} RISK (${totalCallSites} call sites in ${prodFiles.length} prod files, ${testFiles.length} test suites).`;
926
+ return {
927
+ status: "ok",
928
+ symbol: input.symbol,
929
+ riskLevel,
930
+ summary,
931
+ totalCallSites,
932
+ affectedProductionFiles: prodFiles,
933
+ affectedTestFiles: testFiles,
934
+ callSites,
935
+ recommendedActionPlan,
936
+ symbolFound: true
937
+ };
938
+ } catch (err) {
939
+ return {
940
+ status: "error",
941
+ symbol: input.symbol,
942
+ riskLevel: "low",
943
+ summary: "",
944
+ totalCallSites: 0,
945
+ affectedProductionFiles: [],
946
+ affectedTestFiles: [],
947
+ callSites: [],
948
+ recommendedActionPlan: [],
949
+ symbolFound: false,
950
+ error: toErrorMessage3(err)
951
+ };
952
+ }
953
+ }
954
+ };
955
+
956
+ // src/codebase-index/codebase-incoming-calls-tool.ts
957
+ import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
958
+ var codebaseIncomingCallsTool = {
959
+ name: "codebase-incoming-calls",
960
+ category: "Project",
961
+ icon: "index",
962
+ description: "Find all callers of a function, method, or symbol \u2014 who invokes or references it. Uses the codebase index ref graph for instant, exact results. Prefer this over grep for change-impact checks when the index is available.",
963
+ usageHint: 'CALL THIS BEFORE REFACTORING OR CHANGING ANY FUNCTION:\n\n- Prefer this over grep when the index is available; fall back to grep when the index is cold/unavailable or for dynamic dispatch the ref graph cannot see.\n- Call codebase-incoming-calls({ symbol: "funcName" }) before editing the symbol.\n- Returns exact files, line numbers, caller signatures, and call types in milliseconds.\n- Use `file` to disambiguate when multiple symbols share a name.\n- Combine with codebase-outgoing-calls to see what the symbol itself calls.\nIf the index is not built, run codebase-index first.',
964
+ permission: "auto",
965
+ mutating: false,
966
+ capabilities: ["fs.read"],
967
+ timeoutMs: 35e3,
968
+ inputSchema: {
969
+ type: "object",
970
+ properties: {
971
+ symbol: {
972
+ type: "string",
973
+ description: "The function/method/type name to find callers for"
974
+ },
975
+ file: {
976
+ type: "string",
977
+ description: "Scope to a specific file when multiple symbols share the same name"
978
+ },
979
+ limit: {
980
+ type: "integer",
981
+ description: "Maximum call sites to return (default 50, max 200)",
982
+ minimum: 1,
983
+ maximum: 200
984
+ },
985
+ transitive: {
986
+ type: "boolean",
987
+ description: "When true, traverse the full transitive call chain (callers of callers, to unlimited depth). Default false: return only direct callers. Cycle-safe via SQL recursive CTE.",
988
+ default: false
989
+ }
990
+ },
991
+ required: ["symbol"]
992
+ },
993
+ async execute(input, ctx) {
994
+ const state = getIndexState();
995
+ if (state.lastError) {
996
+ const circuit = state.circuit;
997
+ const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s).` : "Try /codebase-reindex.";
998
+ return {
999
+ symbol: input.symbol,
1000
+ calls: [],
1001
+ total: 0,
1002
+ indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
1003
+ };
1004
+ }
1005
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
1006
+ const transitive = input.transitive === true;
1007
+ let serviced;
1008
+ try {
1009
+ serviced = await incomingCallsService({
1010
+ projectRoot: ctx.projectRoot,
1011
+ indexDir: codebaseIndexDirOverride(ctx),
1012
+ symbol: input.symbol,
1013
+ file: input.file,
1014
+ limit,
1015
+ transitive
1016
+ });
1017
+ } catch (err) {
1018
+ if (err.name === "IndexRefreshInProgressError") {
1019
+ return {
1020
+ symbol: input.symbol,
1021
+ calls: [],
1022
+ total: 0,
1023
+ indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files); this symbol has no cached answer yet \u2014 retry after the completed generation is published.`
1024
+ };
1025
+ }
1026
+ return {
1027
+ symbol: input.symbol,
1028
+ calls: [],
1029
+ total: 0,
1030
+ indexStatus: `Index query failed: ${toErrorMessage4(err)}. Fall back to grep for this lookup.`
1031
+ };
1032
+ }
1033
+ const { calls, symbolFound, ambiguous, totalMatches, stale } = serviced;
1034
+ if (!symbolFound) {
1035
+ let hasPersistedIndex = state.ready;
1036
+ if (!hasPersistedIndex) {
1037
+ try {
1038
+ const stats = await codebaseIndexStats({
1039
+ projectRoot: ctx.projectRoot,
1040
+ indexDir: codebaseIndexDirOverride(ctx)
1041
+ });
1042
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
1043
+ } catch {
1044
+ }
1045
+ }
1046
+ if (!hasPersistedIndex) {
1047
+ return {
1048
+ symbol: input.symbol,
1049
+ calls: [],
1050
+ total: 0,
1051
+ indexStatus: "No persisted index data found. Run codebase-index to build it."
1052
+ };
1053
+ }
1054
+ return {
1055
+ symbol: input.symbol,
1056
+ calls: [],
1057
+ total: 0,
1058
+ note: `Symbol "${input.symbol}" not found in the index. Use codebase-search to verify the name.`
1059
+ };
1060
+ }
1061
+ const notes = [];
1062
+ if (totalMatches > limit) {
1063
+ notes.push(
1064
+ `Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`
1065
+ );
1066
+ }
1067
+ if (ambiguous) {
1068
+ notes.push(
1069
+ `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\`.`
1070
+ );
1071
+ }
1072
+ if (stale) {
1073
+ notes.push(
1074
+ `Index refresh in progress (${state.currentFile}/${state.totalFiles} files); callers served from the previous generation \u2014 call sites in files being indexed may lag.`
1075
+ );
1076
+ }
1077
+ return {
1078
+ symbol: input.symbol,
1079
+ calls,
1080
+ total: calls.length,
1081
+ ...stale ? { stale: true } : {},
1082
+ ...notes.length > 0 ? { note: notes.join(" ") } : {}
1083
+ };
1084
+ }
1085
+ };
1086
+
1087
+ // src/codebase-index/codebase-index-tool.ts
1088
+ import { ToolValidationError } from "@wrongstack/core/types";
1089
+
1090
+ // src/codebase-index/codebase-search-tool.ts
1091
+ import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
1092
+ var INDEXABLE_LANG_IDS = [
1093
+ ...new Set(Object.values(EXT_TO_LANG))
1094
+ ].sort();
1095
+ var codebaseSearchTool = {
1096
+ name: "codebase-search",
1097
+ category: "Project",
1098
+ icon: "index",
1099
+ description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Prefer this before broad `tree`, `glob`, or `grep` exploration when finding code by name or concept. Use `grep` instead for exact text, regexes, unsupported content, or concrete usage sites. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
1100
+ usageHint: "FIRST CHOICE FOR INDEXABLE CODE UNDERSTANDING:\n\n- Call before broad `tree`, `glob`, or `grep` exploration when locating symbols, concepts, definitions, or candidate modules.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\n- If `indexStatus` reports no persisted data, run `codebase-index` and retry.",
1101
+ permission: "auto",
1102
+ mutating: false,
1103
+ capabilities: ["fs.read"],
1104
+ // The index host has its own 30s read watchdog. Leave enough headroom for
1105
+ // worker teardown and structured timeout reporting.
1106
+ timeoutMs: 35e3,
1107
+ inputSchema: {
1108
+ type: "object",
1109
+ properties: {
1110
+ query: {
1111
+ type: "string",
1112
+ description: "Search query \u2014 searches symbol names, signatures, and doc comments"
1113
+ },
1114
+ kind: {
1115
+ type: "string",
1116
+ enum: [
1117
+ "class",
1118
+ "interface",
1119
+ "enum",
1120
+ "type",
1121
+ "function",
1122
+ "method",
1123
+ "var",
1124
+ "const",
1125
+ "let",
1126
+ "property",
1127
+ "parameter",
1128
+ "namespace",
1129
+ "object",
1130
+ "literal",
1131
+ "schema",
1132
+ "struct",
1133
+ "trait",
1134
+ "impl",
1135
+ "static",
1136
+ "mod"
1137
+ ],
1138
+ description: "Filter by indexed symbol kind"
1139
+ },
1140
+ lang: {
1141
+ type: "string",
1142
+ enum: [...INDEXABLE_LANG_IDS],
1143
+ description: "Filter by indexed language"
1144
+ },
1145
+ lspKind: {
1146
+ type: "integer",
1147
+ description: "Filter by LSP SymbolKind number (e.g. 5=Class, 12=Function, 11=Interface, 10=Enum)"
1148
+ },
1149
+ file: {
1150
+ type: "string",
1151
+ description: "Filter to files matching this path substring"
1152
+ },
1153
+ limit: {
1154
+ type: "integer",
1155
+ description: "Maximum results to return (default 20, max 100)",
1156
+ minimum: 1,
1157
+ maximum: 100
1158
+ },
1159
+ preferLsp: {
1160
+ type: "boolean",
1161
+ description: "Prefer live LSP results over the index. Ignored unless the LSP plugin is active; when it is active and this is true, results come from live workspaceSymbol queries."
1162
+ }
1163
+ },
1164
+ required: ["query"]
1165
+ },
1166
+ async execute(input, ctx, execOpts) {
1167
+ const state = getIndexState();
1168
+ if (state.lastError) {
1169
+ const circuit = state.circuit;
1170
+ const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s); the user can run /codebase-reindex to retry now.` : "Try /codebase-reindex.";
1171
+ return {
1172
+ results: [],
1173
+ total: 0,
1174
+ query: input.query,
1175
+ indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
1176
+ };
1177
+ }
1178
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
1179
+ let searched;
1180
+ try {
1181
+ searched = await searchCodebaseIndex(
1182
+ {
1183
+ projectRoot: ctx.projectRoot,
1184
+ indexDir: codebaseIndexDirOverride(ctx),
1185
+ query: input.query,
1186
+ kind: input.kind?.toLowerCase(),
1187
+ lang: input.lang?.toLowerCase(),
1188
+ file: input.file,
1189
+ lspKind: input.lspKind,
1190
+ limit
1191
+ },
1192
+ { signal: execOpts?.signal }
1193
+ );
1194
+ } catch (err) {
1195
+ if (execOpts?.signal?.aborted) throw err;
1196
+ if (err.name === "IndexRefreshInProgressError") {
1197
+ return {
1198
+ results: [],
1199
+ total: 0,
1200
+ query: input.query,
1201
+ indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files); this query has no cached answer yet \u2014 retry after the completed generation is published.`
1202
+ };
1203
+ }
1204
+ return {
1205
+ results: [],
1206
+ total: 0,
1207
+ query: input.query,
1208
+ indexStatus: `Index query failed: ${toErrorMessage5(err)}. Fall back to grep/glob for this lookup.`
1209
+ };
1210
+ }
1211
+ const { results, total, stale, indexSummary } = searched;
1212
+ let hasPersistedIndex = state.ready || total > 0;
1213
+ if (!hasPersistedIndex && indexSummary) {
1214
+ hasPersistedIndex = indexSummary.totalFiles > 0 || indexSummary.lastIndexed !== null;
1215
+ }
1216
+ if (!hasPersistedIndex && !indexSummary) {
1217
+ try {
1218
+ const stats = await codebaseIndexStats(
1219
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
1220
+ { signal: execOpts?.signal }
1221
+ );
1222
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
1223
+ } catch {
1224
+ }
1225
+ }
1226
+ return {
1227
+ results,
1228
+ total,
1229
+ query: input.query,
1230
+ ...stale ? {
1231
+ stale: true,
1232
+ indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files); results served from the previous generation \u2014 symbols from files being indexed may lag.`
1233
+ } : {},
1234
+ ...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
1235
+ };
1236
+ }
1237
+ };
1238
+
1239
+ // src/codebase-index/codebase-index-tool.ts
1240
+ var MAX_REPORTED_ERRORS = 20;
1241
+ var codebaseIndexTool = {
1242
+ name: "codebase-index",
1243
+ category: "Project",
1244
+ icon: "index",
1245
+ description: "Build or incrementally update the project-wide symbol index. This powers fast codebase search and understanding. By default it only processes files that have changed since the last indexing run.",
1246
+ usageHint: "CREATE OR REFRESH THE SEARCH INDEX:\n\n- When `codebase-stats` or `codebase-search` reports no persisted index, call this without arguments, then retry the search.\n- Normal and first-time usage: call without arguments for an incremental build.\n- Use `force: true` only for a corrupt/stale index or an explicitly requested clean rebuild.\n- Use `langs` to restrict to specific languages if you only care about certain parts of the project.\nThis tool is relatively expensive \u2014 do not call it on every turn. Use it when the index is stale or before heavy codebase-search sessions.",
1247
+ permission: "confirm",
1248
+ mutating: true,
1249
+ capabilities: ["fs.write.outside-project"],
1250
+ // Must comfortably exceed the index watchdog
1251
+ // (DEFAULT_FULL_INDEX_TIMEOUT_MS = 240s in background-indexer.ts) so the
1252
+ // structured IndexTimeoutError reaches the agent instead of a generic
1253
+ // TOOL_TIMEOUT. The watchdog is armed inside withMutex, so under
1254
+ // contention from a concurrent incremental batch (60s ceiling) the tool
1255
+ // timeout must also cover the mutex queue wait + 240s watchdog + margin.
1256
+ timeoutMs: 305e3,
1257
+ inputSchema: {
1258
+ type: "object",
1259
+ properties: {
1260
+ force: {
1261
+ type: "boolean",
1262
+ description: "Force a full reindex \u2014 clears the index first and reindexes all files."
1263
+ },
1264
+ langs: {
1265
+ type: "array",
1266
+ items: { type: "string", enum: [...INDEXABLE_LANG_IDS] },
1267
+ description: `Limit reindex to specific languages: ${INDEXABLE_LANG_IDS.join(", ")}`
1268
+ }
1269
+ }
1270
+ },
1271
+ async execute(input, ctx, execOpts) {
1272
+ if (input.langs) {
1273
+ const unknown = input.langs.filter(
1274
+ (lang) => !INDEXABLE_LANG_IDS.includes(lang)
1275
+ );
1276
+ if (unknown.length > 0) {
1277
+ throw new ToolValidationError({
1278
+ message: `codebase-index: unknown lang(s) ${unknown.map((l) => `"${l}"`).join(", ")}. Valid ids: ${INDEXABLE_LANG_IDS.join(", ")}.`,
1279
+ field: "langs"
1280
+ });
1281
+ }
1282
+ }
1283
+ if (isIndexing()) {
1284
+ return {
1285
+ filesIndexed: 0,
1286
+ symbolsIndexed: 0,
1287
+ langStats: {},
1288
+ durationMs: 0,
1289
+ errors: [],
1290
+ note: "A full index is already in progress. Retry codebase-index after it completes (check codebase-stats)."
1291
+ };
1292
+ }
1293
+ const circuit = indexCircuitBreaker.snapshot();
1294
+ if (circuit.state === "open" && circuit.cooldownRemainingMs > 0) {
1295
+ return {
1296
+ filesIndexed: 0,
1297
+ symbolsIndexed: 0,
1298
+ langStats: {},
1299
+ durationMs: 0,
1300
+ errors: [],
1301
+ note: `Codebase indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}). Auto-retry possible in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s; the user can run /codebase-reindex to retry immediately.`
1302
+ };
1303
+ }
1304
+ const result = await runStartupIndex({
1305
+ projectRoot: ctx.projectRoot,
1306
+ force: input.force ?? false,
1307
+ langs: input.langs,
1308
+ indexDir: codebaseIndexDirOverride(ctx),
1309
+ signal: execOpts?.signal
1310
+ });
1311
+ if (result.errors.length > MAX_REPORTED_ERRORS) {
1312
+ const hidden = result.errors.length - MAX_REPORTED_ERRORS;
1313
+ return {
1314
+ ...result,
1315
+ errors: [...result.errors.slice(0, MAX_REPORTED_ERRORS), `+${hidden} more`]
1316
+ };
1317
+ }
1318
+ return result;
1319
+ }
1320
+ };
1321
+
1322
+ // src/codebase-index/codebase-outgoing-calls-tool.ts
1323
+ import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils";
1324
+ var codebaseOutgoingCallsTool = {
1325
+ name: "codebase-outgoing-calls",
1326
+ category: "Project",
1327
+ icon: "index",
1328
+ description: "Find all functions/methods/symbols that a given symbol calls or depends on \u2014 its callees. Uses the codebase index ref graph for instant, exact results. Use this to understand a function's dependencies before modifying it.",
1329
+ usageHint: 'USE THIS TO UNDERSTAND A FUNCTION\'S DEPENDENCIES:\n\n- Prefer this over grep when the index is available; fall back to grep when the index is cold/unavailable or for dynamic dispatch the ref graph cannot see.\n- Call codebase-outgoing-calls({ symbol: "funcName" }) to see everything it calls.\n- Returns exact files, line numbers, callee signatures, and call types in milliseconds.\n- Use `file` to disambiguate when multiple symbols share a name.\n- Pair with codebase-incoming-calls for a complete impact picture: incoming = who calls you, outgoing = what you call.\nIf the index is not built, run codebase-index first.',
1330
+ permission: "auto",
1331
+ mutating: false,
1332
+ capabilities: ["fs.read"],
1333
+ timeoutMs: 35e3,
1334
+ inputSchema: {
1335
+ type: "object",
1336
+ properties: {
1337
+ symbol: {
1338
+ type: "string",
1339
+ description: "The function/method/type name to find callees for"
1340
+ },
1341
+ file: {
1342
+ type: "string",
1343
+ description: "Scope to a specific file when multiple symbols share the same name"
1344
+ },
1345
+ limit: {
1346
+ type: "integer",
1347
+ description: "Maximum call sites to return (default 50, max 200)",
1348
+ minimum: 1,
1349
+ maximum: 200
1350
+ },
1351
+ transitive: {
1352
+ type: "boolean",
1353
+ description: "When true, traverse the full transitive dependency chain (callees of callees, to unlimited depth). Default false: return only direct callees. Cycle-safe via SQL recursive CTE.",
1354
+ default: false
1355
+ }
1356
+ },
1357
+ required: ["symbol"]
1358
+ },
1359
+ async execute(input, ctx) {
1360
+ const state = getIndexState();
1361
+ if (state.lastError) {
1362
+ const circuit = state.circuit;
1363
+ const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s).` : "Try /codebase-reindex.";
1364
+ return {
1365
+ symbol: input.symbol,
1366
+ calls: [],
1367
+ total: 0,
1368
+ indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
1369
+ };
1370
+ }
1371
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
1372
+ const transitive = input.transitive === true;
1373
+ let serviced;
1374
+ try {
1375
+ serviced = await outgoingCallsService({
1376
+ projectRoot: ctx.projectRoot,
1377
+ indexDir: codebaseIndexDirOverride(ctx),
1378
+ symbol: input.symbol,
1379
+ file: input.file,
1380
+ limit,
1381
+ transitive
1382
+ });
1383
+ } catch (err) {
1384
+ if (err.name === "IndexRefreshInProgressError") {
1385
+ return {
1386
+ symbol: input.symbol,
1387
+ calls: [],
1388
+ total: 0,
1389
+ indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files); this symbol has no cached answer yet \u2014 retry after the completed generation is published.`
1390
+ };
1391
+ }
1392
+ return {
1393
+ symbol: input.symbol,
1394
+ calls: [],
1395
+ total: 0,
1396
+ indexStatus: `Index query failed: ${toErrorMessage6(err)}. Fall back to grep for this lookup.`
1397
+ };
1398
+ }
1399
+ const { calls, symbolFound, unresolvedCount, totalMatches, stale } = serviced;
1400
+ if (!symbolFound) {
1401
+ let hasPersistedIndex = state.ready;
1402
+ if (!hasPersistedIndex) {
1403
+ try {
1404
+ const stats = await codebaseIndexStats({
1405
+ projectRoot: ctx.projectRoot,
1406
+ indexDir: codebaseIndexDirOverride(ctx)
1407
+ });
1408
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
1409
+ } catch {
1410
+ }
1411
+ }
1412
+ if (!hasPersistedIndex) {
1413
+ return {
1414
+ symbol: input.symbol,
1415
+ calls: [],
1416
+ total: 0,
1417
+ indexStatus: "No persisted index data found. Run codebase-index to build it."
1418
+ };
1419
+ }
1420
+ return {
1421
+ symbol: input.symbol,
1422
+ calls: [],
1423
+ total: 0,
1424
+ note: `Symbol "${input.symbol}" not found in the index. Use codebase-search to verify the name.`
1425
+ };
1426
+ }
1427
+ const notes = [];
1428
+ if (totalMatches > limit) {
1429
+ notes.push(
1430
+ `Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`
1431
+ );
1432
+ }
1433
+ if (unresolvedCount > 0) {
1434
+ notes.push(
1435
+ `${unresolvedCount} unresolved reference(s) not shown \u2014 their targets could not be resolved during indexing.`
1436
+ );
1437
+ }
1438
+ if (stale) {
1439
+ notes.push(
1440
+ `Index refresh in progress (${state.currentFile}/${state.totalFiles} files); callees served from the previous generation \u2014 call sites in files being indexed may lag.`
1441
+ );
1442
+ }
1443
+ return {
1444
+ symbol: input.symbol,
1445
+ calls,
1446
+ total: calls.length,
1447
+ ...stale ? { stale: true } : {},
1448
+ ...notes.length > 0 ? { note: notes.join(" ") } : {}
1449
+ };
1450
+ }
1451
+ };
1452
+
1453
+ // src/codebase-index/codebase-repo-map-tool.ts
1454
+ import { toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
1455
+
1456
+ // src/codebase-index/repo-map.ts
1457
+ import * as fs4 from "node:fs/promises";
1458
+ import * as path6 from "node:path";
1459
+
1460
+ // src/codebase-index/skeleton-extractor.ts
1461
+ import * as fs3 from "node:fs/promises";
1462
+ import * as path5 from "node:path";
1463
+ var tsModule3 = null;
1464
+ async function loadTs3() {
1465
+ if (!tsModule3) {
1466
+ const mod = await import("@typescript/typescript6");
1467
+ tsModule3 = mod.default ?? mod;
1468
+ }
1469
+ return tsModule3;
1470
+ }
1471
+ function applyReplacements(content, replacements) {
1472
+ if (replacements.length === 0) return content;
1473
+ const sorted = [...replacements].sort((a, b) => a.start - b.start);
1474
+ let result = "";
1475
+ let lastIndex = 0;
1476
+ for (const r of sorted) {
1477
+ if (r.start < lastIndex) {
1478
+ continue;
1479
+ }
1480
+ result += content.slice(lastIndex, r.start);
1481
+ result += r.text;
1482
+ lastIndex = r.end;
1483
+ }
1484
+ result += content.slice(lastIndex);
1485
+ return result;
1486
+ }
1487
+ async function extractTsSkeleton(content, lang, opts) {
1488
+ const ts = await loadTs3();
1489
+ const scriptKind = lang === "tsx" ? ts.ScriptKind.TSX : lang === "jsx" ? ts.ScriptKind.JSX : lang === "js" ? ts.ScriptKind.JS : ts.ScriptKind.TS;
1490
+ const sourceFile = ts.createSourceFile(
1491
+ lang === "tsx" ? "temp.tsx" : "temp.ts",
1492
+ content,
1493
+ ts.ScriptTarget.Latest,
1494
+ true,
1495
+ scriptKind
1496
+ );
1497
+ const replacements = [];
1498
+ const symbolRanges = [];
1499
+ let symbolCount = 0;
1500
+ const includeLineNumbers = opts.includeLineNumbers ?? true;
1501
+ function isExported(node) {
1502
+ if (!ts.canHaveModifiers(node)) return false;
1503
+ const modifiers = ts.getModifiers(node);
1504
+ return modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
1505
+ }
1506
+ function visit(node, depth) {
1507
+ if (opts.maxDepth !== void 0 && depth > opts.maxDepth) {
1508
+ return;
1509
+ }
1510
+ if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node)) {
1511
+ symbolCount++;
1512
+ if (opts.exportsOnly && depth === 0 && !isExported(node)) {
1513
+ replacements.push({
1514
+ start: node.getStart(sourceFile),
1515
+ end: node.getEnd(),
1516
+ text: ""
1517
+ });
1518
+ return;
1519
+ }
1520
+ const nodeStart = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
1521
+ const nodeEnd = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
1522
+ let bodyStart;
1523
+ let bodyEnd;
1524
+ if (node.body && ts.isBlock(node.body)) {
1525
+ const start = node.body.getStart(sourceFile);
1526
+ const end = node.body.getEnd();
1527
+ bodyStart = sourceFile.getLineAndCharacterOfPosition(start).line + 1;
1528
+ bodyEnd = sourceFile.getLineAndCharacterOfPosition(end).line + 1;
1529
+ const hasLeadingSpace = start > 0 && /\s/.test(content[start - 1] ?? "");
1530
+ const label = includeLineNumbers ? `/* L${bodyStart}-L${bodyEnd} */` : "/* ... */";
1531
+ replacements.push({
1532
+ start,
1533
+ end,
1534
+ text: hasLeadingSpace ? `{ ${label} }` : ` { ${label} }`
1535
+ });
1536
+ }
1537
+ let name;
1538
+ if ("name" in node && node.name && ts.isIdentifier(node.name)) {
1539
+ name = node.name.text;
1540
+ }
1541
+ symbolRanges.push({
1542
+ name,
1543
+ kind: ts.isFunctionDeclaration(node) ? "function" : "method",
1544
+ startLine: nodeStart,
1545
+ endLine: nodeEnd,
1546
+ bodyStartLine: bodyStart,
1547
+ bodyEndLine: bodyEnd
1548
+ });
1549
+ if (node.body && ts.isBlock(node.body)) {
1550
+ return;
1551
+ }
1552
+ } else if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
1553
+ if (node.body) {
1554
+ const start = node.body.getStart(sourceFile);
1555
+ const end = node.body.getEnd();
1556
+ const bodyStart = sourceFile.getLineAndCharacterOfPosition(start).line + 1;
1557
+ const bodyEnd = sourceFile.getLineAndCharacterOfPosition(end).line + 1;
1558
+ const label = includeLineNumbers ? `/* L${bodyStart}-L${bodyEnd} */` : "/* ... */";
1559
+ if (ts.isBlock(node.body)) {
1560
+ const hasLeadingSpace = start > 0 && /\s/.test(content[start - 1] ?? "");
1561
+ replacements.push({
1562
+ start,
1563
+ end,
1564
+ text: hasLeadingSpace ? `{ ${label} }` : ` { ${label} }`
1565
+ });
1566
+ return;
1567
+ } else {
1568
+ replacements.push({
1569
+ start,
1570
+ end,
1571
+ text: label
1572
+ });
1573
+ return;
1574
+ }
1575
+ }
1576
+ } else if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node)) {
1577
+ symbolCount++;
1578
+ if (opts.exportsOnly && depth === 0 && !isExported(node)) {
1579
+ replacements.push({
1580
+ start: node.getStart(sourceFile),
1581
+ end: node.getEnd(),
1582
+ text: ""
1583
+ });
1584
+ return;
1585
+ }
1586
+ let name;
1587
+ if ("name" in node && node.name && ts.isIdentifier(node.name)) {
1588
+ name = node.name.text;
1589
+ }
1590
+ symbolRanges.push({
1591
+ name,
1592
+ kind: ts.isClassDeclaration(node) ? "class" : ts.isInterfaceDeclaration(node) ? "interface" : ts.isTypeAliasDeclaration(node) ? "type" : "enum",
1593
+ startLine: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
1594
+ endLine: sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1
1595
+ });
1596
+ }
1597
+ ts.forEachChild(node, (child) => visit(child, depth + 1));
1598
+ }
1599
+ ts.forEachChild(sourceFile, (node) => visit(node, 0));
1600
+ let skeleton = applyReplacements(content, replacements);
1601
+ if (opts.collapseImports) {
1602
+ skeleton = collapseTsImports(skeleton);
1603
+ }
1604
+ return { skeleton, symbolCount, symbols: symbolRanges };
1605
+ }
1606
+ function collapseTsImports(code) {
1607
+ const lines = code.split("\n");
1608
+ const resultLines = [];
1609
+ let inImportGroup = false;
1610
+ let importGroupCount = 0;
1611
+ for (let i = 0; i < lines.length; i++) {
1612
+ const line = lines[i] ?? "";
1613
+ const trimmed = line.trim();
1614
+ if (trimmed.startsWith("import ") || trimmed.startsWith("import{")) {
1615
+ inImportGroup = true;
1616
+ importGroupCount++;
1617
+ while (!trimmed.includes(";") && !trimmed.includes("from '") && !trimmed.includes('from "') && i + 1 < lines.length) {
1618
+ i++;
1619
+ }
1620
+ } else {
1621
+ if (inImportGroup) {
1622
+ resultLines.push(`// ... [${importGroupCount} import statements collapsed]`);
1623
+ inImportGroup = false;
1624
+ importGroupCount = 0;
1625
+ }
1626
+ resultLines.push(line);
1627
+ }
1628
+ }
1629
+ if (inImportGroup) {
1630
+ resultLines.push(`// ... [${importGroupCount} import statements collapsed]`);
1631
+ }
1632
+ return resultLines.join("\n");
1633
+ }
1634
+ async function extractTreeSitterSkeleton(content, lang, opts) {
1635
+ const parsed = await parseTreeSitterAst({ content, lang });
1636
+ if (!parsed) return null;
1637
+ const { tree, parser } = parsed;
1638
+ const replacements = [];
1639
+ const symbolRanges = [];
1640
+ let symbolCount = 0;
1641
+ const includeLineNumbers = opts.includeLineNumbers ?? true;
1642
+ try {
1643
+ let walk2 = function(node) {
1644
+ const type = node.type;
1645
+ if (type === "function_definition" || type === "function_declaration" || type === "method_declaration" || type === "function_item" || type === "method" || type === "singleton_method" || type === "func_literal") {
1646
+ symbolCount++;
1647
+ const nodeStart = node.startPosition.row + 1;
1648
+ const nodeEnd = node.endPosition.row + 1;
1649
+ let bodyStart;
1650
+ let bodyEnd;
1651
+ let bodyNode = node.childForFieldName("body") ?? node.children.find(
1652
+ (c) => [
1653
+ "compound_statement",
1654
+ "statement_block",
1655
+ "block",
1656
+ "function_body",
1657
+ "body_statement",
1658
+ "code_block",
1659
+ "do_block"
1660
+ ].includes(c.type)
1661
+ );
1662
+ if (bodyNode) {
1663
+ bodyStart = bodyNode.startPosition.row + 1;
1664
+ bodyEnd = bodyNode.endPosition.row + 1;
1665
+ const label = includeLineNumbers ? `/* L${bodyStart}-L${bodyEnd} */` : "/* ... */";
1666
+ if (lang === "py") {
1667
+ replacements.push({
1668
+ start: bodyNode.startIndex,
1669
+ end: bodyNode.endIndex,
1670
+ text: includeLineNumbers ? `... # L${bodyStart}-L${bodyEnd}` : "..."
1671
+ });
1672
+ } else if (lang === "ruby") {
1673
+ replacements.push({
1674
+ start: bodyNode.startIndex,
1675
+ end: bodyNode.endIndex,
1676
+ text: includeLineNumbers ? `
1677
+ # L${bodyStart}-L${bodyEnd}` : "\n # ..."
1678
+ });
1679
+ } else {
1680
+ const hasLeadingSpace = bodyNode.startIndex > 0 && /\s/.test(content[bodyNode.startIndex - 1] ?? "");
1681
+ replacements.push({
1682
+ start: bodyNode.startIndex,
1683
+ end: bodyNode.endIndex,
1684
+ text: hasLeadingSpace ? `{ ${label} }` : ` { ${label} }`
1685
+ });
1686
+ }
1687
+ }
1688
+ let nameNode = node.childForFieldName("name") ?? node.childForFieldName("declarator");
1689
+ let symbolName = nameNode ? nameNode.text : void 0;
1690
+ symbolRanges.push({
1691
+ name: symbolName,
1692
+ kind: type.includes("function") ? "function" : "method",
1693
+ startLine: nodeStart,
1694
+ endLine: nodeEnd,
1695
+ bodyStartLine: bodyStart,
1696
+ bodyEndLine: bodyEnd
1697
+ });
1698
+ if (bodyNode) return;
1699
+ } else if (type === "class_definition" || type === "class_specifier" || type === "class_declaration" || type === "struct_specifier" || type === "interface_declaration" || type === "trait_item") {
1700
+ symbolCount++;
1701
+ let nameNode = node.childForFieldName("name");
1702
+ symbolRanges.push({
1703
+ name: nameNode ? nameNode.text : void 0,
1704
+ kind: type.includes("class") ? "class" : type.includes("struct") ? "struct" : type.includes("interface") ? "interface" : "trait",
1705
+ startLine: node.startPosition.row + 1,
1706
+ endLine: node.endPosition.row + 1
1707
+ });
1708
+ }
1709
+ for (let i = 0; i < node.namedChildCount; i++) {
1710
+ const child = node.namedChild(i);
1711
+ if (child) walk2(child);
1712
+ }
1713
+ };
1714
+ var walk = walk2;
1715
+ const root = tree.rootNode;
1716
+ walk2(root);
1717
+ const skeleton = applyReplacements(content, replacements);
1718
+ return { skeleton, symbolCount, symbols: symbolRanges };
1719
+ } finally {
1720
+ tree.delete();
1721
+ parser.delete();
1722
+ }
1723
+ }
1724
+ function extractFallbackSkeleton(content, _lang) {
1725
+ const lines = content.split("\n");
1726
+ const skeletonLines = [];
1727
+ let symbolCount = 0;
1728
+ let inBlock = false;
1729
+ for (const line of lines) {
1730
+ const trimmed = line.trim();
1731
+ if (trimmed.startsWith("export ") || trimmed.startsWith("function ") || trimmed.startsWith("def ") || trimmed.startsWith("class ") || trimmed.startsWith("interface ") || trimmed.startsWith("type ") || trimmed.startsWith("struct ") || trimmed.startsWith("pub ") || trimmed.startsWith("import ") || trimmed.startsWith("package ") || trimmed.startsWith("#include")) {
1732
+ symbolCount++;
1733
+ if (trimmed.endsWith("{")) {
1734
+ skeletonLines.push(`${line.replace(/\{$/, "")}{ /* ... */ }`);
1735
+ inBlock = true;
1736
+ } else {
1737
+ skeletonLines.push(line);
1738
+ }
1739
+ } else if (trimmed === "}" && inBlock) {
1740
+ inBlock = false;
1741
+ } else if (!inBlock && (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*"))) {
1742
+ skeletonLines.push(line);
1743
+ }
1744
+ }
1745
+ return {
1746
+ skeleton: skeletonLines.length > 0 ? skeletonLines.join("\n") : content,
1747
+ symbolCount
1748
+ };
1749
+ }
1750
+ async function extractFileSkeleton(opts) {
1751
+ const detected = detectLang(opts.file);
1752
+ const lang = opts.lang ?? detected ?? "other";
1753
+ const options = opts.options ?? {};
1754
+ const originalLines = opts.content.split("\n").length;
1755
+ let extraction;
1756
+ if (["ts", "tsx", "js", "jsx"].includes(lang)) {
1757
+ extraction = await extractTsSkeleton(opts.content, lang, options);
1758
+ } else {
1759
+ const tsResult = await extractTreeSitterSkeleton(opts.content, lang, options);
1760
+ if (tsResult) {
1761
+ extraction = tsResult;
1762
+ } else {
1763
+ extraction = extractFallbackSkeleton(opts.content, lang);
1764
+ }
1765
+ }
1766
+ const skeletonLines = extraction.skeleton.split("\n").length;
1767
+ const originalChars = opts.content.length;
1768
+ const skeletonChars = extraction.skeleton.length;
1769
+ const savings = originalChars > 0 ? (originalChars - skeletonChars) / originalChars * 100 : 0;
1770
+ return {
1771
+ file: opts.file,
1772
+ lang,
1773
+ skeleton: extraction.skeleton,
1774
+ symbols: extraction.symbols,
1775
+ stats: {
1776
+ originalLines,
1777
+ skeletonLines,
1778
+ tokenSavingsPercent: Math.max(0, Math.round(savings * 10) / 10),
1779
+ symbolCount: extraction.symbolCount
1780
+ }
1781
+ };
1782
+ }
1783
+ async function extractDirectorySkeleton(opts) {
1784
+ const maxFiles = opts.maxFiles ?? 30;
1785
+ const filePaths = [];
1786
+ async function scan(dir) {
1787
+ if (filePaths.length >= maxFiles) return;
1788
+ let entries;
1789
+ try {
1790
+ entries = await fs3.readdir(dir, { withFileTypes: true });
1791
+ } catch {
1792
+ return;
1793
+ }
1794
+ for (const entry of entries) {
1795
+ if (filePaths.length >= maxFiles) break;
1796
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build") {
1797
+ continue;
1798
+ }
1799
+ const fullPath = path5.join(dir, entry.name);
1800
+ if (entry.isDirectory()) {
1801
+ await scan(fullPath);
1802
+ } else if (entry.isFile() && isIndexablePath(fullPath)) {
1803
+ filePaths.push(fullPath);
1804
+ }
1805
+ }
1806
+ }
1807
+ await scan(opts.dirPath);
1808
+ const results = [];
1809
+ let totalOrigChars = 0;
1810
+ let totalSkelChars = 0;
1811
+ let totalOrigLines = 0;
1812
+ let totalSkelLines = 0;
1813
+ const combinedParts = [];
1814
+ for (const fp of filePaths) {
1815
+ try {
1816
+ const content = await fs3.readFile(fp, "utf8");
1817
+ const res = await extractFileSkeleton({
1818
+ file: fp,
1819
+ content,
1820
+ options: opts.options ?? {}
1821
+ });
1822
+ results.push(res);
1823
+ totalOrigChars += content.length;
1824
+ totalSkelChars += res.skeleton.length;
1825
+ totalOrigLines += res.stats.originalLines;
1826
+ totalSkelLines += res.stats.skeletonLines;
1827
+ const rel = path5.relative(opts.dirPath, fp).replace(/\\/g, "/");
1828
+ combinedParts.push(
1829
+ `// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 ${rel} (${res.lang}) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
1830
+ ${res.skeleton}`
1831
+ );
1832
+ } catch {
1833
+ }
1834
+ }
1835
+ const overallSavings = totalOrigChars > 0 ? (totalOrigChars - totalSkelChars) / totalOrigChars * 100 : 0;
1836
+ return {
1837
+ files: results,
1838
+ combinedSkeleton: combinedParts.join("\n\n"),
1839
+ totalOriginalLines: totalOrigLines,
1840
+ totalSkeletonLines: totalSkelLines,
1841
+ overallSavingsPercent: Math.max(0, Math.round(overallSavings * 10) / 10)
1842
+ };
1843
+ }
1844
+
1845
+ // src/codebase-index/repo-map.ts
1846
+ var ENTRY_POINT_REGEX = /(?:^|[/\\])(?:index|main|app|server|cli|lib|api|types|routes|mod)\.[a-zA-Z0-9]+$/i;
1847
+ var TEST_OR_MOCK_REGEX = /(?:test|spec|mock|fixture|bench|example)[s]?[/\\._]/i;
1848
+ function getEntryPointBonus(relPath) {
1849
+ if (TEST_OR_MOCK_REGEX.test(relPath)) {
1850
+ return -20;
1851
+ }
1852
+ if (ENTRY_POINT_REGEX.test(relPath)) {
1853
+ return 25;
1854
+ }
1855
+ const depth = relPath.split(/[/\\]/).length;
1856
+ if (depth <= 2) return 10;
1857
+ if (depth <= 3) return 5;
1858
+ return 0;
1859
+ }
1860
+ async function generateRepoMap(opts) {
1861
+ const projectRoot = opts.projectRoot;
1862
+ const maxTokens = opts.maxTokens ?? 1200;
1863
+ const charBudget = maxTokens * 4;
1864
+ const focusSet = new Set((opts.focusFiles ?? []).map((f) => path6.resolve(projectRoot, f)));
1865
+ const allFiles = [];
1866
+ async function scan(dir) {
1867
+ if (allFiles.length > 500) return;
1868
+ let entries;
1869
+ try {
1870
+ entries = await fs4.readdir(dir, { withFileTypes: true });
1871
+ } catch {
1872
+ return;
1873
+ }
1874
+ for (const entry of entries) {
1875
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build" || entry.name === "coverage") {
1876
+ continue;
1877
+ }
1878
+ const fullPath = path6.join(dir, entry.name);
1879
+ if (entry.isDirectory()) {
1880
+ await scan(fullPath);
1881
+ } else if (entry.isFile() && isIndexablePath(fullPath)) {
1882
+ allFiles.push(fullPath);
1883
+ }
1884
+ }
1885
+ }
1886
+ await scan(projectRoot);
1887
+ if (allFiles.length === 0) {
1888
+ return {
1889
+ map: "// [Repo map: No indexable source files found]",
1890
+ filesCount: 0,
1891
+ totalFilesScanned: 0,
1892
+ estimatedTokens: 0,
1893
+ rankedFiles: []
1894
+ };
1895
+ }
1896
+ const scoredFiles = [];
1897
+ for (const file of allFiles) {
1898
+ const relPath = path6.relative(projectRoot, file).replace(/\\/g, "/");
1899
+ let score = 10;
1900
+ if (focusSet.has(file)) {
1901
+ score += 100;
1902
+ }
1903
+ score += getEntryPointBonus(relPath);
1904
+ scoredFiles.push({ file, score, relPath });
1905
+ }
1906
+ scoredFiles.sort((a, b) => b.score - a.score);
1907
+ const includedSections = [];
1908
+ const includedFiles = [];
1909
+ let currentChars = 0;
1910
+ for (const candidate of scoredFiles) {
1911
+ if (currentChars >= charBudget) break;
1912
+ try {
1913
+ const content = await fs4.readFile(candidate.file, "utf8");
1914
+ if (content.length === 0) continue;
1915
+ const skeletonResult = await extractFileSkeleton({
1916
+ file: candidate.file,
1917
+ content,
1918
+ options: {
1919
+ exportsOnly: true,
1920
+ collapseImports: true,
1921
+ includeDocs: false,
1922
+ includeLineNumbers: true,
1923
+ ...opts.options ?? {}
1924
+ }
1925
+ });
1926
+ const cleanSkeleton = skeletonResult.skeleton.trim();
1927
+ if (!cleanSkeleton) continue;
1928
+ const section = `${candidate.relPath} (${skeletonResult.lang}):
1929
+ ${cleanSkeleton.split("\n").map((line) => line.trim() ? ` ${line}` : "").join("\n")}`;
1930
+ const sectionChars = section.length + 2;
1931
+ if (currentChars + sectionChars > charBudget && includedFiles.length > 0) {
1932
+ break;
1933
+ }
1934
+ includedSections.push(section);
1935
+ includedFiles.push(candidate.relPath);
1936
+ currentChars += sectionChars;
1937
+ } catch {
1938
+ }
1939
+ }
1940
+ const map = includedSections.join("\n\n");
1941
+ const estimatedTokens = Math.ceil(map.length / 4);
1942
+ return {
1943
+ map,
1944
+ filesCount: includedFiles.length,
1945
+ totalFilesScanned: allFiles.length,
1946
+ estimatedTokens,
1947
+ rankedFiles: includedFiles
1948
+ };
1949
+ }
1950
+
1951
+ // src/codebase-index/codebase-repo-map-tool.ts
1952
+ var codebaseRepoMapTool = {
1953
+ name: "codebase-repo-map",
1954
+ category: "Project",
1955
+ icon: "index",
1956
+ permission: "auto",
1957
+ mutating: false,
1958
+ capabilities: ["fs.read"],
1959
+ description: "Generate a reference-weighted, token-budgeted Repository Map (Repo Map) of the codebase. Extracts the most architecturally central types, interfaces, classes, and function signatures across the repository into a compact outline (~800-1500 tokens). Use this at the beginning of tasks or when navigating unfamiliar repositories to get a bird-eye view of the architecture.",
1960
+ usageHint: 'USE AT THE START OF COMPLEX OR REPOSITORY-WIDE TASKS:\n\n- Call with default parameters to get a global architecture map within ~1200 tokens.\n- Pass `focusFiles: ["src/auth.ts"]` to ensure candidate files are guaranteed to be mapped alongside related entry-points.\n- Use the returned line numbers (e.g. `/* L32-L45 */`) to navigate or partially read only the functions you need.',
1961
+ inputSchema: {
1962
+ type: "object",
1963
+ properties: {
1964
+ maxTokens: {
1965
+ type: "number",
1966
+ description: "Maximum token budget (approximate) for the map. Defaults to 1200."
1967
+ },
1968
+ focusFiles: {
1969
+ type: "array",
1970
+ items: { type: "string" },
1971
+ description: "File paths to prioritize and ensure inclusion in the map."
1972
+ }
1973
+ },
1974
+ additionalProperties: false
1975
+ },
1976
+ async execute(input, ctx) {
1977
+ try {
1978
+ const projectRoot = ctx.projectRoot;
1979
+ const result = await generateRepoMap({
1980
+ projectRoot,
1981
+ maxTokens: input.maxTokens,
1982
+ focusFiles: input.focusFiles
1983
+ });
1984
+ return {
1985
+ status: "ok",
1986
+ ...result
1987
+ };
1988
+ } catch (err) {
1989
+ return {
1990
+ status: "error",
1991
+ map: "",
1992
+ filesCount: 0,
1993
+ totalFilesScanned: 0,
1994
+ estimatedTokens: 0,
1995
+ rankedFiles: [],
1996
+ error: toErrorMessage7(err)
1997
+ };
1998
+ }
1999
+ }
2000
+ };
2001
+
2002
+ // src/codebase-index/codebase-skeleton-tool.ts
2003
+ import * as fs5 from "node:fs/promises";
2004
+ import * as path7 from "node:path";
2005
+ import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
2006
+ var codebaseSkeletonTool = {
2007
+ name: "codebase-skeleton",
2008
+ category: "Project",
2009
+ icon: "index",
2010
+ description: "Extract an AST-based skeleton of a file or directory with implementation bodies stripped. Preserves imports, type definitions, interfaces, docstrings, and function/method signatures while saving ~70-90% tokens. Prefer this before reading full multi-hundred-line files when exploring module structure, contracts, or candidate APIs.",
2011
+ usageHint: "FIRST CHOICE FOR MODULE CONTRACT & OUTLINE UNDERSTANDING:\n\n- Call on large source files to inspect signatures, types, and exports before issuing a full `read`.\n- Call on directories to generate a compact repository or module map.\n- Use `includeDocs: true` (default) to keep API documentation.\n- Use `collapseImports: true` to further compress import-heavy modules.",
2012
+ permission: "auto",
2013
+ mutating: false,
2014
+ capabilities: ["fs.read"],
2015
+ timeoutMs: 3e4,
2016
+ inputSchema: {
2017
+ type: "object",
2018
+ properties: {
2019
+ path: {
2020
+ type: "string",
2021
+ description: "Relative or absolute file or directory path to extract skeleton for."
2022
+ },
2023
+ includeDocs: {
2024
+ type: "boolean",
2025
+ description: "Keep JSDoc / docstrings in the output skeleton (defaults to true)."
2026
+ },
2027
+ collapseImports: {
2028
+ type: "boolean",
2029
+ description: "Collapse import blocks into a single summary comment (defaults to false)."
2030
+ },
2031
+ exportsOnly: {
2032
+ type: "boolean",
2033
+ description: "Only include exported/public declarations (defaults to false)."
2034
+ },
2035
+ maxFiles: {
2036
+ type: "number",
2037
+ description: "Max files to include if path is a directory (defaults to 20, max 50)."
2038
+ }
2039
+ },
2040
+ required: ["path"],
2041
+ additionalProperties: false
2042
+ },
2043
+ async execute(input, ctx) {
2044
+ const targetPath = path7.isAbsolute(input.path) ? input.path : path7.resolve(ctx.projectRoot ?? process.cwd(), input.path);
2045
+ let stat3;
2046
+ try {
2047
+ stat3 = await fs5.stat(targetPath);
2048
+ } catch (err) {
2049
+ throw new Error(
2050
+ `codebase-skeleton: file or directory not found at "${input.path}": ${toErrorMessage8(err)}`
2051
+ );
2052
+ }
2053
+ const options = {
2054
+ includeDocs: input.includeDocs ?? true,
2055
+ collapseImports: input.collapseImports ?? false,
2056
+ exportsOnly: input.exportsOnly ?? false
2057
+ };
2058
+ if (stat3.isDirectory()) {
2059
+ const maxFiles = Math.min(Math.max(1, input.maxFiles ?? 20), 50);
2060
+ const dirResult = await extractDirectorySkeleton({
2061
+ dirPath: targetPath,
2062
+ maxFiles,
2063
+ options
2064
+ });
2065
+ let totalSymbols = 0;
2066
+ for (const f of dirResult.files) {
2067
+ totalSymbols += f.stats.symbolCount;
2068
+ }
2069
+ return {
2070
+ path: input.path,
2071
+ isDir: true,
2072
+ skeleton: dirResult.combinedSkeleton,
2073
+ stats: {
2074
+ originalLines: dirResult.totalOriginalLines,
2075
+ skeletonLines: dirResult.totalSkeletonLines,
2076
+ tokenSavingsPercent: dirResult.overallSavingsPercent,
2077
+ symbolCount: totalSymbols,
2078
+ fileCount: dirResult.files.length
2079
+ },
2080
+ files: dirResult.files
2081
+ };
2082
+ }
2083
+ const content = await fs5.readFile(targetPath, "utf8");
2084
+ const fileResult = await extractFileSkeleton({
2085
+ file: targetPath,
2086
+ content,
2087
+ options
2088
+ });
2089
+ return {
2090
+ path: input.path,
2091
+ isDir: false,
2092
+ skeleton: fileResult.skeleton,
2093
+ stats: fileResult.stats
2094
+ };
2095
+ }
2096
+ };
2097
+
2098
+ // src/codebase-index/codebase-stats-tool.ts
2099
+ var codebaseStatsTool = {
2100
+ name: "codebase-stats",
2101
+ category: "Project",
2102
+ icon: "index",
2103
+ description: "Check whether a persisted codebase index exists and report its health and statistics (symbols, files, language/kind breakdown, size, last update).",
2104
+ usageHint: "CHECK ONCE BEFORE BROAD CODE EXPLORATION:\n\n- Call before broad `tree`, `glob`, or `grep` exploration to see whether index-backed search is available.\n- If it reports no persisted data, run `codebase-index`, then use `codebase-search`.\n- No arguments required.\nLightweight and safe to call frequently.",
2105
+ permission: "auto",
2106
+ mutating: false,
2107
+ capabilities: ["fs.read"],
2108
+ // The index host has its own 30s read watchdog. Keep the outer tool budget
2109
+ // longer so the host can return a structured timeout instead of being
2110
+ // pre-empted by the generic tool executor.
2111
+ timeoutMs: 35e3,
2112
+ inputSchema: {
2113
+ type: "object",
2114
+ properties: {},
2115
+ additionalProperties: false
2116
+ },
2117
+ async execute(_input, ctx, execOpts) {
2118
+ const idxState = getIndexState();
2119
+ const indexPath = resolveIndexDir(ctx.projectRoot, codebaseIndexDirOverride(ctx));
2120
+ if (idxState.indexing) {
2121
+ return {
2122
+ totalSymbols: 0,
2123
+ totalFiles: 0,
2124
+ byLang: {},
2125
+ byKind: {},
2126
+ lastIndexed: null,
2127
+ sizeBytes: 0,
2128
+ indexPath,
2129
+ version: SCHEMA_VERSION,
2130
+ statsAvailable: false,
2131
+ indexing: {
2132
+ currentFile: idxState.currentFile,
2133
+ totalFiles: idxState.totalFiles
2134
+ },
2135
+ indexStatus: idxState.ready ? `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). The persisted snapshot remains available to codebase-search; retry stats after refresh.` : `Startup indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files). Do not start another index; retry stats or codebase-search after completion.`
2136
+ };
2137
+ }
2138
+ let stats;
2139
+ try {
2140
+ stats = await codebaseIndexStats(
2141
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
2142
+ { signal: execOpts?.signal }
2143
+ );
2144
+ } catch (err) {
2145
+ if (!(err instanceof IndexTimeoutError) && err?.name !== "IndexTimeoutError") {
2146
+ throw err;
2147
+ }
2148
+ return {
2149
+ totalSymbols: 0,
2150
+ totalFiles: 0,
2151
+ byLang: {},
2152
+ byKind: {},
2153
+ lastIndexed: null,
2154
+ sizeBytes: 0,
2155
+ indexPath,
2156
+ version: SCHEMA_VERSION,
2157
+ statsAvailable: false,
2158
+ indexStatus: "Index statistics timed out. Do not repeatedly retry stats; try codebase-search directly or use the appropriate grep/glob/tree fallback."
2159
+ };
2160
+ }
2161
+ const circuit = idxState.circuit;
2162
+ const hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
2163
+ let indexStatus;
2164
+ if (circuit.state === "open") {
2165
+ indexStatus = `${hasPersistedIndex ? "Indexing" : "No persisted index data found, and indexing"} is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}); auto-retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s, or run /codebase-reindex. ` + (hasPersistedIndex ? "Stats reflect the last successful build." : "Build the index after recovery.");
2166
+ } else if (!hasPersistedIndex) {
2167
+ indexStatus = "No persisted index data found. Run codebase-index to build it before broad code exploration.";
2168
+ }
2169
+ return {
2170
+ totalSymbols: stats.totalSymbols,
2171
+ totalFiles: stats.totalFiles,
2172
+ byLang: stats.byLang,
2173
+ byKind: stats.byKind,
2174
+ lastIndexed: stats.lastIndexed,
2175
+ sizeBytes: stats.sizeBytes,
2176
+ indexPath: stats.indexPath,
2177
+ version: stats.version,
2178
+ statsAvailable: true,
2179
+ ...indexStatus ? { indexStatus } : {}
2180
+ };
2181
+ }
2182
+ };
2183
+
2184
+ // src/codebase-index/codebase-targeted-test-tool.ts
2185
+ import * as fs6 from "node:fs/promises";
2186
+ import * as path8 from "node:path";
2187
+ import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
2188
+ var TEST_FILE_REGEX2 = /(?:test|spec|mock|fixture|bench)[s]?[/\\._]/i;
2189
+ async function findConventionTestFiles(projectRoot, sourceRelPath) {
2190
+ const parsed = path8.parse(sourceRelPath);
2191
+ const candidates = [
2192
+ path8.join(parsed.dir, `${parsed.name}.test${parsed.ext}`),
2193
+ path8.join(parsed.dir, `${parsed.name}.spec${parsed.ext}`),
2194
+ path8.join(parsed.dir, `${parsed.name}_test${parsed.ext}`),
2195
+ path8.join(parsed.dir, `test_${parsed.name}${parsed.ext}`),
2196
+ path8.join("tests", `${parsed.name}.test${parsed.ext}`),
2197
+ path8.join("tests", `${parsed.name}.spec${parsed.ext}`),
2198
+ path8.join("test", `${parsed.name}.test${parsed.ext}`),
2199
+ path8.join(parsed.dir, "__tests__", `${parsed.name}.test${parsed.ext}`),
2200
+ path8.join(parsed.dir, "__tests__", `${parsed.name}${parsed.ext}`)
2201
+ ];
2202
+ const found = [];
2203
+ for (const cand of candidates) {
2204
+ const absPath = path8.resolve(projectRoot, cand);
2205
+ try {
2206
+ const st = await fs6.stat(absPath);
2207
+ if (st.isFile()) {
2208
+ found.push(cand.replace(/\\/g, "/"));
2209
+ }
2210
+ } catch {
2211
+ }
2212
+ }
2213
+ return found;
2214
+ }
2215
+ var codebaseTargetedTestTool = {
2216
+ name: "codebase-targeted-test",
2217
+ category: "Code Quality",
2218
+ icon: "test",
2219
+ permission: "confirm",
2220
+ mutating: false,
2221
+ capabilities: ["shell.restricted", "fs.read"],
2222
+ timeoutMs: 6e4,
2223
+ description: "Smart targeted test runner. Automatically identifies and executes only the test suites that cover a specific symbol or file using Call Graph references and convention mapping. Executes in ~100-500ms rather than waiting minutes for the entire test suite.",
2224
+ usageHint: 'CALL IMMEDIATELY AFTER MUTATING A FUNCTION OR FILE:\n\n- Pass `symbol: "calculateDiscount"` or `file: "src/order-service.ts"`.\n- Discovers covering tests via the call graph and conventions, then executes only those files.\n- Returns instant test feedback and failure logs for self-healing loops.',
2225
+ inputSchema: {
2226
+ type: "object",
2227
+ properties: {
2228
+ symbol: {
2229
+ type: "string",
2230
+ description: "Symbol name whose covering tests should be discovered and run."
2231
+ },
2232
+ file: {
2233
+ type: "string",
2234
+ description: "Source file path whose covering tests should be run."
2235
+ },
2236
+ testFiles: {
2237
+ type: "array",
2238
+ items: { type: "string" },
2239
+ description: "Explicit list of test files to execute."
2240
+ }
2241
+ },
2242
+ additionalProperties: false
2243
+ },
2244
+ async execute(input, ctx) {
2245
+ const projectRoot = ctx.projectRoot;
2246
+ const suitesSet = /* @__PURE__ */ new Set();
2247
+ try {
2248
+ if (input.testFiles) {
2249
+ for (const tf of input.testFiles) suitesSet.add(tf.replace(/\\/g, "/"));
2250
+ }
2251
+ if (input.symbol) {
2252
+ try {
2253
+ const indexDir = codebaseIndexDirOverride(ctx);
2254
+ const serviced = await incomingCallsService({
2255
+ projectRoot,
2256
+ indexDir,
2257
+ symbol: input.symbol,
2258
+ file: input.file,
2259
+ limit: 100,
2260
+ transitive: true
2261
+ });
2262
+ for (const site of serviced.calls) {
2263
+ const relPath = path8.relative(projectRoot, site.symbol.file).replace(/\\/g, "/");
2264
+ if (TEST_FILE_REGEX2.test(relPath)) {
2265
+ suitesSet.add(relPath);
2266
+ }
2267
+ }
2268
+ } catch {
2269
+ }
2270
+ }
2271
+ if (input.file) {
2272
+ const convFiles = await findConventionTestFiles(projectRoot, input.file);
2273
+ for (const cf of convFiles) suitesSet.add(cf);
2274
+ }
2275
+ const discoveredSuites = [...suitesSet];
2276
+ if (discoveredSuites.length === 0) {
2277
+ return {
2278
+ status: "no_tests_found",
2279
+ discoveredSuites: [],
2280
+ testsRun: 0,
2281
+ passed: 0,
2282
+ failed: 0,
2283
+ durationMs: 0,
2284
+ output: "No targeted test suites found for the given symbol or file."
2285
+ };
2286
+ }
2287
+ const start = Date.now();
2288
+ let runnerCmd = "npx";
2289
+ let runnerArgs = ["vitest", "run", ...discoveredSuites];
2290
+ if (discoveredSuites.every((s) => s.endsWith(".py"))) {
2291
+ runnerCmd = "pytest";
2292
+ runnerArgs = discoveredSuites;
2293
+ } else if (discoveredSuites.every((s) => s.endsWith("_test.go"))) {
2294
+ runnerCmd = "go";
2295
+ runnerArgs = ["test", ...discoveredSuites];
2296
+ }
2297
+ const signal = ctx.signal ?? new AbortController().signal;
2298
+ const gen = spawnStream({
2299
+ cmd: runnerCmd,
2300
+ args: runnerArgs,
2301
+ cwd: projectRoot,
2302
+ signal,
2303
+ maxBytes: 1e5
2304
+ });
2305
+ let genResult = await gen.next();
2306
+ while (!genResult.done) {
2307
+ genResult = await gen.next();
2308
+ }
2309
+ const streamRes = genResult.value;
2310
+ const durationMs = Date.now() - start;
2311
+ const combinedOutput = normalizeCommandOutput(
2312
+ `${streamRes.stdout}
2313
+ ${streamRes.stderr}`.trim(),
2314
+ { maxBytes: 4e3 }
2315
+ );
2316
+ const isPassed = streamRes.exitCode === 0;
2317
+ const passMatch = combinedOutput.match(/(\d+)\s+passed/i);
2318
+ const failMatch = combinedOutput.match(/(\d+)\s+failed/i);
2319
+ const passed = passMatch ? parseInt(passMatch[1] ?? "0", 10) : isPassed ? discoveredSuites.length : 0;
2320
+ const failed = failMatch ? parseInt(failMatch[1] ?? "0", 10) : isPassed ? 0 : 1;
2321
+ const testsRun = passed + failed;
2322
+ return {
2323
+ status: isPassed ? "passed" : "failed",
2324
+ discoveredSuites,
2325
+ testsRun,
2326
+ passed,
2327
+ failed,
2328
+ durationMs,
2329
+ output: combinedOutput
2330
+ };
2331
+ } catch (err) {
2332
+ return {
2333
+ status: "error",
2334
+ discoveredSuites: [...suitesSet],
2335
+ testsRun: 0,
2336
+ passed: 0,
2337
+ failed: 0,
2338
+ durationMs: 0,
2339
+ output: "",
2340
+ error: toErrorMessage9(err)
2341
+ };
2342
+ }
2343
+ }
2344
+ };
2345
+
2346
+ // src/codebase-index/dead-code-scan.ts
2347
+ import * as fs7 from "node:fs";
2348
+ import * as path9 from "node:path";
2349
+ var deadCodeScanTool = {
2350
+ name: "dead-code-scan",
2351
+ category: "Project",
2352
+ icon: "index",
2353
+ description: "Scan TypeScript/JavaScript source files for exported symbols that appear unused anywhere in the project. Uses the codebase-index reference graph (import/call/type-ref edges) to compute transitive reachability from package.json entry points. Requires a built codebase-index (run `codebase-index` first if you get no results).",
2354
+ usageHint: "SCANS ALL INDEXED FILES UNDER THE PROJECT ROOT:\n\n- `projectRoot` defaults to the current project root; `indexDir` overrides the resolved index location.\n- `entryPoints` is an array of file paths that AUGMENTS the auto-discovered entry points (package.json bin/main/exports/types plus conventional src/index.ts-style files) \u2014 it does not replace them.\n\nThe scan runs against the existing index; results are best-effort (dynamic imports, external consumers, and config-driven registration are invisible).",
2355
+ permission: "auto",
2356
+ mutating: false,
2357
+ capabilities: ["fs.read"],
2358
+ timeoutMs: 6e4,
2359
+ inputSchema: {
2360
+ type: "object",
2361
+ properties: {
2362
+ projectRoot: {
2363
+ type: "string",
2364
+ description: "Project root (defaults to ctx.projectRoot)."
2365
+ },
2366
+ indexDir: {
2367
+ type: "string",
2368
+ description: "Override index directory."
2369
+ },
2370
+ entryPoints: {
2371
+ type: "array",
2372
+ items: { type: "string" },
2373
+ description: "Additional entry-point file paths to seed the scan."
2374
+ }
2375
+ }
2376
+ },
2377
+ async execute(input, ctx, _execOpts) {
2378
+ const startMs = Date.now();
2379
+ const projectRoot = input.projectRoot ?? ctx.projectRoot;
2380
+ const indexDir = input.indexDir ?? codebaseIndexDirOverride(ctx) ?? void 0;
2381
+ const result = runDeadCodeScan(projectRoot, {
2382
+ indexDir,
2383
+ userEntryPoints: input.entryPoints
2384
+ });
2385
+ return {
2386
+ ...result,
2387
+ stats: { ...result.stats, durationMs: Date.now() - startMs }
2388
+ };
2389
+ }
2390
+ };
2391
+ function tryReadJson(filePath) {
2392
+ try {
2393
+ const raw = fs7.readFileSync(filePath, "utf8");
2394
+ return JSON.parse(raw);
2395
+ } catch {
2396
+ return null;
2397
+ }
2398
+ }
2399
+ function resolveAgainst(base, relative7) {
2400
+ if (path9.isAbsolute(relative7)) return relative7;
2401
+ return path9.resolve(base, relative7);
2402
+ }
2403
+ function discoverEntryPoints(projectRoot, userEntryPoints) {
2404
+ const entries = /* @__PURE__ */ new Set();
2405
+ if (userEntryPoints) {
2406
+ for (const ep of userEntryPoints) {
2407
+ const resolved = resolveAgainst(projectRoot, ep);
2408
+ if (fs7.existsSync(resolved)) entries.add(resolved);
2409
+ }
2410
+ }
2411
+ const rootPkg = tryReadJson(path9.join(projectRoot, "package.json"));
2412
+ if (rootPkg) {
2413
+ addPkgJsonEntryPoints(projectRoot, rootPkg, entries);
2414
+ }
2415
+ let workspaces;
2416
+ if (rootPkg) {
2417
+ workspaces = extractWorkspaceGlobs(rootPkg, projectRoot);
2418
+ if (workspaces.length === 0) {
2419
+ workspaces = extractPnpmWorkspaceDirs(projectRoot);
2420
+ }
2421
+ } else {
2422
+ workspaces = [];
2423
+ }
2424
+ for (const wsDir of workspaces) {
2425
+ const pkgJsonPath = path9.join(wsDir, "package.json");
2426
+ const pkg = tryReadJson(pkgJsonPath);
2427
+ if (pkg) {
2428
+ addPkgJsonEntryPoints(wsDir, pkg, entries);
2429
+ const convention1 = path9.join(wsDir, "src", "index.ts");
2430
+ if (fs7.existsSync(convention1)) entries.add(convention1);
2431
+ const convention2 = path9.join(wsDir, "src", "main.ts");
2432
+ if (fs7.existsSync(convention2)) entries.add(convention2);
2433
+ const convention3 = path9.join(wsDir, "index.ts");
2434
+ if (fs7.existsSync(convention3)) entries.add(convention3);
2435
+ }
2436
+ }
2437
+ if (!rootPkg || !workspaces.length) {
2438
+ for (const name of ["src/index.ts", "src/main.ts", "index.ts"]) {
2439
+ const convention = path9.join(projectRoot, name);
2440
+ if (fs7.existsSync(convention)) entries.add(convention);
2441
+ }
2442
+ }
2443
+ return [...entries];
2444
+ }
2445
+ var BUILD_OUTPUT_DIRS = ["dist", "out", "build", "release"];
2446
+ var BUILD_OUTPUT_DIR_NAMES = BUILD_OUTPUT_DIRS.map((d) => `${path9.sep}${d}${path9.sep}`);
2447
+ function trySourceEquivalent(resolved) {
2448
+ resolved = resolved.replace(/[/\\]/g, path9.sep);
2449
+ for (const marker of BUILD_OUTPUT_DIR_NAMES) {
2450
+ const idx = resolved.indexOf(marker);
2451
+ if (idx === -1) continue;
2452
+ const base = resolved.replace(marker, `${path9.sep}src${path9.sep}`);
2453
+ const candidate = base.replace(/\.(js|mjs|cjs)$/, ".ts");
2454
+ if (candidate !== base && fs7.existsSync(candidate)) {
2455
+ return candidate;
2456
+ }
2457
+ const dtsStripped = base.replace(/\.d\.ts$/, "");
2458
+ const candidateDts = dtsStripped + ".ts";
2459
+ if (candidateDts !== base && candidateDts !== candidate && fs7.existsSync(candidateDts)) {
2460
+ return candidateDts;
2461
+ }
2462
+ const candidateNoExt = base + ".ts";
2463
+ if (candidate !== candidateNoExt && candidateNoExt !== candidateDts && fs7.existsSync(candidateNoExt)) {
2464
+ return candidateNoExt;
2465
+ }
2466
+ }
2467
+ return null;
2468
+ }
2469
+ function tryAddEntryPath(pkgDir, rawPath, entries) {
2470
+ const resolved = resolveAgainst(pkgDir, rawPath);
2471
+ if (fs7.existsSync(resolved)) entries.add(resolved);
2472
+ const tsResolved = resolved.replace(/\.(js|mjs|cjs)$/, ".ts");
2473
+ if (tsResolved !== resolved && fs7.existsSync(tsResolved)) {
2474
+ entries.add(tsResolved);
2475
+ }
2476
+ const srcAlt = trySourceEquivalent(resolved);
2477
+ if (srcAlt) entries.add(srcAlt);
2478
+ }
2479
+ function addPkgJsonEntryPoints(pkgDir, pkg, entries) {
2480
+ if (typeof pkg.main === "string") {
2481
+ tryAddEntryPath(pkgDir, pkg.main, entries);
2482
+ }
2483
+ const bin = pkg.bin;
2484
+ if (typeof bin === "string") {
2485
+ tryAddEntryPath(pkgDir, bin, entries);
2486
+ } else if (bin && typeof bin === "object") {
2487
+ for (const value of Object.values(bin)) {
2488
+ if (typeof value === "string") {
2489
+ tryAddEntryPath(pkgDir, value, entries);
2490
+ }
2491
+ }
2492
+ }
2493
+ for (const key of ["types", "typings"]) {
2494
+ if (typeof pkg[key] === "string") {
2495
+ tryAddEntryPath(pkgDir, pkg[key], entries);
2496
+ }
2497
+ }
2498
+ const exports_ = pkg.exports;
2499
+ if (exports_ && typeof exports_ === "object") {
2500
+ for (const value of Object.values(exports_)) {
2501
+ if (typeof value === "string") {
2502
+ tryAddEntryPath(pkgDir, value, entries);
2503
+ } else if (value && typeof value === "object") {
2504
+ for (const nested of Object.values(value)) {
2505
+ if (typeof nested === "string") {
2506
+ tryAddEntryPath(pkgDir, nested, entries);
2507
+ }
2508
+ }
2509
+ }
2510
+ }
2511
+ }
2512
+ }
2513
+ function expandGlobPattern(entry, projectRoot) {
2514
+ const dirs = [];
2515
+ if (entry.includes("*")) {
2516
+ const base = entry.replace(/\/\*+$/, "");
2517
+ const baseDir = path9.resolve(projectRoot, base);
2518
+ try {
2519
+ const children = fs7.readdirSync(baseDir, { withFileTypes: true });
2520
+ for (const child of children) {
2521
+ if (child.isDirectory()) {
2522
+ dirs.push(path9.join(baseDir, child.name));
2523
+ }
2524
+ }
2525
+ } catch {
2526
+ }
2527
+ } else {
2528
+ dirs.push(path9.resolve(projectRoot, entry));
2529
+ }
2530
+ return dirs;
2531
+ }
2532
+ function extractWorkspaceGlobs(pkg, projectRoot) {
2533
+ const dirs = [];
2534
+ const workspaces = pkg.workspaces;
2535
+ if (Array.isArray(workspaces)) {
2536
+ for (const entry of workspaces) {
2537
+ if (typeof entry === "string") {
2538
+ dirs.push(...expandGlobPattern(entry, projectRoot));
2539
+ }
2540
+ }
2541
+ }
2542
+ return dirs;
2543
+ }
2544
+ function extractPnpmWorkspaceDirs(projectRoot) {
2545
+ const yamlPath = path9.join(projectRoot, "pnpm-workspace.yaml");
2546
+ if (!fs7.existsSync(yamlPath)) return [];
2547
+ try {
2548
+ const content = fs7.readFileSync(yamlPath, "utf8");
2549
+ const dirs = [];
2550
+ let inPackages = false;
2551
+ const lines = content.split("\n");
2552
+ const itemRe = /^\s+-\s+"([^"]+)"|^\s+-\s+'([^']+)'|^\s+-\s+(\S+)/;
2553
+ for (const line of lines) {
2554
+ const trimmed = line.trim();
2555
+ if (/^packages\s*:\s*$/.test(trimmed)) {
2556
+ inPackages = true;
2557
+ continue;
2558
+ }
2559
+ if (inPackages && trimmed.length > 0 && !line.startsWith(" ") && !line.startsWith(" ")) {
2560
+ if (!trimmed.startsWith("-")) {
2561
+ inPackages = false;
2562
+ continue;
2563
+ }
2564
+ }
2565
+ if (inPackages) {
2566
+ const m = itemRe.exec(line);
2567
+ if (m) {
2568
+ const entry = m[1] ?? m[2] ?? m[3];
2569
+ if (entry) {
2570
+ dirs.push(...expandGlobPattern(entry, projectRoot));
2571
+ }
2572
+ }
2573
+ }
2574
+ }
2575
+ return dirs;
2576
+ } catch {
2577
+ return [];
2578
+ }
2579
+ }
2580
+ function resolveModulePath(importerPath, moduleSpecifier, indexedFiles) {
2581
+ if (!moduleSpecifier.startsWith(".")) return [];
2582
+ const dir = path9.dirname(importerPath);
2583
+ const base = path9.resolve(dir, moduleSpecifier);
2584
+ const results = [];
2585
+ const stripped = base.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, "");
2586
+ const skipBase = stripped !== base && /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(base);
2587
+ const candidates = skipBase ? [stripped] : [base];
2588
+ for (const candidate of candidates) {
2589
+ if (indexedFiles.has(candidate + ".ts")) results.push(candidate + ".ts");
2590
+ if (indexedFiles.has(candidate + ".tsx")) results.push(candidate + ".tsx");
2591
+ if (indexedFiles.has(candidate + ".js")) results.push(candidate + ".js");
2592
+ if (indexedFiles.has(candidate + ".jsx")) results.push(candidate + ".jsx");
2593
+ if (indexedFiles.has(candidate + ".mjs")) results.push(candidate + ".mjs");
2594
+ if (indexedFiles.has(candidate + ".cjs")) results.push(candidate + ".cjs");
2595
+ if (indexedFiles.has(path9.join(candidate, "index.ts")))
2596
+ results.push(path9.join(candidate, "index.ts"));
2597
+ if (indexedFiles.has(path9.join(candidate, "index.tsx")))
2598
+ results.push(path9.join(candidate, "index.tsx"));
2599
+ if (indexedFiles.has(path9.join(candidate, "index.js")))
2600
+ results.push(path9.join(candidate, "index.js"));
2601
+ if (indexedFiles.has(path9.join(candidate, "index.jsx")))
2602
+ results.push(path9.join(candidate, "index.jsx"));
2603
+ if (indexedFiles.has(path9.join(candidate, "index.mjs")))
2604
+ results.push(path9.join(candidate, "index.mjs"));
2605
+ if (indexedFiles.has(path9.join(candidate, "index.cjs")))
2606
+ results.push(path9.join(candidate, "index.cjs"));
2607
+ }
2608
+ return [...new Set(results)];
2609
+ }
2610
+ function parseNamedExportSymbols(matchText) {
2611
+ const braceStart = matchText.indexOf("{");
2612
+ if (braceStart === -1) return null;
2613
+ const braceEnd = matchText.indexOf("}", braceStart);
2614
+ if (braceEnd === -1) return null;
2615
+ const inner = matchText.slice(braceStart + 1, braceEnd);
2616
+ const symbols = [];
2617
+ for (const part of inner.split(",")) {
2618
+ let s = part.trim();
2619
+ if (!s) continue;
2620
+ s = s.replace(/^type\s+/, "");
2621
+ const asIdx = s.search(/\s+as\s+/);
2622
+ if (asIdx !== -1) {
2623
+ s = s.slice(0, asIdx).trim();
2624
+ }
2625
+ if (s) symbols.push(s);
2626
+ }
2627
+ return symbols;
2628
+ }
2629
+ function runDeadCodeScan(projectRoot, opts = {}) {
2630
+ const store = opts.store ?? indexStorePool.acquire(projectRoot, { indexDir: opts.indexDir });
2631
+ try {
2632
+ const allSymbols = store.getAllSymbols();
2633
+ const symbolById = /* @__PURE__ */ new Map();
2634
+ for (const s of allSymbols) {
2635
+ symbolById.set(s.id, s);
2636
+ }
2637
+ const discoveredFiles = discoverEntryPoints(projectRoot, opts.userEntryPoints);
2638
+ const entryFileSet = new Set(discoveredFiles.map((f) => path9.resolve(f)));
2639
+ const indexedFiles = /* @__PURE__ */ new Set();
2640
+ for (const s of allSymbols) indexedFiles.add(s.file);
2641
+ for (const fm of store.getAllFileMetas()) indexedFiles.add(fm.file);
2642
+ const seedIds = /* @__PURE__ */ new Set();
2643
+ for (const s of allSymbols) {
2644
+ if (entryFileSet.has(s.file)) {
2645
+ seedIds.add(s.id);
2646
+ }
2647
+ }
2648
+ const fileToSymbolIds = /* @__PURE__ */ new Map();
2649
+ for (const s of allSymbols) {
2650
+ let byFile = fileToSymbolIds.get(s.file);
2651
+ if (!byFile) {
2652
+ byFile = [];
2653
+ fileToSymbolIds.set(s.file, byFile);
2654
+ }
2655
+ byFile.push(s.id);
2656
+ }
2657
+ const scannedBarrels = /* @__PURE__ */ new Set();
2658
+ const barrelWorkList = [...entryFileSet];
2659
+ while (barrelWorkList.length > 0) {
2660
+ const epFile = barrelWorkList.pop();
2661
+ if (scannedBarrels.has(epFile)) continue;
2662
+ scannedBarrels.add(epFile);
2663
+ try {
2664
+ const content = fs7.readFileSync(epFile, "utf8");
2665
+ const strippedContent = content.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length));
2666
+ const reExportRe = /export\s+(?:(?:type\s+)?\{[\s\S]*?\}\s+from|\*\s+as\s+\w+\s+from|\*\s+from)\s+['"]([^'"]+)['"]/g;
2667
+ for (; ; ) {
2668
+ const match = reExportRe.exec(strippedContent);
2669
+ if (match === null) break;
2670
+ const moduleSpec = match[1];
2671
+ const resolvedFiles = resolveModulePath(epFile, moduleSpec, indexedFiles);
2672
+ for (const rf of resolvedFiles) {
2673
+ const fileSyms = fileToSymbolIds.get(rf);
2674
+ if (fileSyms) {
2675
+ const namedSymbols = parseNamedExportSymbols(match[0]);
2676
+ if (namedSymbols) {
2677
+ const nameSet = new Set(namedSymbols);
2678
+ for (const sid of fileSyms) {
2679
+ const sym = symbolById.get(sid);
2680
+ if (sym && nameSet.has(sym.name)) seedIds.add(sid);
2681
+ }
2682
+ } else {
2683
+ for (const sid of fileSyms) seedIds.add(sid);
2684
+ }
2685
+ }
2686
+ if (!scannedBarrels.has(rf)) {
2687
+ barrelWorkList.push(rf);
2688
+ }
2689
+ }
2690
+ }
2691
+ } catch (err) {
2692
+ if (err instanceof Error && err.code !== "ENOENT") {
2693
+ console.warn(
2694
+ JSON.stringify({
2695
+ level: "warn",
2696
+ event: "dead_code_scan_barrel_read_failed",
2697
+ message: err.message,
2698
+ file: epFile,
2699
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2700
+ })
2701
+ );
2702
+ }
2703
+ }
2704
+ }
2705
+ const alive = store.findReachableSymbolIds([...seedIds]);
2706
+ const dead = [];
2707
+ const symbolsByFile = /* @__PURE__ */ new Map();
2708
+ const usedFiles = /* @__PURE__ */ new Set();
2709
+ for (const s of allSymbols) {
2710
+ if (alive.has(s.id)) {
2711
+ usedFiles.add(s.file);
2712
+ continue;
2713
+ }
2714
+ if (s.kind === "parameter") continue;
2715
+ if (s.kind === "let" || s.kind === "var") {
2716
+ continue;
2717
+ }
2718
+ const reason = "unreferenced";
2719
+ dead.push({
2720
+ name: s.name,
2721
+ kind: s.kind,
2722
+ lang: detectLang(s.file) ?? "ts",
2723
+ file: s.file,
2724
+ line: s.line,
2725
+ reason
2726
+ });
2727
+ let fileSymbols = symbolsByFile.get(s.file);
2728
+ if (!fileSymbols) {
2729
+ fileSymbols = [];
2730
+ symbolsByFile.set(s.file, fileSymbols);
2731
+ }
2732
+ fileSymbols.push(s);
2733
+ }
2734
+ const deadFiles = [];
2735
+ for (const [file, syms] of symbolsByFile) {
2736
+ let anyAlive = false;
2737
+ for (const s of allSymbols) {
2738
+ if (s.file === file && alive.has(s.id)) {
2739
+ anyAlive = true;
2740
+ break;
2741
+ }
2742
+ }
2743
+ if (!anyAlive) {
2744
+ deadFiles.push({
2745
+ file,
2746
+ symbolCount: syms.length,
2747
+ lang: detectLang(file) ?? "ts"
2748
+ });
2749
+ }
2750
+ }
2751
+ const deadPackages = [];
2752
+ const pkgEntries = findPackageEntries(projectRoot);
2753
+ for (const [pkgName, pkgDir] of pkgEntries) {
2754
+ const pkgFiles = allSymbols.filter((s) => s.file.startsWith(pkgDir + path9.sep));
2755
+ if (pkgFiles.length === 0) continue;
2756
+ const pkgUsed = pkgFiles.filter((s) => alive.has(s.id));
2757
+ if (pkgUsed.length === 0) {
2758
+ const uniqueFiles = new Set(pkgFiles.map((s) => s.file));
2759
+ deadPackages.push({
2760
+ package: pkgName,
2761
+ path: pkgDir,
2762
+ fileCount: uniqueFiles.size
2763
+ });
2764
+ }
2765
+ }
2766
+ dead.sort((a, b) => {
2767
+ if (a.reason !== b.reason) return a.reason === "unreferenced-export" ? -1 : 1;
2768
+ return a.file.localeCompare(b.file) || a.line - b.line;
2769
+ });
2770
+ return {
2771
+ deadSymbols: dead,
2772
+ deadFiles,
2773
+ deadPackages,
2774
+ entryPoints: discoveredFiles,
2775
+ stats: {
2776
+ totalSymbols: allSymbols.length,
2777
+ alive: alive.size,
2778
+ dead: dead.length,
2779
+ durationMs: 0
2780
+ }
2781
+ };
2782
+ } finally {
2783
+ if (!opts.store) {
2784
+ indexStorePool.release(store);
2785
+ }
2786
+ }
2787
+ }
2788
+ function findPackageEntries(projectRoot) {
2789
+ const pkgMap = /* @__PURE__ */ new Map();
2790
+ const rootPkg = tryReadJson(path9.join(projectRoot, "package.json"));
2791
+ if (rootPkg && typeof rootPkg.name === "string") {
2792
+ pkgMap.set(rootPkg.name, projectRoot);
2793
+ }
2794
+ if (rootPkg) {
2795
+ let wsDirs = extractWorkspaceGlobs(rootPkg, projectRoot);
2796
+ if (wsDirs.length === 0) {
2797
+ wsDirs = extractPnpmWorkspaceDirs(projectRoot);
2798
+ }
2799
+ for (const wsDir of wsDirs) {
2800
+ const wsPkg = tryReadJson(path9.join(wsDir, "package.json"));
2801
+ if (wsPkg && typeof wsPkg.name === "string") {
2802
+ pkgMap.set(wsPkg.name, wsDir);
2803
+ }
2804
+ }
2805
+ }
2806
+ return pkgMap;
2807
+ }
2808
+
2809
+ export {
2810
+ PolyglotInvariantEngine,
2811
+ polyglotInvariantEngine,
2812
+ replaceSymbolInFile,
2813
+ codebaseAstReplaceTool,
2814
+ codebaseInvariantCheckTool,
2815
+ codebaseImpactAnalysisTool,
2816
+ codebaseIncomingCallsTool,
2817
+ codebaseSearchTool,
2818
+ codebaseIndexTool,
2819
+ codebaseOutgoingCallsTool,
2820
+ extractFileSkeleton,
2821
+ extractDirectorySkeleton,
2822
+ generateRepoMap,
2823
+ codebaseRepoMapTool,
2824
+ codebaseSkeletonTool,
2825
+ codebaseStatsTool,
2826
+ codebaseTargetedTestTool,
2827
+ deadCodeScanTool,
2828
+ runDeadCodeScan
2829
+ };
2830
+ //# sourceMappingURL=chunk-L25CVUVR.js.map