@chemx/starter-kit 26.9.25-789 → 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.
Files changed (47) hide show
  1. package/README.md +2 -2
  2. package/blueprints/molecule-capsule/m-sample-card.spec.ts +10 -4
  3. package/cli/audit/prompts.js +1 -1
  4. package/cli/commands/cmd-router.js +20 -2
  5. package/cli/commands-schema.js +65 -6
  6. package/cli/generator-help.js +26 -6
  7. package/cli/generator-jig-cli.js +67 -0
  8. package/cli/generator-jig.js +479 -0
  9. package/cli/generator-jig.spec.js +148 -0
  10. package/cli/generator.js +15 -2
  11. package/cli/index.js +4 -1
  12. package/cli/installer-templates.js +8 -3
  13. package/cli/mcp/manifests.js +22 -4
  14. package/cli/mcp/tools-generate.js +22 -2
  15. package/cli/mcp/tools-read.js +50 -8
  16. package/cli/mcp/tools-verify.js +2 -0
  17. package/cli/mcp/tools.js +52 -3
  18. package/cli/project-detector.js +105 -2
  19. package/cli/reader-cli.js +146 -0
  20. package/cli/reader-logic.js +392 -0
  21. package/cli/reader-logic.spec.js +160 -0
  22. package/cli/reader.js +78 -171
  23. package/cli/search-commands-graph.js +125 -1
  24. package/cli/search-commands.js +7 -1
  25. package/cli/search-db.js +2 -0
  26. package/cli/search-queries-graph.js +294 -0
  27. package/cli/search-queries.js +6 -1
  28. package/cli/search.js +28 -30
  29. package/cli/team/team-adversarial-locks.spec.js +71 -0
  30. package/cli/team/team-commands-lock.js +64 -0
  31. package/cli/team/team-commands.js +16 -23
  32. package/cli/team/team-db-locks.js +1 -1
  33. package/cli/team/team-flags.js +4 -1
  34. package/cli/team/team-format.js +88 -0
  35. package/cli/team/team-memory.js +99 -13
  36. package/cli/team/team-memory.spec.js +49 -0
  37. package/cli/team/team-multiprocess-concurrency.spec.js +1 -1
  38. package/cli/tesseract.spec.js +2 -2
  39. package/cli/theme.js +1 -0
  40. package/cli/trace.spec.js +92 -0
  41. package/cli/verify-helpers.js +80 -18
  42. package/cli/verify-lint.js +224 -0
  43. package/cli/verify-lint.spec.js +44 -0
  44. package/cli/verify.js +8 -1
  45. package/cli/verify.spec.js +52 -0
  46. package/package.json +4 -4
  47. package/scripts/pre-commit.sh +8 -3
@@ -179,3 +179,91 @@ export const formatTaskDetailCard = (task, events = []) => {
179
179
  lines.push(`\x1b[90m${'─'.repeat(58)}\x1b[0m\n`);
180
180
  return lines.join('\n');
181
181
  };
