@chemx/starter-kit 26.9.25-943 → 26.9.26-253
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/cli/audit/prompts.js +1 -1
- package/cli/commands/cmd-router.js +20 -2
- package/cli/commands-schema.js +65 -6
- package/cli/generator-help.js +26 -6
- package/cli/generator-jig-cli.js +67 -0
- package/cli/generator-jig.js +479 -0
- package/cli/generator-jig.spec.js +148 -0
- package/cli/generator.js +15 -2
- package/cli/index.js +4 -1
- package/cli/installer-templates.js +8 -3
- package/cli/mcp/manifests.js +22 -4
- package/cli/mcp/tools-generate.js +22 -2
- package/cli/mcp/tools-read.js +50 -8
- package/cli/mcp/tools-verify.js +2 -0
- package/cli/mcp/tools.js +52 -3
- package/cli/project-detector.js +105 -2
- package/cli/reader-cli.js +146 -0
- package/cli/reader-logic.js +392 -0
- package/cli/reader-logic.spec.js +160 -0
- package/cli/reader.js +78 -171
- package/cli/search-commands-graph.js +125 -1
- package/cli/search-commands.js +7 -1
- package/cli/search-db.js +2 -0
- package/cli/search-queries-graph.js +294 -0
- package/cli/search-queries.js +6 -1
- package/cli/search.js +28 -30
- package/cli/team/team-memory.js +30 -7
- package/cli/team/team-memory.spec.js +24 -0
- package/cli/tesseract.spec.js +2 -2
- package/cli/trace.spec.js +92 -0
- package/cli/verify-helpers.js +46 -3
- package/cli/verify-lint.js +224 -0
- package/cli/verify-lint.spec.js +44 -0
- package/cli/verify.js +8 -1
- package/cli/verify.spec.js +17 -0
- package/package.json +1 -1
- package/scripts/pre-commit.sh +8 -3
|
@@ -1,4 +1,23 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { extractSymbolBlock } from './reader.js';
|
|
5
|
+
|
|
6
|
+
const _require = createRequire(import.meta.url);
|
|
7
|
+
let _parse = null;
|
|
8
|
+
let _traverse = null;
|
|
9
|
+
const loadBabel = () => {
|
|
10
|
+
if (_parse) return { parse: _parse, traverse: _traverse };
|
|
11
|
+
try {
|
|
12
|
+
_parse = _require('@babel/parser').parse;
|
|
13
|
+
const t = _require('@babel/traverse');
|
|
14
|
+
_traverse = t.default?.default || t.default || t;
|
|
15
|
+
} catch {
|
|
16
|
+
_parse = null;
|
|
17
|
+
_traverse = null;
|
|
18
|
+
}
|
|
19
|
+
return { parse: _parse, traverse: _traverse };
|
|
20
|
+
};
|
|
2
21
|
|
|
3
22
|
const isSpecOrTest = (filePath) => {
|
|
4
23
|
if (!filePath || typeof filePath !== 'string') return false;
|
|
@@ -107,3 +126,278 @@ export const calculateBlastRadius = (db, targetPathOrSymbol, options = {}) => {
|
|
|
107
126
|
tiers
|
|
108
127
|
};
|
|
109
128
|
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Extracts called function and method symbols from a code block via AST parsing.
|
|
132
|
+
* Falls back to regex extraction if @babel/parser is unavailable.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} code Source code snippet.
|
|
135
|
+
* @returns {string[]} List of unique called symbol names.
|
|
136
|
+
*/
|
|
137
|
+
export const extractCalleesFromCode = (code) => {
|
|
138
|
+
const callees = new Set();
|
|
139
|
+
const { parse, traverse } = loadBabel();
|
|
140
|
+
try {
|
|
141
|
+
if (!parse || !traverse) throw new Error('babel unavailable');
|
|
142
|
+
const ast = parse(code, {
|
|
143
|
+
sourceType: 'module',
|
|
144
|
+
plugins: ['typescript', 'jsx', 'decorators-legacy', 'topLevelAwait'],
|
|
145
|
+
errorRecovery: true,
|
|
146
|
+
});
|
|
147
|
+
traverse(ast, {
|
|
148
|
+
CallExpression(nodePath) {
|
|
149
|
+
const callee = nodePath.node.callee;
|
|
150
|
+
if (callee.type === 'Identifier') {
|
|
151
|
+
callees.add(callee.name);
|
|
152
|
+
} else if (callee.type === 'MemberExpression') {
|
|
153
|
+
const obj = callee.object?.name || callee.object?.property?.name || '';
|
|
154
|
+
const prop = callee.property?.name || '';
|
|
155
|
+
if (obj && prop) callees.add(`${obj}.${prop}`);
|
|
156
|
+
else if (prop) callees.add(prop);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
OptionalCallExpression(nodePath) {
|
|
160
|
+
const callee = nodePath.node.callee;
|
|
161
|
+
if (callee.type === 'Identifier') {
|
|
162
|
+
callees.add(callee.name);
|
|
163
|
+
} else if (callee.type === 'MemberExpression') {
|
|
164
|
+
const obj = callee.object?.name || '';
|
|
165
|
+
const prop = callee.property?.name || '';
|
|
166
|
+
if (obj && prop) callees.add(`${obj}.${prop}`);
|
|
167
|
+
else if (prop) callees.add(prop);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
} catch {
|
|
172
|
+
const matches = code.matchAll(/\b([A-Za-z0-9_$]+)\s*\(/g);
|
|
173
|
+
for (const m of matches) {
|
|
174
|
+
if (!['if', 'for', 'while', 'switch', 'catch', 'function'].includes(m[1])) {
|
|
175
|
+
callees.add(m[1]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return Array.from(callees);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Calculates forward call trace: downstream functions invoked by target symbol.
|
|
184
|
+
*
|
|
185
|
+
* @param {object} db SQLite index database.
|
|
186
|
+
* @param {string} targetSymbolOrPath Target symbol or file path.
|
|
187
|
+
* @param {object} options Traversal options.
|
|
188
|
+
* @returns {object} Call trace tree payload.
|
|
189
|
+
*/
|
|
190
|
+
export const calculateCallTrace = (db, targetSymbolOrPath, options = {}) => {
|
|
191
|
+
if (!db || !targetSymbolOrPath) {
|
|
192
|
+
return { target: targetSymbolOrPath || '', totalCallees: 0, depth: 0, callees: [] };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const cleanTarget = targetSymbolOrPath.trim();
|
|
196
|
+
const maxDepth = typeof options.maxDepth === 'number' ? options.maxDepth : 3;
|
|
197
|
+
|
|
198
|
+
const symRow = db.prepare(`
|
|
199
|
+
SELECT s.name, s.kind, s.start_line, s.end_line, s.file_path, f.tier
|
|
200
|
+
FROM symbols s
|
|
201
|
+
JOIN files f ON s.file_path = f.path
|
|
202
|
+
WHERE s.name = ?
|
|
203
|
+
LIMIT 1
|
|
204
|
+
`).get(cleanTarget);
|
|
205
|
+
|
|
206
|
+
let targetPath = symRow ? symRow.file_path : cleanTarget;
|
|
207
|
+
let symbolName = symRow ? symRow.name : cleanTarget;
|
|
208
|
+
let startLine = symRow ? Number(symRow.start_line) : 1;
|
|
209
|
+
let endLine = symRow ? Number(symRow.end_line) : 1;
|
|
210
|
+
let tier = symRow ? symRow.tier : 'utility';
|
|
211
|
+
|
|
212
|
+
const visited = new Set([symbolName]);
|
|
213
|
+
|
|
214
|
+
const traceCallees = (symbol, filePath, currentDepth) => {
|
|
215
|
+
if (currentDepth > maxDepth) return [];
|
|
216
|
+
|
|
217
|
+
let codeSlice = '';
|
|
218
|
+
let fullText = '';
|
|
219
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
|
|
220
|
+
if (fs.existsSync(absPath)) {
|
|
221
|
+
fullText = fs.readFileSync(absPath, 'utf-8');
|
|
222
|
+
const block = extractSymbolBlock(fullText, symbol);
|
|
223
|
+
if (block) {
|
|
224
|
+
codeSlice = block.code;
|
|
225
|
+
} else {
|
|
226
|
+
const fileLines = fullText.split('\n');
|
|
227
|
+
const start = Math.max(0, startLine - 1);
|
|
228
|
+
const end = Math.min(fileLines.length, endLine > start ? endLine : start + 100);
|
|
229
|
+
codeSlice = fileLines.slice(start, end).join('\n');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!codeSlice) return [];
|
|
234
|
+
|
|
235
|
+
const foundCallees = extractCalleesFromCode(codeSlice);
|
|
236
|
+
const results = [];
|
|
237
|
+
|
|
238
|
+
for (const c of foundCallees) {
|
|
239
|
+
if (visited.has(c)) continue;
|
|
240
|
+
visited.add(c);
|
|
241
|
+
|
|
242
|
+
const calleeSym = db.prepare(`
|
|
243
|
+
SELECT s.name, s.kind, s.start_line, s.end_line, s.file_path, f.tier
|
|
244
|
+
FROM symbols s
|
|
245
|
+
JOIN files f ON s.file_path = f.path
|
|
246
|
+
WHERE s.name = ?
|
|
247
|
+
LIMIT 1
|
|
248
|
+
`).get(c);
|
|
249
|
+
|
|
250
|
+
if (calleeSym) {
|
|
251
|
+
const subCallees = traceCallees(calleeSym.name, calleeSym.file_path, currentDepth + 1);
|
|
252
|
+
results.push({
|
|
253
|
+
symbol: calleeSym.name,
|
|
254
|
+
file: calleeSym.file_path,
|
|
255
|
+
line: Number(calleeSym.start_line),
|
|
256
|
+
tier: calleeSym.tier,
|
|
257
|
+
depth: currentDepth,
|
|
258
|
+
isExternal: false,
|
|
259
|
+
callees: subCallees
|
|
260
|
+
});
|
|
261
|
+
} else {
|
|
262
|
+
const baseName = c.includes('.') ? c.split('.')[0] : c;
|
|
263
|
+
const importRow = db.prepare(`
|
|
264
|
+
SELECT source_module
|
|
265
|
+
FROM imports
|
|
266
|
+
WHERE (importer_path = ? OR importer_path LIKE ?)
|
|
267
|
+
AND (imported_symbol = ? OR imported_symbol = ? OR imported_symbol = '*')
|
|
268
|
+
LIMIT 1
|
|
269
|
+
`).get(filePath, `%${path.basename(filePath)}%`, c, baseName);
|
|
270
|
+
|
|
271
|
+
if (importRow) {
|
|
272
|
+
results.push({
|
|
273
|
+
symbol: c,
|
|
274
|
+
file: importRow.source_module,
|
|
275
|
+
line: 1,
|
|
276
|
+
tier: 'external',
|
|
277
|
+
depth: currentDepth,
|
|
278
|
+
isExternal: true,
|
|
279
|
+
callees: []
|
|
280
|
+
});
|
|
281
|
+
} else {
|
|
282
|
+
const isLocal = new RegExp(`(?:const|function|let|var|class)\\s+${baseName}\\b`).test(fullText);
|
|
283
|
+
if (isLocal && baseName !== symbol) {
|
|
284
|
+
const subCallees = traceCallees(baseName, filePath, currentDepth + 1);
|
|
285
|
+
results.push({
|
|
286
|
+
symbol: c,
|
|
287
|
+
file: filePath,
|
|
288
|
+
line: 1,
|
|
289
|
+
tier: 'local',
|
|
290
|
+
depth: currentDepth,
|
|
291
|
+
isExternal: false,
|
|
292
|
+
callees: subCallees
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return results;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const calleeTree = traceCallees(symbolName, targetPath, 1);
|
|
303
|
+
const totalCount = visited.size - 1;
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
target: symbolName,
|
|
307
|
+
filePath: targetPath,
|
|
308
|
+
startLine,
|
|
309
|
+
tier,
|
|
310
|
+
totalCallees: totalCount,
|
|
311
|
+
depth: calleeTree.reduce((acc, c) => Math.max(acc, c.depth), 0),
|
|
312
|
+
callees: calleeTree
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Calculates reverse backtrace: upstream callers leading to target symbol.
|
|
318
|
+
*
|
|
319
|
+
* @param {object} db SQLite index database.
|
|
320
|
+
* @param {string} targetSymbolOrPath Target symbol or file path.
|
|
321
|
+
* @param {object} options Traversal options.
|
|
322
|
+
* @returns {object} Backtrace causal path payload.
|
|
323
|
+
*/
|
|
324
|
+
export const calculateBacktrace = (db, targetSymbolOrPath, options = {}) => {
|
|
325
|
+
if (!db || !targetSymbolOrPath) {
|
|
326
|
+
return { target: targetSymbolOrPath || '', totalCallers: 0, depth: 0, chains: [], callers: [] };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const cleanTarget = targetSymbolOrPath.trim();
|
|
330
|
+
const maxDepth = typeof options.maxDepth === 'number' ? options.maxDepth : 5;
|
|
331
|
+
|
|
332
|
+
let seedPath = '';
|
|
333
|
+
let targetSymbolName = cleanTarget;
|
|
334
|
+
const symRow = db.prepare('SELECT file_path, name FROM symbols WHERE name = ? LIMIT 1').get(cleanTarget);
|
|
335
|
+
if (symRow) {
|
|
336
|
+
seedPath = symRow.file_path;
|
|
337
|
+
targetSymbolName = symRow.name;
|
|
338
|
+
} else {
|
|
339
|
+
const fileRow = db.prepare('SELECT path FROM files WHERE path = ? OR path LIKE ? LIMIT 1').get(cleanTarget, `%${cleanTarget}%`);
|
|
340
|
+
if (fileRow) seedPath = fileRow.path;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const seedBaseName = seedPath ? path.basename(seedPath).replace(/\.[^.]+$/, '') : cleanTarget;
|
|
344
|
+
|
|
345
|
+
const querySql = `
|
|
346
|
+
WITH RECURSIVE backtrace_tree(caller_path, caller_symbol, depth, chain) AS (
|
|
347
|
+
SELECT DISTINCT i.importer_path, i.imported_symbol, 1, i.importer_path || ' (' || i.imported_symbol || ')'
|
|
348
|
+
FROM imports i
|
|
349
|
+
WHERE (? != '' AND i.imported_symbol = ?)
|
|
350
|
+
OR (i.resolved_path = ? AND i.resolved_path != '')
|
|
351
|
+
OR (? != '' AND i.source_module LIKE '%' || ? || '%')
|
|
352
|
+
|
|
353
|
+
UNION
|
|
354
|
+
|
|
355
|
+
SELECT DISTINCT i.importer_path, i.imported_symbol, bt.depth + 1, i.importer_path || ' -> ' || bt.chain
|
|
356
|
+
FROM imports i
|
|
357
|
+
JOIN backtrace_tree bt ON (
|
|
358
|
+
(i.resolved_path = bt.caller_path AND i.resolved_path != '')
|
|
359
|
+
OR i.source_module LIKE '%' || bt.caller_path || '%'
|
|
360
|
+
)
|
|
361
|
+
WHERE bt.depth < ?
|
|
362
|
+
AND instr(bt.chain, i.importer_path) = 0
|
|
363
|
+
)
|
|
364
|
+
SELECT bt.caller_path, bt.caller_symbol, min(bt.depth) as depth, bt.chain, COALESCE(f.tier, 'utility') as tier
|
|
365
|
+
FROM backtrace_tree bt
|
|
366
|
+
LEFT JOIN files f ON bt.caller_path = f.path
|
|
367
|
+
WHERE bt.caller_path != ?
|
|
368
|
+
GROUP BY bt.caller_path, bt.caller_symbol
|
|
369
|
+
ORDER BY depth DESC, bt.caller_path ASC;
|
|
370
|
+
`;
|
|
371
|
+
|
|
372
|
+
const rows = db.prepare(querySql).all(
|
|
373
|
+
targetSymbolName,
|
|
374
|
+
targetSymbolName,
|
|
375
|
+
seedPath,
|
|
376
|
+
seedBaseName,
|
|
377
|
+
seedBaseName,
|
|
378
|
+
maxDepth,
|
|
379
|
+
seedPath
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
const callers = rows.map((r) => ({
|
|
383
|
+
path: r.caller_path,
|
|
384
|
+
symbol: r.caller_symbol,
|
|
385
|
+
depth: Number(r.depth),
|
|
386
|
+
chain: r.chain,
|
|
387
|
+
tier: r.tier,
|
|
388
|
+
isEntry: ['view', 'page', 'route', 'template'].includes(r.tier) || isSpecOrTest(r.caller_path)
|
|
389
|
+
}));
|
|
390
|
+
|
|
391
|
+
const chains = callers.map((c) => `${c.chain} -> ${cleanTarget}`);
|
|
392
|
+
const maxReachedDepth = callers.reduce((acc, c) => Math.max(acc, c.depth), 0);
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
target: cleanTarget,
|
|
396
|
+
seedPath,
|
|
397
|
+
totalCallers: callers.length,
|
|
398
|
+
depth: maxReachedDepth,
|
|
399
|
+
chains,
|
|
400
|
+
callers,
|
|
401
|
+
rootCallers: callers.filter((c) => c.isEntry || c.depth === maxReachedDepth)
|
|
402
|
+
};
|
|
403
|
+
};
|
package/cli/search-queries.js
CHANGED
|
@@ -239,7 +239,12 @@ export const queryFilesByHealth = (db, { status = 'all', limit = 50 } = {}) => {
|
|
|
239
239
|
}));
|
|
240
240
|
};
|
|
241
241
|
|
|
242
|
-
export {
|
|
242
|
+
export {
|
|
243
|
+
calculateBlastRadius,
|
|
244
|
+
calculateCallTrace,
|
|
245
|
+
calculateBacktrace,
|
|
246
|
+
extractCalleesFromCode
|
|
247
|
+
} from './search-queries-graph.js';
|
|
243
248
|
export { querySemanticIndex, queryHybridIndex } from './search-queries-semantic.js';
|
|
244
249
|
|
|
245
250
|
|
package/cli/search.js
CHANGED
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
handleHealthFilterCommand,
|
|
29
29
|
handleCheckCommand,
|
|
30
30
|
handleBlastRadiusCommand,
|
|
31
|
+
handleCallTraceCommand,
|
|
32
|
+
handleBacktraceCommand,
|
|
31
33
|
handleSemanticCommand,
|
|
32
34
|
handleHybridCommand
|
|
33
35
|
} from './search-commands.js';
|
|
@@ -39,41 +41,21 @@ import { ANSI } from './theme.js';
|
|
|
39
41
|
export { toColumnar, fromColumnar } from './columnar.js';
|
|
40
42
|
|
|
41
43
|
export {
|
|
42
|
-
openIndexDb,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
findFileDependents,
|
|
49
|
-
calculateBlastRadius,
|
|
50
|
-
querySemanticIndex,
|
|
51
|
-
queryHybridIndex,
|
|
52
|
-
syncViolationsIndex,
|
|
53
|
-
queryViolations,
|
|
54
|
-
recordAuditSnapshot,
|
|
55
|
-
getAuditProgression,
|
|
56
|
-
queryFilesByHealth
|
|
44
|
+
openIndexDb, upsertFileIndex, getIndexStats,
|
|
45
|
+
findSymbolDefinition, findSymbolReferences,
|
|
46
|
+
findFileDependencies, findFileDependents,
|
|
47
|
+
calculateBlastRadius, querySemanticIndex, queryHybridIndex,
|
|
48
|
+
syncViolationsIndex, queryViolations,
|
|
49
|
+
recordAuditSnapshot, getAuditProgression, queryFilesByHealth
|
|
57
50
|
} from './search-db.js';
|
|
58
51
|
export {
|
|
59
|
-
handleCheckCommand,
|
|
60
|
-
|
|
61
|
-
handleSemanticCommand,
|
|
62
|
-
handleHybridCommand
|
|
52
|
+
handleCheckCommand, handleBlastRadiusCommand,
|
|
53
|
+
handleSemanticCommand, handleHybridCommand
|
|
63
54
|
} from './search-commands.js';
|
|
64
55
|
|
|
65
56
|
const IGNORED_DIRS = new Set([
|
|
66
|
-
'node_modules',
|
|
67
|
-
'
|
|
68
|
-
'build',
|
|
69
|
-
'vendor',
|
|
70
|
-
'.git',
|
|
71
|
-
'.next',
|
|
72
|
-
'.turbo',
|
|
73
|
-
'.output',
|
|
74
|
-
'.nuxt',
|
|
75
|
-
'.cache',
|
|
76
|
-
'out'
|
|
57
|
+
'node_modules', 'dist', 'build', 'vendor', '.git',
|
|
58
|
+
'.next', '.turbo', '.output', '.nuxt', '.cache', 'out'
|
|
77
59
|
]);
|
|
78
60
|
|
|
79
61
|
const EXCLUDED_NAME_PATTERNS = ['.test.', '.spec.', '.min.'];
|
|
@@ -370,6 +352,22 @@ export const runSearch = async (rawArgs = [], isCli = true) => {
|
|
|
370
352
|
return handleBlastRadiusCommand(db, target, { isJson, isCli, isColumnar, maxDepth });
|
|
371
353
|
}
|
|
372
354
|
|
|
355
|
+
const isTraceCommand = firstArg === 'trace' || rawArgs.includes('--trace');
|
|
356
|
+
if (isTraceCommand) {
|
|
357
|
+
const target = firstArg === 'trace' ? secondArg : firstArg;
|
|
358
|
+
const maxDepthFlag = rawArgs.find((a) => a.startsWith('--max-depth='));
|
|
359
|
+
const maxDepth = maxDepthFlag ? parseInt(maxDepthFlag.split('=')[1], 10) : 3;
|
|
360
|
+
return handleCallTraceCommand(db, target, { isJson, isCli, maxDepth });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const isBacktraceCommand = firstArg === 'backtrace' || rawArgs.includes('--backtrace');
|
|
364
|
+
if (isBacktraceCommand) {
|
|
365
|
+
const target = firstArg === 'backtrace' ? secondArg : firstArg;
|
|
366
|
+
const maxDepthFlag = rawArgs.find((a) => a.startsWith('--max-depth='));
|
|
367
|
+
const maxDepth = maxDepthFlag ? parseInt(maxDepthFlag.split('=')[1], 10) : 5;
|
|
368
|
+
return handleBacktraceCommand(db, target, { isJson, isCli, maxDepth });
|
|
369
|
+
}
|
|
370
|
+
|
|
373
371
|
const isSemanticCommand = firstArg === 'semantic' || rawArgs.includes('--semantic');
|
|
374
372
|
if (isSemanticCommand) {
|
|
375
373
|
const query = firstArg === 'semantic' ? secondArg : firstArg;
|
package/cli/team/team-memory.js
CHANGED
|
@@ -84,17 +84,26 @@ export const runAblationComparison = (db) => {
|
|
|
84
84
|
const precisionGain = (100 / 12.0).toFixed(1) + 'x';
|
|
85
85
|
return {
|
|
86
86
|
hasRealData: false,
|
|
87
|
+
hasMeasuredTokens: false,
|
|
87
88
|
isSimulated: true,
|
|
88
89
|
workloadTurns: turns,
|
|
89
90
|
memoryEnabled: { mode: 'SQLite AST & Event Memory', tokensUsed: memTokens, staleRetryRatePct: 0.0, retrievalPrecisionPct: 100, astQualityScore: 100 },
|
|
90
91
|
memoryDisabled: { mode: 'Stateless Monolithic File Re-read', tokensUsed: baseTokens, staleRetryRatePct: 18.5, retrievalPrecisionPct: 12.0, astQualityScore: 84 },
|
|
91
|
-
delta: {
|
|
92
|
+
delta: {
|
|
93
|
+
tokenReductionPct: Math.round((tokensSaved / baseTokens) * 100),
|
|
94
|
+
tokenReductionBasis: 'estimated_typical_usage',
|
|
95
|
+
isEstimated: true,
|
|
96
|
+
costAvoidedUsd: Number(((tokensSaved / 1000000) * 10.0).toFixed(4)),
|
|
97
|
+
retrievalPrecisionGainRatio: precisionGain,
|
|
98
|
+
staleRetriesEliminated: Math.round(turns * 0.185)
|
|
99
|
+
}
|
|
92
100
|
};
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
const turns = taskStats.totalTasks + m.totalCount;
|
|
96
104
|
const liveTokens = taskStats.totalPromptTokens + taskStats.totalCompletionTokens;
|
|
97
|
-
const
|
|
105
|
+
const hasMeasuredTokens = liveTokens > 0;
|
|
106
|
+
const memTokens = hasMeasuredTokens ? liveTokens : Math.max(turns * 70, m.totalInjectedTokens || 70);
|
|
98
107
|
const baseTokens = turns * 2500 + Math.round(memTokens * 0.5);
|
|
99
108
|
const tokensSaved = Math.max(0, baseTokens - memTokens);
|
|
100
109
|
const tokenReductionPct = baseTokens > 0 ? Math.round((tokensSaved / baseTokens) * 100) : 0;
|
|
@@ -105,11 +114,19 @@ export const runAblationComparison = (db) => {
|
|
|
105
114
|
|
|
106
115
|
return {
|
|
107
116
|
hasRealData: true,
|
|
117
|
+
hasMeasuredTokens,
|
|
108
118
|
isSimulated: false,
|
|
109
119
|
workloadTurns: turns,
|
|
110
120
|
memoryEnabled: { mode: 'SQLite AST & Event Memory', tokensUsed: memTokens, staleRetryRatePct: retryRatePct, retrievalPrecisionPct: m.precisionPct, astQualityScore: 100 },
|
|
111
121
|
memoryDisabled: { mode: 'Stateless Monolithic File Re-read', tokensUsed: baseTokens, staleRetryRatePct: 18.5, retrievalPrecisionPct: 12.0, astQualityScore: 84 },
|
|
112
|
-
delta: {
|
|
122
|
+
delta: {
|
|
123
|
+
tokenReductionPct,
|
|
124
|
+
tokenReductionBasis: hasMeasuredTokens ? 'measured' : 'estimated_typical_usage',
|
|
125
|
+
isEstimated: !hasMeasuredTokens,
|
|
126
|
+
costAvoidedUsd,
|
|
127
|
+
retrievalPrecisionGainRatio: precisionGainRatio,
|
|
128
|
+
staleRetriesEliminated
|
|
129
|
+
}
|
|
113
130
|
};
|
|
114
131
|
};
|
|
115
132
|
|
|
@@ -132,13 +149,19 @@ export const formatAblationCard = (a) => {
|
|
|
132
149
|
);
|
|
133
150
|
}
|
|
134
151
|
|
|
152
|
+
const tokenReductionLabel = a.hasMeasuredTokens
|
|
153
|
+
? `${delta.tokenReductionPct}% fewer tokens consumed (measured vs modeled baseline)`
|
|
154
|
+
: `${delta.tokenReductionPct}% fewer tokens consumed (estimated based on typical usage)`;
|
|
155
|
+
const costAvoidedSuffix = a.hasMeasuredTokens ? '' : ' (projected)';
|
|
156
|
+
const memoryTokensSuffix = a.hasMeasuredTokens ? '' : ' (est.)';
|
|
157
|
+
|
|
135
158
|
lines.push(
|
|
136
159
|
` \x1b[1mWorkload:\x1b[0m ${a.workloadTurns} agent turns / tasks`,
|
|
137
|
-
` \x1b[1mMemory Mode (SQLite):\x1b[0m ${m.tokensUsed.toLocaleString()} tokens │ Precision: ${m.retrievalPrecisionPct}% │ Retry: ${m.staleRetryRatePct}%`,
|
|
138
|
-
` \x1b[1mBaseline (Stateless):\x1b[0m ${d.tokensUsed.toLocaleString()} tokens │ Precision: ${d.retrievalPrecisionPct}% │ Retry: ${d.staleRetryRatePct}%`,
|
|
160
|
+
` \x1b[1mMemory Mode (SQLite):\x1b[0m ${m.tokensUsed.toLocaleString()} tokens${memoryTokensSuffix} │ Precision: ${m.retrievalPrecisionPct}% │ Retry: ${m.staleRetryRatePct}%`,
|
|
161
|
+
` \x1b[1mBaseline (Stateless):\x1b[0m ${d.tokensUsed.toLocaleString()} tokens (modeled) │ Precision: ${d.retrievalPrecisionPct}% │ Retry: ${d.staleRetryRatePct}%`,
|
|
139
162
|
`\x1b[90m${'─'.repeat(58)}\x1b[0m`,
|
|
140
|
-
` \x1b[32m✔ Token Reduction:\x1b[0m ${
|
|
141
|
-
` \x1b[32m✔ Cost Avoided:\x1b[0m ${delta.costAvoidedUsd.toFixed(4)} USD`,
|
|
163
|
+
` \x1b[32m✔ Token Reduction:\x1b[0m ${tokenReductionLabel}`,
|
|
164
|
+
` \x1b[32m✔ Cost Avoided:\x1b[0m ${delta.costAvoidedUsd.toFixed(4)} USD${costAvoidedSuffix}`,
|
|
142
165
|
` \x1b[32m✔ Precision Gain:\x1b[0m ${delta.retrievalPrecisionGainRatio} higher retrieval precision per dollar`,
|
|
143
166
|
` \x1b[32m✔ Stale Retries:\x1b[0m ${delta.staleRetriesEliminated} retries eliminated`,
|
|
144
167
|
`\x1b[90m${'─'.repeat(58)}\x1b[0m\n`
|
|
@@ -95,5 +95,29 @@ test("runAblationComparison computes dynamic metrics from live task and memory r
|
|
|
95
95
|
|
|
96
96
|
const card = formatAblationCard(ablation);
|
|
97
97
|
assert.ok(card.includes("Live Project Telemetry"));
|
|
98
|
+
assert.ok(card.includes("measured vs modeled baseline"));
|
|
99
|
+
assert.strictEqual(ablation.hasMeasuredTokens, true);
|
|
100
|
+
assert.strictEqual(ablation.delta.isEstimated, false);
|
|
101
|
+
assert.strictEqual(ablation.delta.tokenReductionBasis, "measured");
|
|
98
102
|
assert.strictEqual(card.includes("—"), false);
|
|
99
103
|
}));
|
|
104
|
+
|
|
105
|
+
test("runAblationComparison marks token reduction as estimated when live tasks lack token counts", () => withTmpCwd((db) => {
|
|
106
|
+
db.prepare(`
|
|
107
|
+
INSERT INTO agent_tasks (title, status, created_at, updated_at)
|
|
108
|
+
VALUES ('Task without token telemetry', 'done', 1000, 1000)
|
|
109
|
+
`).run();
|
|
110
|
+
|
|
111
|
+
const ablation = runAblationComparison(db);
|
|
112
|
+
assert.strictEqual(ablation.hasRealData, true);
|
|
113
|
+
assert.strictEqual(ablation.hasMeasuredTokens, false);
|
|
114
|
+
assert.strictEqual(ablation.delta.isEstimated, true);
|
|
115
|
+
assert.strictEqual(ablation.delta.tokenReductionBasis, "estimated_typical_usage");
|
|
116
|
+
assert.strictEqual(ablation.delta.tokenReductionPct, 97);
|
|
117
|
+
|
|
118
|
+
const card = formatAblationCard(ablation);
|
|
119
|
+
assert.ok(card.includes("estimated based on typical usage"));
|
|
120
|
+
assert.ok(card.includes("(projected)"));
|
|
121
|
+
assert.strictEqual(card.includes("—"), false);
|
|
122
|
+
}));
|
|
123
|
+
|
package/cli/tesseract.spec.js
CHANGED
|
@@ -27,7 +27,7 @@ test('tesseract: cli command execution succeeds with exit code 0', () => {
|
|
|
27
27
|
const cliPath = path.resolve('cli/index.js');
|
|
28
28
|
const res = spawnSync(process.execPath, [cliPath, 'tesseract'], {
|
|
29
29
|
encoding: 'utf8',
|
|
30
|
-
timeout:
|
|
30
|
+
timeout: 15000
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
assert.strictEqual(res.status, 0, 'CLI tesseract should exit with 0');
|
|
@@ -40,7 +40,7 @@ test('tesseract: cli aliases cube and matrix work identically', () => {
|
|
|
40
40
|
for (const alias of ['cube', 'matrix']) {
|
|
41
41
|
const res = spawnSync(process.execPath, [cliPath, alias], {
|
|
42
42
|
encoding: 'utf8',
|
|
43
|
-
timeout:
|
|
43
|
+
timeout: 15000
|
|
44
44
|
});
|
|
45
45
|
assert.strictEqual(res.status, 0, `CLI ${alias} should exit with 0`);
|
|
46
46
|
assert.ok(res.stdout.includes('TESSERACT'), `CLI ${alias} should render Tesseract`);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
extractCalleesFromCode,
|
|
5
|
+
calculateCallTrace,
|
|
6
|
+
calculateBacktrace
|
|
7
|
+
} from './search-queries-graph.js';
|
|
8
|
+
import {
|
|
9
|
+
handleCallTraceCommand,
|
|
10
|
+
handleBacktraceCommand
|
|
11
|
+
} from './search-commands-graph.js';
|
|
12
|
+
import { openIndexDb, upsertFileIndex } from './search-db.js';
|
|
13
|
+
|
|
14
|
+
describe('Call Trace & Backtrace Engine', () => {
|
|
15
|
+
it('extracts call expressions from code snippet', () => {
|
|
16
|
+
const code = `
|
|
17
|
+
const handleAction = async () => {
|
|
18
|
+
if (!canProceed.value) return;
|
|
19
|
+
state.value = { status: 'loading' };
|
|
20
|
+
await api.postOrder(items.value);
|
|
21
|
+
emit('success');
|
|
22
|
+
};
|
|
23
|
+
`;
|
|
24
|
+
const callees = extractCalleesFromCode(code);
|
|
25
|
+
assert.ok(callees.includes('api.postOrder'), 'Should extract api.postOrder');
|
|
26
|
+
assert.ok(callees.includes('emit'), 'Should extract emit');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('calculates forward call trace gracefully on unknown or empty target', () => {
|
|
30
|
+
const db = openIndexDb();
|
|
31
|
+
const trace = calculateCallTrace(db, 'nonExistentSymbol', { maxDepth: 2 });
|
|
32
|
+
assert.equal(trace.target, 'nonExistentSymbol');
|
|
33
|
+
assert.equal(trace.totalCallees, 0);
|
|
34
|
+
assert.deepEqual(trace.callees, []);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('calculates reverse backtrace using recursive imports CTE', () => {
|
|
38
|
+
const db = openIndexDb();
|
|
39
|
+
|
|
40
|
+
upsertFileIndex(db, {
|
|
41
|
+
path: 'src/controllers/trace-cart.ts',
|
|
42
|
+
mtime: Date.now(),
|
|
43
|
+
size: 400,
|
|
44
|
+
tier: 'hook',
|
|
45
|
+
lines: 40,
|
|
46
|
+
chars: 400,
|
|
47
|
+
symbols: [{ name: 'useTraceCartController', kind: 'const', isExport: true, startLine: 1, endLine: 40 }],
|
|
48
|
+
imports: []
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
upsertFileIndex(db, {
|
|
52
|
+
path: 'src/organisms/trace-cart.vue',
|
|
53
|
+
mtime: Date.now(),
|
|
54
|
+
size: 800,
|
|
55
|
+
tier: 'organism',
|
|
56
|
+
lines: 80,
|
|
57
|
+
chars: 800,
|
|
58
|
+
symbols: [{ name: 'TraceCartOrganism', kind: 'const', isExport: true, startLine: 1, endLine: 80 }],
|
|
59
|
+
imports: [
|
|
60
|
+
{ importedSymbol: 'useTraceCartController', sourceModule: '@/controllers/trace-cart.ts', resolvedPath: 'src/controllers/trace-cart.ts', line: 3 }
|
|
61
|
+
]
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
upsertFileIndex(db, {
|
|
65
|
+
path: 'src/views/trace-checkout.vue',
|
|
66
|
+
mtime: Date.now(),
|
|
67
|
+
size: 500,
|
|
68
|
+
tier: 'view',
|
|
69
|
+
lines: 50,
|
|
70
|
+
chars: 500,
|
|
71
|
+
symbols: [{ name: 'TraceCheckoutView', kind: 'const', isExport: true, startLine: 1, endLine: 50 }],
|
|
72
|
+
imports: [
|
|
73
|
+
{ importedSymbol: 'TraceCartOrganism', sourceModule: '@/organisms/trace-cart.vue', resolvedPath: 'src/organisms/trace-cart.vue', line: 5 }
|
|
74
|
+
]
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const backtrace = calculateBacktrace(db, 'useTraceCartController', { maxDepth: 5 });
|
|
78
|
+
assert.equal(backtrace.target, 'useTraceCartController');
|
|
79
|
+
assert.ok(backtrace.totalCallers >= 2, `Expected at least 2 callers, got ${backtrace.totalCallers}`);
|
|
80
|
+
assert.ok(backtrace.chains.some((c) => c.includes('trace-checkout.vue')), 'Causal chain should reach root view');
|
|
81
|
+
assert.ok(backtrace.rootCallers.some((r) => r.tier === 'view'), 'Root callers should include view tier');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('formats call trace and backtrace as JSON payloads', () => {
|
|
85
|
+
const db = openIndexDb();
|
|
86
|
+
const traceRes = handleCallTraceCommand(db, 'foo', { isJson: true, isCli: false });
|
|
87
|
+
assert.equal(traceRes.target, 'foo');
|
|
88
|
+
|
|
89
|
+
const backtraceRes = handleBacktraceCommand(db, 'foo', { isJson: true, isCli: false });
|
|
90
|
+
assert.equal(backtraceRes.target, 'foo');
|
|
91
|
+
});
|
|
92
|
+
});
|
package/cli/verify-helpers.js
CHANGED
|
@@ -30,16 +30,59 @@ export const detectTypecheckCommand = (customCmd, cwd = process.cwd()) => {
|
|
|
30
30
|
return `${pm} run typecheck`;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
export const detectTestCommand = (customCmd, cwd = process.cwd()) => {
|
|
34
|
-
|
|
33
|
+
export const detectTestCommand = (customCmd, cwd = process.cwd(), options = {}) => {
|
|
34
|
+
const target = options.target ? String(options.target).trim() : null;
|
|
35
|
+
const filter = options.filter ? String(options.filter).trim() : null;
|
|
36
|
+
|
|
37
|
+
if (customCmd && customCmd.trim().length > 0) {
|
|
38
|
+
let cmd = customCmd.trim();
|
|
39
|
+
if (target && !cmd.includes(target)) cmd += ` ${target}`;
|
|
40
|
+
if (filter && !cmd.includes(filter)) cmd += ` -t "${filter}"`;
|
|
41
|
+
return cmd;
|
|
42
|
+
}
|
|
43
|
+
|
|
35
44
|
const pkg = loadLocalPackageJson(cwd);
|
|
36
45
|
const scripts = (pkg && pkg.scripts) || {};
|
|
46
|
+
const deps = { ...(pkg?.dependencies || {}), ...(pkg?.devDependencies || {}) };
|
|
37
47
|
const pm = resolvePackageManager(cwd);
|
|
38
48
|
|
|
49
|
+
const hasVitest = Boolean(
|
|
50
|
+
deps.vitest ||
|
|
51
|
+
(scripts.test && scripts.test.includes('vitest')) ||
|
|
52
|
+
fs.existsSync(path.join(cwd, 'vitest.config.ts')) ||
|
|
53
|
+
fs.existsSync(path.join(cwd, 'vitest.config.js'))
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const hasNodeTest = Boolean(
|
|
57
|
+
scripts.test && (scripts.test.includes('node --test') || scripts.test.includes('node:test'))
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (target || filter) {
|
|
61
|
+
if (hasVitest) {
|
|
62
|
+
let vitestCmd = 'npx vitest run';
|
|
63
|
+
if (target) vitestCmd += ` ${target}`;
|
|
64
|
+
if (filter) vitestCmd += ` -t "${filter}"`;
|
|
65
|
+
return vitestCmd;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (hasNodeTest || target?.endsWith('.spec.js') || target?.endsWith('.test.js')) {
|
|
69
|
+
let nodeCmd = 'node --test';
|
|
70
|
+
if (target) nodeCmd += ` ${target}`;
|
|
71
|
+
if (filter) nodeCmd += ` --test-name-pattern="${filter}"`;
|
|
72
|
+
return nodeCmd;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const baseTest = pm === 'yarn' ? 'yarn test' : `${pm} test`;
|
|
76
|
+
let args = '';
|
|
77
|
+
if (target) args += ` ${target}`;
|
|
78
|
+
if (filter) args += ` -t "${filter}"`;
|
|
79
|
+
return `${baseTest} --${args}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
39
82
|
const isLegitTest = scripts.test && !scripts.test.includes('no test specified');
|
|
40
83
|
if (isLegitTest) return pm === 'yarn' ? 'yarn test' : `${pm} run test`;
|
|
41
84
|
|
|
42
|
-
if (
|
|
85
|
+
if (hasVitest) {
|
|
43
86
|
return 'npx vitest run';
|
|
44
87
|
}
|
|
45
88
|
|