@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/reader.js CHANGED
@@ -4,63 +4,27 @@ import { parse } from '@babel/parser';
4
4
  import traverseModule from '@babel/traverse';
5
5
  import { ANSI } from './theme.js';
6
6
 
7
- const traverse = traverseModule.default || traverseModule;
8
-
9
- /**
10
- * Strips JavaScript/TypeScript comments while preserving line boundaries where possible.
11
- *
12
- * @param {string} code Source code.
13
- * @returns {string} Code without comments.
14
- */
15
- export const stripCodeComments = (code) => {
16
- return code
17
- .replace(/\/\*[\s\S]*?\*\//g, '')
18
- .replace(/(^|[^\:])\/\/.*$/gm, '$1');
19
- };
20
-
21
- /**
22
- * Collapses consecutive blank lines and trims trailing spaces.
23
- *
24
- * @param {string} code Source code.
25
- * @returns {string} Compact code.
26
- */
27
- export const compactCode = (code) => {
28
- return code
29
- .split('\n')
30
- .map((line) => line.trimEnd())
31
- .filter((line, idx, arr) => {
32
- const isCurrentEmpty = line.trim() === '';
33
- const isPrevEmpty = idx > 0 && arr[idx - 1].trim() === '';
34
- return !(isCurrentEmpty && isPrevEmpty);
35
- })
36
- .join('\n');
37
- };
38
-
39
- const resolveDeclarationKind = (declaration) => {
40
- const init = declaration.init;
41
- if (!init) return 'const';
42
-
43
- if (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') {
44
- return 'function';
45
- }
46
-
47
- if (init.type !== 'CallExpression') {
48
- return 'const';
49
- }
7
+ import {
8
+ stripCodeComments,
9
+ compactCode,
10
+ resolveDeclarationKind,
11
+ summarizeTemplate,
12
+ extractTemplateContent,
13
+ generateAstLogicSkeleton
14
+ } from './reader-logic.js';
15
+ import { runReaderCli } from './reader-cli.js';
16
+
17
+ export {
18
+ stripCodeComments,
19
+ compactCode,
20
+ resolveDeclarationKind,
21
+ summarizeTemplate,
22
+ extractTemplateContent,
23
+ generateAstLogicSkeleton
24
+ } from './reader-logic.js';
25
+ export { runReaderCli } from './reader-cli.js';
50
26
 
51
- const calleeName = init.callee?.name;
52
- if (calleeName === 'computed') {
53
- return 'computed';
54
- }
55
- if (calleeName === 'ref') {
56
- return 'ref';
57
- }
58
- if (calleeName && calleeName.startsWith('use')) {
59
- return 'hook';
60
- }
61
-
62
- return 'const';
63
- };
27
+ const traverse = traverseModule.default || traverseModule;
64
28
 
65
29
  /**
66
30
  * Extracts an AST structural outline of a file (types, exports, props, signatures).
@@ -215,6 +179,22 @@ export const extractSymbolBlock = (code, symbol) => {
215
179
  };
216
180
  };
217
181
 
182
+ /**
183
+ * Appends a compacted logic skeleton after an outline block.
184
+ * Composable overlay: enriches outline without replacing it.
185
+ *
186
+ * @param {string} rawContent Full file source.
187
+ * @param {string} filePath File path (for template detection).
188
+ * @param {object} options Reader options forwarded to skeleton generator.
189
+ * @returns {{ text: string, tokensEst: number }} Enriched section text and token estimate.
190
+ */
191
+ const enrichOutline = (rawContent, filePath, options) => {
192
+ const skeleton = generateAstLogicSkeleton(rawContent, filePath, options);
193
+ const compacted = compactCode(skeleton);
194
+ const text = `\n// --- Logic Skeleton ---\n${compacted}`;
195
+ return { text, tokensEst: Math.round(text.length / 3.8) };
196
+ };
197
+
218
198
  /**
219
199
  * Primary token-optimized file reader.
220
200
  *
@@ -249,14 +229,52 @@ export const readTokenOptimized = (targetPath, options = {}) => {
249
229
  const rawLines = rawContent.split('\n');
250
230
  const totalLines = rawLines.length;
251
231
 
232
+ if (options.template) {
233
+ const templateText = extractTemplateContent(rawContent, targetPath);
234
+ return {
235
+ file: targetPath,
236
+ totalLines,
237
+ mode: 'template',
238
+ tokensEst: Math.round(templateText.length / 3.8),
239
+ content: templateText,
240
+ };
241
+ }
242
+
243
+ if (options.logic) {
244
+ let logicSource = rawContent;
245
+ if (options.symbol) {
246
+ const block = extractSymbolBlock(rawContent, options.symbol);
247
+ if (!block) {
248
+ throw new Error(`Symbol "${options.symbol}" not found in ${targetPath}`);
249
+ }
250
+ logicSource = block.code;
251
+ }
252
+ let logicText = generateAstLogicSkeleton(logicSource, targetPath, options);
253
+ if (options.compact !== false) {
254
+ logicText = compactCode(logicText);
255
+ }
256
+ return {
257
+ file: targetPath,
258
+ totalLines,
259
+ mode: 'logic',
260
+ symbol: options.symbol || null,
261
+ lineCount: logicText.split('\n').length,
262
+ tokensEst: Math.round(logicText.length / 3.8),
263
+ content: logicText,
264
+ };
265
+ }
266
+
252
267
  if (options.outline) {
253
268
  const outlineText = generateAstOutline(rawContent, targetPath);
269
+ const enriched = options.enrich ? enrichOutline(rawContent, targetPath, options) : null;
254
270
  return {
255
271
  file: targetPath,
256
272
  totalLines,
257
273
  mode: 'outline',
258
274
  tokensEst: Math.round(outlineText.length / 3.8),
275
+ tokensEnriched: enriched ? enriched.tokensEst : 0,
259
276
  content: outlineText,
277
+ enriched: enriched ? enriched.text : null,
260
278
  };
261
279
  }
262
280
 
@@ -290,12 +308,15 @@ export const readTokenOptimized = (targetPath, options = {}) => {
290
308
  `// To read a specific block, request symbol: chemx({ action: 'read', params: { path: '${rawPath}', symbol: '<name>' } })`,
291
309
  `// Or specify a line range: chemx({ action: 'read', params: { path: '${rawPath}', startLine: 1, endLine: 50 } })\n`
292
310
  ].join('\n');
311
+ const enriched = options.enrich ? enrichOutline(rawContent, rawPath, options) : null;
293
312
  return {
294
313
  file: rawPath,
295
314
  totalLines,
296
315
  mode: 'auto-outline',
297
316
  tokensEst: Math.round((outlineText.length + notice.length) / 3.8),
298
- content: notice + outlineText
317
+ tokensEnriched: enriched ? enriched.tokensEst : 0,
318
+ content: notice + outlineText,
319
+ enriched: enriched ? enriched.text : null,
299
320
  };
300
321
  }
301
322
 
@@ -345,117 +366,3 @@ export const readTokenOptimized = (targetPath, options = {}) => {
345
366
  content: processedContent,
346
367
  };
347
368
  };
348
-
349
- /**
350
- * CLI command runner for chemx read / chemx view.
351
- *
352
- * @param {string[]} args CLI arguments.
353
- * @param {boolean} isCli Whether invoked directly from CLI.
354
- */
355
- export const runReaderCli = (args, isCli = false) => {
356
- if (args.includes('--help') || args.includes('-h') || args.includes('help')) {
357
- const isJson = args.includes('--json');
358
- if (isJson) {
359
- process.stdout.write(JSON.stringify({ help: true, success: true }) + '\n');
360
- } else {
361
- process.stdout.write([
362
- `${ANSI.BOLD}USAGE${ANSI.RESET}`,
363
- ` chemx read <file> [options]`,
364
- '',
365
- `${ANSI.BOLD}OPTIONS${ANSI.RESET}`,
366
- ` --outline AST structure outline only (80%+ token savings)`,
367
- ` --symbol=<name> Extract specific function/hook/interface declaration`,
368
- ` --start=<N> Start line number (1-indexed)`,
369
- ` --end=<N> End line number (1-indexed)`,
370
- ` --strip-comments Remove comments to minimize tokens`,
371
- ` --compact Collapse empty whitespace lines`,
372
- ` --json Output result as minified JSON`,
373
- ` -h, --help Show this help message`,
374
- ''
375
- ].join('\n'));
376
- }
377
- if (isCli) process.exit(0);
378
- return { help: true, success: true };
379
- }
380
-
381
- const nonFlagArgs = args.filter((a) => !a.startsWith('-'));
382
- let filePath = nonFlagArgs[0];
383
-
384
- if (!filePath) {
385
- process.stderr.write(`${ANSI.RED}✕ Missing file path. Usage: chemx r <file[:start-end]> [-o] [-s name]${ANSI.RESET}\n`);
386
- if (isCli) process.exit(1);
387
- return null;
388
- }
389
-
390
- let startLine;
391
- let endLine;
392
-
393
- const colonMatch = filePath.match(/^([^:]+):(\d+)(?:[-:](\d+))?$/);
394
- if (colonMatch) {
395
- filePath = colonMatch[1];
396
- startLine = parseInt(colonMatch[2], 10);
397
- if (colonMatch[3]) endLine = parseInt(colonMatch[3], 10);
398
- }
399
-
400
- const isJson = args.includes('--json') || args.includes('-j');
401
- const isOutline = args.includes('--outline') || args.includes('-o');
402
- const isCompact = args.includes('--compact') || args.includes('-c');
403
- const isStripComments = args.includes('--strip-comments') || args.includes('--no-comments') || args.includes('-sc');
404
-
405
- let symbol = null;
406
- const symEqFlag = args.find((a) => a.startsWith('--symbol=') || a.startsWith('-sym='));
407
- if (symEqFlag) {
408
- symbol = symEqFlag.split('=')[1];
409
- } else {
410
- const sIndex = args.findIndex((a) => a === '-s' || a === '--symbol');
411
- if (sIndex !== -1 && args[sIndex + 1] && !args[sIndex + 1].startsWith('-')) {
412
- symbol = args[sIndex + 1];
413
- } else {
414
- const sFlag = args.find((a) => a.startsWith('-s='));
415
- if (sFlag) {
416
- const val = sFlag.split('=')[1];
417
- if (/^\d+$/.test(val)) {
418
- if (startLine === undefined) startLine = parseInt(val, 10);
419
- } else {
420
- symbol = val;
421
- }
422
- }
423
- }
424
- }
425
-
426
- if (startLine === undefined) {
427
- const startFlag = args.find((a) => a.startsWith('--start='));
428
- if (startFlag) startLine = parseInt(startFlag.split('=')[1], 10);
429
- }
430
-
431
- if (endLine === undefined) {
432
- const endFlag = args.find((a) => a.startsWith('--end=') || a.startsWith('-e='));
433
- if (endFlag) endLine = parseInt(endFlag.split('=')[1], 10);
434
- }
435
-
436
- try {
437
- const res = readTokenOptimized(filePath, {
438
- outline: isOutline,
439
- compact: isCompact,
440
- stripComments: isStripComments,
441
- symbol,
442
- startLine,
443
- endLine,
444
- });
445
-
446
- if (isJson) {
447
- process.stdout.write(JSON.stringify(res, null, 2) + '\n');
448
- } else {
449
- process.stdout.write(`${ANSI.BOLD}${ANSI.CYAN}--- ${res.file} (${res.lineCount || res.totalLines} lines, ~${res.tokensEst} tokens) ---${ANSI.RESET}\n`);
450
- process.stdout.write(res.content + '\n');
451
- }
452
-
453
- if (isCli) process.exit(0);
454
- return res;
455
- } catch (err) {
456
- const msg = err instanceof Error ? err.message : String(err);
457
- process.stderr.write(`${ANSI.RED}✕ ${msg}${ANSI.RESET}\n`);
458
- if (isCli) process.exit(1);
459
- return null;
460
- }
461
- };
@@ -1,6 +1,11 @@
1
1
  import { ANSI } from './theme.js';
2
2
  import { toColumnar } from './columnar.js';
3
- import { calculateBlastRadius } from './search-db.js';
3
+ import {
4
+ calculateBlastRadius,
5
+ calculateCallTrace,
6
+ calculateBacktrace,
7
+ openIndexDb
8
+ } from './search-db.js';
4
9
 
5
10
  export const handleBlastRadiusCommand = (db, target, { isJson = false, isCli = true, isColumnar = false, maxDepth = 5 } = {}) => {
6
11
  const result = calculateBlastRadius(db, target, { maxDepth });
@@ -80,3 +85,122 @@ export const handleBlastRadiusCommand = (db, target, { isJson = false, isCli = t
80
85
  if (isCli) process.exit(0);
81
86
  return result;
82
87
  };
88
+
89
+ const renderCalleeTreeLines = (callees, indent = ' ') => {
90
+ const lines = [];
91
+ callees.forEach((c, idx) => {
92
+ const isLast = idx === callees.length - 1;
93
+ const branch = isLast ? '└── ' : '├── ';
94
+ const subIndent = isLast ? ' ' : '│ ';
95
+ const tag = c.isExternal ? `${ANSI.DIM}[external]${ANSI.RESET}` : `${ANSI.GOLD}[${c.tier}]${ANSI.RESET}`;
96
+ const loc = c.file ? ` ${ANSI.DIM}(${c.file}${c.line ? `:${c.line}` : ''})${ANSI.RESET}` : '';
97
+ lines.push(`${indent}${branch}${ANSI.BOLD}${c.symbol}${ANSI.RESET} ${tag}${loc}`);
98
+ if (c.callees && c.callees.length > 0) {
99
+ lines.push(...renderCalleeTreeLines(c.callees, `${indent}${subIndent}`));
100
+ }
101
+ });
102
+ return lines;
103
+ };
104
+
105
+ export const handleCallTraceCommand = (db, target, { isJson = false, isCli = true, maxDepth = 3 } = {}) => {
106
+ const result = calculateCallTrace(db, target, { maxDepth });
107
+
108
+ if (isJson) {
109
+ process.stdout.write(JSON.stringify(result) + '\n');
110
+ if (isCli) process.exit(0);
111
+ return result;
112
+ }
113
+
114
+ process.stdout.write(`\n${ANSI.BOLD}${ANSI.CYAN}Forward Call Trace:${ANSI.RESET} ${ANSI.BOLD}${result.target}${ANSI.RESET}\n`);
115
+ if (result.filePath) {
116
+ process.stdout.write(` ${ANSI.DIM}Defined in:${ANSI.RESET} ${result.filePath}:${result.startLine} ${ANSI.DIM}[${result.tier}]${ANSI.RESET}\n`);
117
+ }
118
+ process.stdout.write(` ${ANSI.MINT}Callee Summary:${ANSI.RESET} ${result.totalCallees} downstream calls mapped across ${result.depth} hops\n`);
119
+
120
+ if (result.callees.length === 0) {
121
+ process.stdout.write(` ${ANSI.DIM}No downstream calls or external invocations detected.${ANSI.RESET}\n\n`);
122
+ if (isCli) process.exit(0);
123
+ return result;
124
+ }
125
+
126
+ const treeLines = renderCalleeTreeLines(result.callees, ' ');
127
+ process.stdout.write(treeLines.join('\n') + '\n\n');
128
+
129
+ if (isCli) process.exit(0);
130
+ return result;
131
+ };
132
+
133
+ export const handleBacktraceCommand = (db, target, { isJson = false, isCli = true, maxDepth = 5 } = {}) => {
134
+ const result = calculateBacktrace(db, target, { maxDepth });
135
+
136
+ if (isJson) {
137
+ process.stdout.write(JSON.stringify(result) + '\n');
138
+ if (isCli) process.exit(0);
139
+ return result;
140
+ }
141
+
142
+ process.stdout.write(`\n${ANSI.BOLD}${ANSI.CYAN}Backtrace Causal Path:${ANSI.RESET} ${ANSI.BOLD}${result.target}${ANSI.RESET}\n`);
143
+ if (result.seedPath) {
144
+ process.stdout.write(` ${ANSI.DIM}Resolved Target:${ANSI.RESET} ${result.seedPath}\n`);
145
+ }
146
+ process.stdout.write(` ${ANSI.MINT}Caller Summary:${ANSI.RESET} ${result.totalCallers} upstream callers across ${result.depth} causal hops\n`);
147
+
148
+ if (result.totalCallers === 0) {
149
+ process.stdout.write(` ${ANSI.DIM}No upstream callers found in project imports.${ANSI.RESET}\n\n`);
150
+ if (isCli) process.exit(0);
151
+ return result;
152
+ }
153
+
154
+ if (result.chains.length > 0) {
155
+ process.stdout.write(` ${ANSI.GOLD}Causal Chains (${result.chains.length}):${ANSI.RESET}\n`);
156
+ result.chains.slice(0, 10).forEach((chain, idx) => {
157
+ process.stdout.write(` ${ANSI.DIM}${idx + 1}.${ANSI.RESET} ${chain}\n`);
158
+ });
159
+ if (result.chains.length > 10) {
160
+ process.stdout.write(` ${ANSI.DIM}...and ${result.chains.length - 10} more causal chain(s)${ANSI.RESET}\n`);
161
+ }
162
+ }
163
+
164
+ if (result.rootCallers && result.rootCallers.length > 0) {
165
+ process.stdout.write(` ${ANSI.PURPLE}Root Entry Points (${result.rootCallers.length}):${ANSI.RESET}\n`);
166
+ result.rootCallers.forEach((r) => {
167
+ process.stdout.write(` ${ANSI.BOLD}${r.path}${ANSI.RESET} ${ANSI.DIM}[${r.tier}] (depth ${r.depth})${ANSI.RESET}\n`);
168
+ });
169
+ }
170
+
171
+ process.stdout.write('\n');
172
+ if (isCli) process.exit(0);
173
+ return result;
174
+ };
175
+
176
+ export const runTraceCli = async (args = [], isCli = true) => {
177
+ const nonFlagArgs = args.filter((a) => !a.startsWith('-'));
178
+ const target = nonFlagArgs[0];
179
+ if (!target) {
180
+ process.stderr.write(`${ANSI.RED}✕ Missing symbol or file target. Usage: chemx trace <symbol> [--max-depth=N] [--json]${ANSI.RESET}\n`);
181
+ if (isCli) process.exit(1);
182
+ return null;
183
+ }
184
+ const isJson = args.includes('--json') || args.includes('-j');
185
+ const maxDepthFlag = args.find((a) => a.startsWith('--max-depth=') || a.startsWith('-d='));
186
+ const maxDepth = maxDepthFlag ? parseInt(maxDepthFlag.split('=')[1], 10) : 3;
187
+
188
+ const db = openIndexDb(process.cwd());
189
+ return handleCallTraceCommand(db, target, { isJson, isCli, maxDepth });
190
+ };
191
+
192
+ export const runBacktraceCli = async (args = [], isCli = true) => {
193
+ const nonFlagArgs = args.filter((a) => !a.startsWith('-'));
194
+ const target = nonFlagArgs[0];
195
+ if (!target) {
196
+ process.stderr.write(`${ANSI.RED}✕ Missing symbol or file target. Usage: chemx backtrace <symbol> [--max-depth=N] [--json]${ANSI.RESET}\n`);
197
+ if (isCli) process.exit(1);
198
+ return null;
199
+ }
200
+ const isJson = args.includes('--json') || args.includes('-j');
201
+ const maxDepthFlag = args.find((a) => a.startsWith('--max-depth=') || a.startsWith('-d='));
202
+ const maxDepth = maxDepthFlag ? parseInt(maxDepthFlag.split('=')[1], 10) : 5;
203
+
204
+ const db = openIndexDb(process.cwd());
205
+ return handleBacktraceCommand(db, target, { isJson, isCli, maxDepth });
206
+ };
@@ -384,7 +384,13 @@ export const handleCheckCommand = (targetFile, { isJson = false, isCli = true }
384
384
  return payload;
385
385
  };
386
386
 
387
- export { handleBlastRadiusCommand } from './search-commands-graph.js';
387
+ export {
388
+ handleBlastRadiusCommand,
389
+ handleCallTraceCommand,
390
+ handleBacktraceCommand,
391
+ runTraceCli,
392
+ runBacktraceCli
393
+ } from './search-commands-graph.js';
388
394
  export { handleSemanticCommand, handleHybridCommand } from './search-commands-semantic.js';
389
395
 
390
396
 
package/cli/search-db.js CHANGED
@@ -17,6 +17,8 @@ export {
17
17
  findFileDependencies,
18
18
  findFileDependents,
19
19
  calculateBlastRadius,
20
+ calculateCallTrace,
21
+ calculateBacktrace,
20
22
  querySemanticIndex,
21
23
  queryHybridIndex,
22
24
  syncViolationsIndex,