182
+
183
+ export const formatTeamHelpCard = () => {
184
+ const lines = [
185
+ '',
186
+ '\x1b[1m\x1b[36m⚡ [Chemical X] Multi-Agent Swarm Orchestration (team)\x1b[0m',
187
+ `\x1b[90m${'─'.repeat(58)}\x1b[0m`,
188
+ ' \x1b[1mUsage:\x1b[0m chemx team <command> [options]',
189
+ '',
190
+ ' \x1b[1mCommands:\x1b[0m',
191
+ ' \x1b[32mstatus\x1b[0m Display swarm agent, task, and lock status card',
192
+ ' \x1b[32mtask\x1b[0m [action] Manage agent tasks (list, show, add, claim, done, update)',
193
+ ' \x1b[32mlock\x1b[0m [acquire|release] Acquire or release a mutual exclusion file lock',
194
+ ' \x1b[32munlock\x1b[0m <file> Release a file lock held by an agent',
195
+ ' \x1b[32mfeed\x1b[0m View swarm feed event timeline',
196
+ ' \x1b[32mpost\x1b[0m <message> Post broadcast message to swarm feed',
197
+ ' \x1b[32minbox\x1b[0m [@agent] View agent mailbox and notifications',
198
+ ' \x1b[32mdm\x1b[0m <@agent> <msg> Send direct message to another agent',
199
+ ' \x1b[32mtokens\x1b[0m Inspect token consumption and telemetry breakdown',
200
+ ' \x1b[32mtriage\x1b[0m Generate tasks automatically from AST audit hazards',
201
+ ' \x1b[32mbenchmark\x1b[0m Run memory and token reduction ablation benchmark',
202
+ ' \x1b[32mtrain\x1b[0m Train/evaluate vector and pattern indices',
203
+ '',
204
+ ' \x1b[1mGlobal Options:\x1b[0m',
205
+ ' \x1b[33m--as\x1b[0m <@agent> Execute action as specified agent handle',
206
+ ' \x1b[33m--json\x1b[0m Output machine-readable JSON format',
207
+ ' \x1b[33m--help, -h\x1b[0m Show help for team or specific subcommands',
208
+ `\x1b[90m${'─'.repeat(58)}\x1b[0m\n`
209
+ ];
210
+ return lines.join('\n');
211
+ };
212
+
213
+ export const formatTaskHelpCard = () => {
214
+ const lines = [
215
+ '',
216
+ '\x1b[1m\x1b[36m⚡ [Chemical X] Task Coordination Commands (team task)\x1b[0m',
217
+ `\x1b[90m${'─'.repeat(58)}\x1b[0m`,
218
+ ' \x1b[1mUsage:\x1b[0m chemx team task <action> [arguments] [options]',
219
+ '',
220
+ ' \x1b[1mActions:\x1b[0m',
221
+ ' \x1b[32mlist\x1b[0m List swarm tasks (default action)',
222
+ ' \x1b[32mshow\x1b[0m <taskId> Display full details and activity stream for a task',
223
+ ' \x1b[32madd\x1b[0m "<title>" Create a new task in the queue',
224
+ ' \x1b[32mclaim\x1b[0m <taskId> Claim an unassigned task for an agent',
225
+ ' \x1b[32mdone\x1b[0m <taskId> Complete a task with automatic verification audit',
226
+ ' \x1b[32mupdate\x1b[0m <taskId> [status] Update task status (in_progress, blocked, done)',
227
+ ' \x1b[32mcomment\x1b[0m <taskId> <msg> Post a comment or status update to a task',
228
+ ' \x1b[32mtriage\x1b[0m Generate tasks from AST architectural hazards',
229
+ ' \x1b[32mreconcile\x1b[0m Auto-resolve tasks whose hazards have been fixed',
230
+ ' \x1b[32mset-target\x1b[0m <id> <path> Assign target file path to a task',
231
+ ' \x1b[32mvds-slot\x1b[0m <id> <moscow> <p> Set MoSCoW slot and priority on a task',
232
+ ' \x1b[32mtrace\x1b[0m <id> <url> Attach provenance trace URL to a task',
233
+ '',
234
+ ' \x1b[1mOptions:\x1b[0m',
235
+ ' \x1b[33m--as\x1b[0m <@agent> Agent handle (e.g. @agent-1)',
236
+ ' \x1b[33m--status\x1b[0m <status> Filter by status (todo, in_progress, blocked, done)',
237
+ ' \x1b[33m--agent\x1b[0m <@agent> Filter tasks assigned to agent',
238
+ ' \x1b[33m--prio\x1b[0m <1-5> Task priority filter or setting',
239
+ ' \x1b[33m--force, -f\x1b[0m Force complete task even with remaining hazards',
240
+ ' \x1b[33m--json\x1b[0m Output machine-readable JSON format',
241
+ ' \x1b[33m--help, -h\x1b[0m Show this help message',
242
+ `\x1b[90m${'─'.repeat(58)}\x1b[0m\n`
243
+ ];
244
+ return lines.join('\n');
245
+ };
246
+
247
+ export const formatLockHelpCard = () => {
248
+ const lines = [
249
+ '',
250
+ '\x1b[1m\x1b[36m⚡ [Chemical X] Mutual Exclusion File Locking (team lock)\x1b[0m',
251
+ `\x1b[90m${'─'.repeat(58)}\x1b[0m`,
252
+ ' \x1b[1mUsage:\x1b[0m chemx team lock [acquire|release] <filePath> [options]',
253
+ ' chemx team unlock <filePath> [options]',
254
+ '',
255
+ ' \x1b[1mActions:\x1b[0m',
256
+ ' \x1b[32macquire\x1b[0m <path> Acquire mutual exclusion lock (or enqueue in FIFO)',
257
+ ' \x1b[32mrelease\x1b[0m <path> Release lock and promote next FIFO waiter',
258
+ '',
259
+ ' \x1b[1mOptions:\x1b[0m',
260
+ ' \x1b[33m--as\x1b[0m <@agent> Agent handle acquiring/releasing lock (default: @agent)',
261
+ ' \x1b[33m--purpose\x1b[0m "<reason>" Reason or intent for acquiring lock',
262
+ ' \x1b[33m--priority\x1b[0m <1-5> Priority in FIFO wait queue (1 = highest, default: 2)',
263
+ ' \x1b[33m--pid\x1b[0m <pid> Optional process PID for crash-recovery lease reclamation',
264
+ ' \x1b[33m--json\x1b[0m Output machine-readable JSON format',
265
+ ' \x1b[33m--help, -h\x1b[0m Show this help message',
266
+ `\x1b[90m${'─'.repeat(58)}\x1b[0m\n`
267
+ ];
268
+ return lines.join('\n');
269
+ };
@@ -17,7 +17,10 @@ export const recordMemoryInjection = (db, entry = {}) => {
17
17
  };
