@wrongstack/tools 0.313.0 → 0.313.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.
- package/dist/audit.js +10 -4055
- package/dist/bash.js +9 -2308
- package/dist/batch-tool-use.js +2 -187
- package/dist/browser/index.js +25 -1339
- package/dist/builtin.js +69 -33949
- package/dist/chunk-2CTB3UK5.js +197 -0
- package/dist/chunk-2ME7ZWDD.js +1272 -0
- package/dist/chunk-2TFQDEPU.js +113 -0
- package/dist/chunk-2ZCJNKFE.js +315 -0
- package/dist/chunk-3PVYQFRQ.js +472 -0
- package/dist/chunk-3QMQBO5Z.js +89 -0
- package/dist/chunk-42AKKX7O.js +385 -0
- package/dist/chunk-42SKEETB.js +113 -0
- package/dist/chunk-4BHUSMNL.js +104 -0
- package/dist/chunk-4I4LWFOO.js +431 -0
- package/dist/chunk-553IQUMO.js +478 -0
- package/dist/chunk-55KF77OK.js +1944 -0
- package/dist/chunk-6HLZK37X.js +359 -0
- package/dist/chunk-6RNIFLG4.js +305 -0
- package/dist/chunk-A6BRWIXN.js +317 -0
- package/dist/chunk-AHHDWNXC.js +200 -0
- package/dist/chunk-B2CIOKWP.js +899 -0
- package/dist/chunk-BTSFTGIL.js +7 -0
- package/dist/chunk-D33S4RQG.js +222 -0
- package/dist/chunk-DIJKHSUO.js +134 -0
- package/dist/chunk-DPL47QGS.js +3515 -0
- package/dist/chunk-EMRGN4KM.js +170 -0
- package/dist/chunk-FGOABE4Z.js +955 -0
- package/dist/chunk-FW65TP4G.js +520 -0
- package/dist/chunk-GOIZNNOA.js +221 -0
- package/dist/chunk-H5MJUAA6.js +295 -0
- package/dist/chunk-H5OHH6F4.js +223 -0
- package/dist/chunk-HC5RM4WY.js +199 -0
- package/dist/chunk-HEQFMLZ7.js +190 -0
- package/dist/chunk-HFXSNKCV.js +196 -0
- package/dist/chunk-HXSBQ2SE.js +1723 -0
- package/dist/chunk-ILESNGNM.js +163 -0
- package/dist/chunk-INF5CGAO.js +137 -0
- package/dist/chunk-IWDFZ6XR.js +337 -0
- package/dist/chunk-IXMNTNW5.js +238 -0
- package/dist/chunk-J4SRJKBW.js +428 -0
- package/dist/chunk-J5SKD6LN.js +235 -0
- package/dist/chunk-JRJJX3L5.js +424 -0
- package/dist/chunk-KXNDHMUB.js +366 -0
- package/dist/chunk-LIJQMYYX.js +59 -0
- package/dist/chunk-MAN26PZ5.js +260 -0
- package/dist/chunk-MKRQMLZA.js +474 -0
- package/dist/chunk-NBDZMMG2.js +169 -0
- package/dist/chunk-NHA6BEIH.js +101 -0
- package/dist/chunk-OA6YJIWL.js +4412 -0
- package/dist/chunk-OZC255IC.js +2830 -0
- package/dist/chunk-PUR6WU2B.js +154 -0
- package/dist/chunk-Q5JPPCZC.js +12 -0
- package/dist/chunk-QBV725YG.js +128 -0
- package/dist/chunk-QHVW4AAM.js +892 -0
- package/dist/chunk-QNJ7AQ4B.js +1000 -0
- package/dist/chunk-RGISBZBD.js +231 -0
- package/dist/chunk-RJZQWEMS.js +84 -0
- package/dist/chunk-RP2ZA47V.js +15 -0
- package/dist/chunk-RXISBFR5.js +315 -0
- package/dist/chunk-S5CCLZL3.js +99 -0
- package/dist/chunk-TVTCSBJF.js +122 -0
- package/dist/chunk-U4D5S3VQ.js +21 -0
- package/dist/chunk-U4VAATKP.js +193 -0
- package/dist/chunk-UKU2ZX4C.js +203 -0
- package/dist/chunk-UNSKG6DC.js +159 -0
- package/dist/chunk-URFKEIHM.js +1091 -0
- package/dist/chunk-UTV7YAW7.js +178 -0
- package/dist/chunk-V43EQ6VR.js +68 -0
- package/dist/chunk-VB3MJGCV.js +610 -0
- package/dist/chunk-WHIIIREL.js +223 -0
- package/dist/chunk-XMIFYKUT.js +1371 -0
- package/dist/chunk-YF2RSV34.js +54 -0
- package/dist/chunk-Z6DAKQ3U.js +1 -0
- package/dist/chunk-ZIGAZFWE.js +522 -0
- package/dist/circuit-breaker.js +3 -191
- package/dist/codebase-index/index.js +84 -13688
- package/dist/codebase-index/parser-worker-script.js +8 -3995
- package/dist/codebase-index/project-server.js +60 -8548
- package/dist/codebase-index/worker.js +20 -8416
- package/dist/design.js +2 -299
- package/dist/diff.js +4 -256
- package/dist/document.js +4 -222
- package/dist/e2e.js +5 -476
- package/dist/edit.js +14 -10200
- package/dist/exec.js +16 -3131
- package/dist/fetch.js +8 -398
- package/dist/format.js +10 -3750
- package/dist/generic-parser-VUXDIAOZ.js +13 -0
- package/dist/git.js +4 -380
- package/dist/glob.js +6 -280
- package/dist/go-parser-6CA77WZ4.js +469 -0
- package/dist/grep.js +7 -604
- package/dist/index.js +318 -35387
- package/dist/install.js +10 -4091
- package/dist/json-parser-7YMDJSBJ.js +213 -0
- package/dist/json.js +4 -665
- package/dist/kanban.js +3 -2096
- package/dist/languages/index.js +23 -4664
- package/dist/legacy-bridge-7UKZT46G.js +18 -0
- package/dist/lint.js +10 -3706
- package/dist/logs.js +4 -287
- package/dist/memory.js +6 -215
- package/dist/mode.js +3 -99
- package/dist/outdated.js +4 -5170
- package/dist/pack.js +65 -33897
- package/dist/parser-dispatch-VPOFDO2U.js +14 -0
- package/dist/patch.js +13 -9935
- package/dist/plan.js +5 -3286
- package/dist/process-registry.js +8 -651
- package/dist/ps-slash.js +12 -1439
- package/dist/py-parser-GPM5VYRG.js +16 -0
- package/dist/read.js +13 -9816
- package/dist/replace.js +15 -10014
- package/dist/rs-parser-WVQ4KYCX.js +81 -0
- package/dist/scaffold.js +4 -253
- package/dist/search.js +5 -585
- package/dist/session-kanban.js +30 -972
- package/dist/skill.js +2 -215
- package/dist/task.js +5 -3410
- package/dist/test.js +10 -3773
- package/dist/todo.js +4 -2891
- package/dist/tool-help.js +3 -123
- package/dist/tool-search.js +3 -79
- package/dist/tool-tier.js +68 -33984
- package/dist/tool-use.js +2 -93
- package/dist/tree-sitter-parser-YCILHGZD.js +17 -0
- package/dist/tree.js +4 -288
- package/dist/ts-parser-YH4ZMWMH.js +232 -0
- package/dist/typecheck.js +10 -3753
- package/dist/win32.js +6 -60
- package/dist/write.js +14 -9882
- package/dist/yaml-parser-GFAOKV53.js +157 -0
- package/package.json +5 -5
|
@@ -0,0 +1,2830 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseTreeSitterAst
|
|
3
|
+
} from "./chunk-B2CIOKWP.js";
|
|
4
|
+
import {
|
|
5
|
+
spawnStream
|
|
6
|
+
} from "./chunk-MAN26PZ5.js";
|
|
7
|
+
import {
|
|
8
|
+
codebaseIndexStats,
|
|
9
|
+
enqueueReindex,
|
|
10
|
+
getIndexState,
|
|
11
|
+
incomingCallsService,
|
|
12
|
+
isIndexing,
|
|
13
|
+
outgoingCallsService,
|
|
14
|
+
runStartupIndex,
|
|
15
|
+
searchCodebaseIndex
|
|
16
|
+
} from "./chunk-2ME7ZWDD.js";
|
|
17
|
+
import {
|
|
18
|
+
IndexTimeoutError,
|
|
19
|
+
SCHEMA_VERSION,
|
|
20
|
+
codebaseIndexDirOverride,
|
|
21
|
+
indexCircuitBreaker,
|
|
22
|
+
indexStorePool,
|
|
23
|
+
resolveIndexDir
|
|
24
|
+
} from "./chunk-OA6YJIWL.js";
|
|
25
|
+
import {
|
|
26
|
+
EXT_TO_LANG,
|
|
27
|
+
detectLang,
|
|
28
|
+
isIndexablePath
|
|
29
|
+
} from "./chunk-ILESNGNM.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-OZC255IC.js.map
|