18
18
 
19
19
  export const recordMemoryUtilization = (db, { taskId, utilizedProvenance = [], causedRetry = false } = {}) => {
20
- if (!db || !taskId) return;
20
+ const hasDb = Boolean(db);
21
+ const hasTaskId = Boolean(taskId);
22
+ const canRecord = hasDb && hasTaskId;
23
+ if (!canRecord) return;
21
24
  if (causedRetry) db.prepare('UPDATE agent_memory_log SET caused_retry = 1 WHERE task_id = ?').run(taskId);
22
25
  for (const prov of utilizedProvenance) {
23
26
  db.prepare('UPDATE agent_memory_log SET was_utilized = 1 WHERE task_id = ? AND (provenance_path = ? OR provenance_path LIKE ?)').run(taskId, prov, `%${prov}%`);
@@ -51,14 +54,79 @@ export const calculateMemoryMetrics = (db) => {
51
54
 
52
55
  export const runAblationComparison = (db) => {
53
56
  const m = calculateMemoryMetrics(db);
54
- const turns = Math.max(10, m.totalCount || 10);
55
- const baseTokens = turns * 2500, memTokens = Math.max(turns * 70, m.totalInjectedTokens || 700);
56
- const tokensSaved = baseTokens - memTokens;
57
+ const row = db ? db.prepare(`
58
+ SELECT
59
+ COUNT(*) as totalTasks,
60
+ COALESCE(SUM(prompt_tokens), 0) as totalPromptTokens,
61
+ COALESCE(SUM(completion_tokens), 0) as totalCompletionTokens,
62
+ COALESCE(SUM(cached_tokens), 0) as totalCachedTokens,
63
+ COALESCE(SUM(cost_usd), 0) as totalCost
64
+ FROM agent_tasks
65
+ `).get() : null;
66
+
67
+ const taskStats = {
68
+ totalTasks: Number(row?.totalTasks || 0),
69
+ totalPromptTokens: Number(row?.totalPromptTokens || 0),
70
+ totalCompletionTokens: Number(row?.totalCompletionTokens || 0),
71
+ totalCachedTokens: Number(row?.totalCachedTokens || 0),
72
+ totalCost: Number(row?.totalCost || 0)
73
+ };
74
+
75
+ const hasTasks = taskStats.totalTasks > 0;
76
+ const hasMemories = m.totalCount > 0;
77
+ const hasRealData = Boolean(hasTasks || hasMemories);
78
+
79
+ if (!hasRealData) {
80
+ const turns = 10;
81
+ const baseTokens = turns * 2500;
82
+ const memTokens = 700;
83
+ const tokensSaved = baseTokens - memTokens;
84
+ const precisionGain = (100 / 12.0).toFixed(1) + 'x';
85
+ return {
86
+ hasRealData: false,
87
+ hasMeasuredTokens: false,
88
+ isSimulated: true,
89
+ workloadTurns: turns,
90
+ memoryEnabled: { mode: 'SQLite AST & Event Memory', tokensUsed: memTokens, staleRetryRatePct: 0.0, retrievalPrecisionPct: 100, astQualityScore: 100 },
91
+ memoryDisabled: { mode: 'Stateless Monolithic File Re-read', tokensUsed: baseTokens, staleRetryRatePct: 18.5, retrievalPrecisionPct: 12.0, astQualityScore: 84 },
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
+ }
100
+ };
101
+ }
102
+
103
+ const turns = taskStats.totalTasks + m.totalCount;
104
+ const liveTokens = taskStats.totalPromptTokens + taskStats.totalCompletionTokens;
105
+ const hasMeasuredTokens = liveTokens > 0;
106
+ const memTokens = hasMeasuredTokens ? liveTokens : Math.max(turns * 70, m.totalInjectedTokens || 70);
107
+ const baseTokens = turns * 2500 + Math.round(memTokens * 0.5);
108
+ const tokensSaved = Math.max(0, baseTokens - memTokens);
109
+ const tokenReductionPct = baseTokens > 0 ? Math.round((tokensSaved / baseTokens) * 100) : 0;
110
+ const costAvoidedUsd = Number(((tokensSaved / 1000000) * 10.0).toFixed(4));
111
+ const precisionGainRatio = (m.precisionPct / 12.0).toFixed(1) + 'x';
112
+ const retryRatePct = m.totalCount > 0 ? Number(((m.staleRetries / m.totalCount) * 100).toFixed(1)) : 0.0;
113
+ const staleRetriesEliminated = Math.max(0, Math.round(turns * 0.185) - m.staleRetries);
114
+
57
115
  return {
116
+ hasRealData: true,
117
+ hasMeasuredTokens,
118
+ isSimulated: false,
58
119
  workloadTurns: turns,
59
- memoryEnabled: { mode: 'SQLite AST & Event Memory', tokensUsed: memTokens, staleRetryRatePct: 0.0, retrievalPrecisionPct: m.precisionPct, astQualityScore: 100 },
120
+ memoryEnabled: { mode: 'SQLite AST & Event Memory', tokensUsed: memTokens, staleRetryRatePct: retryRatePct, retrievalPrecisionPct: m.precisionPct, astQualityScore: 100 },
60
121
  memoryDisabled: { mode: 'Stateless Monolithic File Re-read', tokensUsed: baseTokens, staleRetryRatePct: 18.5, retrievalPrecisionPct: 12.0, astQualityScore: 84 },
61
- delta: { tokenReductionPct: Math.round((tokensSaved / baseTokens) * 100), costAvoidedUsd: Number(((tokensSaved / 1000000) * 10.0).toFixed(4)), retrievalPrecisionGainRatio: '7.8x', staleRetriesEliminated: Math.round(turns * 0.185) }
122
+ delta: {
123
+ tokenReductionPct,
124
+ tokenReductionBasis: hasMeasuredTokens ? 'measured' : 'estimated_typical_usage',
125
+ isEstimated: !hasMeasuredTokens,
126
+ costAvoidedUsd,
127
+ retrievalPrecisionGainRatio: precisionGainRatio,
128
+ staleRetriesEliminated
129
+ }
62
130
  };
63
131
  };
64
132
 
@@ -66,19 +134,37 @@ export const formatAblationCard = (a) => {
66
134
  const m = a.memoryEnabled;
67
135
  const d = a.memoryDisabled;
68
136
  const delta = a.delta;
137
+ const headerSuffix = a.hasRealData ? '[Live Project Telemetry]' : '[Reference Projection - Zero Local Tasks]';
69
138
  const lines = [
70
139
  '',
71
- '\x1b[1m\x1b[36m⚡ [Chemical X] Persistent Memory Ablation Benchmark\x1b[0m',
72
- `\x1b[90m${'─'.repeat(58)}\x1b[0m`,
140
+ `\x1b[1m\x1b[36m⚡ [Chemical X] Persistent Memory Ablation Benchmark ${headerSuffix}\x1b[0m`,
141
+ `\x1b[90m${'─'.repeat(58)}\x1b[0m`
142
+ ];
143
+
144
+ if (!a.hasRealData) {
145
+ lines.push(
146
+ ' \x1b[33mℹ Notice: Zero local task history recorded. Displaying reference projection.\x1b[0m',
147
+ ' \x1b[33m Run team tasks and memory operations to generate live project benchmarks.\x1b[0m',
148
+ `\x1b[90m${'─'.repeat(58)}\x1b[0m`
149
+ );
150
+ }
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
+
158
+ lines.push(
73
159
  ` \x1b[1mWorkload:\x1b[0m ${a.workloadTurns} agent turns / tasks`,
74
- ` \x1b[1mMemory Mode (SQLite):\x1b[0m ${m.tokensUsed.toLocaleString()} tokens │ Precision: ${m.retrievalPrecisionPct}% │ Retry: ${m.staleRetryRatePct}%`,
75
- ` \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}%`,
76
162
  `\x1b[90m${'─'.repeat(58)}\x1b[0m`,
77
- ` \x1b[32m✔ Token Reduction:\x1b[0m ${delta.tokenReductionPct}% fewer tokens consumed`,
78
- ` \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}`,
79
165
  ` \x1b[32m✔ Precision Gain:\x1b[0m ${delta.retrievalPrecisionGainRatio} higher retrieval precision per dollar`,
80
166
  ` \x1b[32m✔ Stale Retries:\x1b[0m ${delta.staleRetriesEliminated} retries eliminated`,
81
167
  `\x1b[90m${'─'.repeat(58)}\x1b[0m\n`
82
- ];
168
+ );
83
169
  return lines.join('\n');
84
170
  };
@@ -68,7 +68,56 @@ test("runAblationComparison and formatAblationCard generate metrics card without
68
68
  assert.strictEqual(ablation.memoryEnabled.astQualityScore, 100);
69
69
  assert.strictEqual(ablation.memoryDisabled.astQualityScore, 84);
70
70
  assert.ok(ablation.delta.tokenReductionPct >= 90);
71
+ assert.strictEqual(ablation.hasRealData, false);
72
+ assert.strictEqual(ablation.isSimulated, true);
71
73
  const card = formatAblationCard(ablation);
72
74
  assert.ok(card.includes("Persistent Memory Ablation Benchmark"));
75
+ assert.ok(card.includes("Reference Projection"));
73
76
  assert.strictEqual(card.includes("—"), false);
74
77
  }));
78
+
79
+ test("runAblationComparison computes dynamic metrics from live task and memory records", () => withTmpCwd((db) => {
80
+ db.prepare(`
81
+ INSERT INTO agent_tasks (title, status, prompt_tokens, completion_tokens, cost_usd, created_at, updated_at)
82
+ VALUES ('Refactor parser', 'done', 1500, 300, 0.05, 1000, 1000)
83
+ `).run();
84
+
85
+ recordMemoryInjection(db, { taskId: 1, provenance: "src/parser.ts", tokens: 200 });
86
+ recordMemoryUtilization(db, { taskId: 1, utilizedProvenance: ["src/parser.ts"], causedRetry: false });
87
+
88
+ const ablation = runAblationComparison(db);
89
+ assert.strictEqual(ablation.hasRealData, true);
90
+ assert.strictEqual(ablation.isSimulated, false);
91
+ assert.strictEqual(ablation.workloadTurns, 2);
92
+ assert.strictEqual(ablation.memoryEnabled.tokensUsed, 1800);
93
+ assert.ok(ablation.delta.tokenReductionPct > 0);
94
+ assert.ok(ablation.delta.retrievalPrecisionGainRatio.endsWith('x'));
95
+
96
+ const card = formatAblationCard(ablation);
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");
102
+ assert.strictEqual(card.includes("—"), false);
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
+
@@ -129,7 +129,7 @@ test('swarm multi-process: lock automatically releases when holding worker proce
129
129
  import { openIndexDb } from '${path.resolve('cli/search-schema.js')}';
130
130
  import { requestFileLock } from '${path.resolve('cli/team/team-db-locks.js')}';
131
131
  const db = openIndexDb('${tmpDir}');
132
- requestFileLock(db, '${testFile}', '@crashing-worker', { ttlMs: 3600000 });
132
+ requestFileLock(db, '${testFile}', '@crashing-worker', { ttlMs: 3600000, pid: process.pid });
133
133
  process.exit(1);
134
134
  `;
135
135
 
@@ -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: 5000
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: 5000
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`);
package/cli/theme.js CHANGED
@@ -34,6 +34,7 @@ export const ANSI = Object.freeze({
34
34
  LIME: '\x1b[38;2;163;230;53m',
35
35
  GREEN: '\x1b[32m',
36
36
  GOLD: '\x1b[38;2;251;191;36m',
37
+ YELLOW: '\x1b[38;2;251;191;36m',
37
38
  RED: '\x1b[38;2;239;68;68m',
38
39
  BOLD: '\x1b[1m',
39
40
  DIM: '\x1b[2m',
@@ -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
+ });
@@ -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
- if (customCmd && customCmd.trim().length > 0) return customCmd.trim();
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 (fs.existsSync(path.join(cwd, 'vitest.config.ts')) || fs.existsSync(path.join(cwd, 'vitest.config.js'))) {
85
+ if (hasVitest) {
43
86
  return 'npx vitest run';
44
87
  }
45
88
 
@@ -91,37 +134,56 @@ export const parseTestOutput = (stdout = '', stderr = '', exitCode = 0) => {
91
134
  let skipped = 0;
92
135
  let checkmarkPasses = 0;
93
136
  let crossmarkFails = 0;
137
+ let hasRunnerSummary = false;
94
138
 
95
139
  for (const line of lines) {
96
140
  const clean = stripAnsi(line).trim();
97
141
  if (!clean) continue;
98
142
 
99
- if (clean.startsWith('✔')) checkmarkPasses++;
143
+ if (clean.startsWith('✔') || clean.startsWith('✓')) checkmarkPasses++;
100
144
  if (clean.startsWith('✖') || clean.startsWith('FAIL ')) crossmarkFails++;
101
145
 
102
- const nodeTests = clean.match(/(?:ℹ\s*)?tests\s+(\d+)/);
103
- if (nodeTests) totalTests = parseInt(nodeTests[1], 10);
146
+ const isRunnerSummary = /^Tests:?\s+/i.test(clean);
147
+ if (isRunnerSummary) {
148
+ const passM = clean.match(/(\d+)\s+passed/i);
149
+ const failM = clean.match(/(\d+)\s+failed/i);
150
+ const skipM = clean.match(/(\d+)\s+(?:skipped|todo|pending)/i);
151
+ const totalParenM = clean.match(/\((\d+)\)/);
152
+ const totalWordM = clean.match(/(\d+)\s+total/i);
153
+
154
+ if (passM) passed = parseInt(passM[1], 10);
155
+ if (failM) failed = parseInt(failM[1], 10);
156
+ if (skipM) skipped = parseInt(skipM[1], 10);
157
+
158
+ if (totalParenM) totalTests = parseInt(totalParenM[1], 10);
159
+ else if (totalWordM) totalTests = parseInt(totalWordM[1], 10);
160
+ else totalTests = passed + failed + skipped;
161
+
162
+ hasRunnerSummary = true;
163
+ continue;
164
+ }
104
165
 
105
- const nodePass = clean.match(/(?:ℹ\s*)?pass\s+(\d+)/);
106
- if (nodePass) passed = parseInt(nodePass[1], 10);
166
+ if (!hasRunnerSummary) {
167
+ const nodeTests = clean.match(/^(?:[ℹ#]\s+)?tests\s+(\d+)$/i);
168
+ if (nodeTests) totalTests = parseInt(nodeTests[1], 10);
107
169
 
108
- const nodeFail = clean.match(/(?:ℹ\s*)?fail\s+(\d+)/);
109
- if (nodeFail) failed = parseInt(nodeFail[1], 10);
170
+ const nodePass = clean.match(/^(?:[ℹ#]\s+)?pass\s+(\d+)$/i);
171
+ if (nodePass) passed = parseInt(nodePass[1], 10);
110
172
 
111
- const nodeSkip = clean.match(/(?:ℹ\s*)?(?:skipped|todo)\s+(\d+)/);
112
- if (nodeSkip) skipped += parseInt(nodeSkip[1], 10);
173
+ const nodeFail = clean.match(/^(?:[ℹ#]\s+)?fail\s+(\d+)$/i);
174
+ if (nodeFail) failed = parseInt(nodeFail[1], 10);
113
175
 
114
- const vitestMatch = clean.match(/Tests\s+(?:(\d+)\s+failed)?(?:,\s*)?(?:(\d+)\s+passed)?\s*\((\d+)\)/);
115
- if (vitestMatch) {
116
- if (vitestMatch[1]) failed = parseInt(vitestMatch[1], 10);
117
- if (vitestMatch[2]) passed = parseInt(vitestMatch[2], 10);
118
- if (vitestMatch[3]) totalTests = parseInt(vitestMatch[3], 10);
176
+ const nodeSkip = clean.match(/^(?:[ℹ#]\s+)?(?:skipped|todo)\s+(\d+)$/i);
177
+ if (nodeSkip) skipped += parseInt(nodeSkip[1], 10);
119
178
  }
120
179
  }
121
180
 
122
181
  if (passed === 0 && checkmarkPasses > 0) passed = checkmarkPasses;
123
182
  if (failed === 0 && crossmarkFails > 0) failed = crossmarkFails;
124
- if (totalTests === 0) totalTests = passed + failed;
183
+ if (totalTests === 0) totalTests = passed + failed + skipped;
184
+ const isCleanExit = exitCode === 0 && failed === 0;
185
+ const isTestCountUnderflow = isCleanExit && totalTests < passed;
186
+ if (isTestCountUnderflow) totalTests = passed;
125
187
 
126
188
  const failures = [];
127
189
  if (exitCode !== 0 || failed > 0) {