@mod-computer/cli 0.2.4 → 0.2.5

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 (74) hide show
  1. package/package.json +3 -3
  2. package/dist/app.js +0 -227
  3. package/dist/cli.bundle.js.map +0 -7
  4. package/dist/cli.js +0 -132
  5. package/dist/commands/add.js +0 -245
  6. package/dist/commands/agents-run.js +0 -71
  7. package/dist/commands/auth.js +0 -259
  8. package/dist/commands/branch.js +0 -1411
  9. package/dist/commands/claude-sync.js +0 -772
  10. package/dist/commands/comment.js +0 -568
  11. package/dist/commands/diff.js +0 -182
  12. package/dist/commands/index.js +0 -73
  13. package/dist/commands/init.js +0 -597
  14. package/dist/commands/ls.js +0 -135
  15. package/dist/commands/members.js +0 -687
  16. package/dist/commands/mv.js +0 -282
  17. package/dist/commands/recover.js +0 -207
  18. package/dist/commands/rm.js +0 -257
  19. package/dist/commands/spec.js +0 -386
  20. package/dist/commands/status.js +0 -296
  21. package/dist/commands/sync.js +0 -119
  22. package/dist/commands/trace.js +0 -1752
  23. package/dist/commands/workspace.js +0 -447
  24. package/dist/components/conflict-resolution-ui.js +0 -120
  25. package/dist/components/messages.js +0 -5
  26. package/dist/components/thread.js +0 -8
  27. package/dist/config/features.js +0 -83
  28. package/dist/containers/branches-container.js +0 -140
  29. package/dist/containers/directory-container.js +0 -92
  30. package/dist/containers/thread-container.js +0 -214
  31. package/dist/containers/threads-container.js +0 -27
  32. package/dist/containers/workspaces-container.js +0 -27
  33. package/dist/daemon/conflict-resolution.js +0 -172
  34. package/dist/daemon/content-hash.js +0 -31
  35. package/dist/daemon/file-sync.js +0 -985
  36. package/dist/daemon/index.js +0 -203
  37. package/dist/daemon/mime-types.js +0 -166
  38. package/dist/daemon/offline-queue.js +0 -211
  39. package/dist/daemon/path-utils.js +0 -64
  40. package/dist/daemon/share-policy.js +0 -83
  41. package/dist/daemon/wasm-errors.js +0 -189
  42. package/dist/daemon/worker.js +0 -557
  43. package/dist/daemon-worker.js +0 -258
  44. package/dist/errors/workspace-errors.js +0 -48
  45. package/dist/lib/auth-server.js +0 -216
  46. package/dist/lib/browser.js +0 -35
  47. package/dist/lib/diff.js +0 -284
  48. package/dist/lib/formatters.js +0 -204
  49. package/dist/lib/git.js +0 -137
  50. package/dist/lib/local-fs.js +0 -201
  51. package/dist/lib/prompts.js +0 -56
  52. package/dist/lib/storage.js +0 -213
  53. package/dist/lib/trace-formatters.js +0 -314
  54. package/dist/services/add-service.js +0 -554
  55. package/dist/services/add-validation.js +0 -124
  56. package/dist/services/automatic-file-tracker.js +0 -303
  57. package/dist/services/cli-orchestrator.js +0 -227
  58. package/dist/services/feature-flags.js +0 -187
  59. package/dist/services/file-import-service.js +0 -283
  60. package/dist/services/file-transformation-service.js +0 -218
  61. package/dist/services/logger.js +0 -44
  62. package/dist/services/mod-config.js +0 -67
  63. package/dist/services/modignore-service.js +0 -328
  64. package/dist/services/sync-daemon.js +0 -244
  65. package/dist/services/thread-notification-service.js +0 -50
  66. package/dist/services/thread-service.js +0 -147
  67. package/dist/stores/use-directory-store.js +0 -96
  68. package/dist/stores/use-threads-store.js +0 -46
  69. package/dist/stores/use-workspaces-store.js +0 -54
  70. package/dist/types/add-types.js +0 -99
  71. package/dist/types/config.js +0 -16
  72. package/dist/types/index.js +0 -2
  73. package/dist/types/workspace-connection.js +0 -53
  74. package/dist/types.js +0 -1
@@ -1,1752 +0,0 @@
1
- /**
2
- * Trace Command
3
- * CLI commands for trace management and reporting
4
- *
5
- * spec: packages/mod-cli/specs/traces-cli.md
6
- */
7
- import fs from 'fs';
8
- import path from 'path';
9
- import { execSync } from 'child_process';
10
- import readline from 'readline';
11
- import { createModWorkspace } from '@mod/mod-core';
12
- import { readWorkspaceConnection } from '../lib/storage.js';
13
- /**
14
- * Prompt user for confirmation
15
- */
16
- async function confirm(message) {
17
- const rl = readline.createInterface({
18
- input: process.stdin,
19
- output: process.stdout,
20
- });
21
- return new Promise((resolve) => {
22
- rl.question(`${message} [y/N]: `, (answer) => {
23
- rl.close();
24
- resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
25
- });
26
- });
27
- }
28
- // Track if we've shown the glassware error warning already
29
- let glasswareErrorShown = false;
30
- /**
31
- * Execute a glassware query with grouping and return parsed JSON result
32
- * Uses --group-mode duplicate so nodes referencing multiple targets appear in all groups
33
- */
34
- function runGlasswareGroupedQuery(queryName, groupBy) {
35
- try {
36
- const cmd = `glassware query ${queryName} --format json --group-by "${groupBy}" --group-mode duplicate`;
37
- const output = execSync(cmd, {
38
- cwd: process.cwd(),
39
- encoding: 'utf-8',
40
- stdio: ['pipe', 'pipe', 'pipe'],
41
- maxBuffer: 100 * 1024 * 1024, // 100MB buffer for large outputs
42
- });
43
- return JSON.parse(output);
44
- }
45
- catch (error) {
46
- console.error(`Warning: Grouped query "${queryName}" failed:`, error.message);
47
- return { query: queryName, group_by: groupBy, total_count: 0, groups: [] };
48
- }
49
- }
50
- /**
51
- * Execute a glassware query and return parsed JSON result
52
- * Returns empty result on error (glassware may fail on annotation parse errors)
53
- */
54
- function runGlasswareQuery(queryName, groupBy) {
55
- try {
56
- let cmd = `glassware query ${queryName} --format json`;
57
- if (groupBy) {
58
- cmd += ` --group-by "${groupBy}"`;
59
- }
60
- const output = execSync(cmd, {
61
- cwd: process.cwd(),
62
- encoding: 'utf-8',
63
- stdio: ['pipe', 'pipe', 'pipe'],
64
- });
65
- return JSON.parse(output);
66
- }
67
- catch (error) {
68
- // Glassware may fail on annotation parse errors in the codebase
69
- // Return empty result rather than failing completely
70
- const errMsg = error.stderr?.toString() || error.message || '';
71
- if (errMsg.includes('Invalid reference') || errMsg.includes('Parse error')) {
72
- // Silently return empty result - annotation errors are common during development
73
- glasswareErrorShown = true;
74
- return { nodes: [], edges: [] };
75
- }
76
- throw new Error(`Failed to run glassware query "${queryName}": ${error.message}`);
77
- }
78
- }
79
- /**
80
- * Execute glassware show command to get a single node
81
- */
82
- function runGlasswareShow(nodeId) {
83
- try {
84
- const output = execSync(`glassware show ${nodeId}`, {
85
- cwd: process.cwd(),
86
- encoding: 'utf-8',
87
- stdio: ['pipe', 'pipe', 'pipe'],
88
- });
89
- return JSON.parse(output);
90
- }
91
- catch {
92
- return null;
93
- }
94
- }
95
- /**
96
- * Pass through to glassware CLI directly
97
- */
98
- function runGlasswareDirect(args) {
99
- try {
100
- const output = execSync(`glassware ${args.join(' ')}`, {
101
- cwd: process.cwd(),
102
- encoding: 'utf-8',
103
- stdio: ['pipe', 'pipe', 'pipe'],
104
- });
105
- return output;
106
- }
107
- catch (error) {
108
- if (error.stderr) {
109
- return error.stderr.toString();
110
- }
111
- throw error;
112
- }
113
- }
114
- function parseListArgs(args) {
115
- const result = { json: false, source: 'all' };
116
- for (let i = 0; i < args.length; i++) {
117
- const arg = args[i];
118
- if (arg === '--json') {
119
- result.json = true;
120
- }
121
- else if (arg === '--type' && args[i + 1]) {
122
- result.type = args[i + 1];
123
- i++;
124
- }
125
- else if (arg.startsWith('--type=')) {
126
- result.type = arg.slice('--type='.length);
127
- }
128
- else if (arg === '--file' && args[i + 1]) {
129
- result.file = args[i + 1];
130
- i++;
131
- }
132
- else if (arg.startsWith('--file=')) {
133
- result.file = arg.slice('--file='.length);
134
- }
135
- else if (arg === '--source' && args[i + 1]) {
136
- const src = args[i + 1];
137
- if (src === 'mod' || src === 'inline' || src === 'all') {
138
- result.source = src;
139
- }
140
- i++;
141
- }
142
- else if (arg.startsWith('--source=')) {
143
- const src = arg.slice('--source='.length);
144
- if (src === 'mod' || src === 'inline' || src === 'all') {
145
- result.source = src;
146
- }
147
- }
148
- }
149
- return result;
150
- }
151
- function parseReportArgs(args) {
152
- const result = { json: false };
153
- for (let i = 0; i < args.length; i++) {
154
- const arg = args[i];
155
- if (arg === '--json') {
156
- result.json = true;
157
- }
158
- else if (!arg.startsWith('-') && !result.file) {
159
- result.file = arg;
160
- }
161
- }
162
- return result;
163
- }
164
- function parseCoverageArgs(args) {
165
- return {
166
- json: args.includes('--json'),
167
- };
168
- }
169
- function parseAddArgs(args) {
170
- const result = { json: false, allUnmarked: false, link: [], store: 'mod' };
171
- for (let i = 0; i < args.length; i++) {
172
- const arg = args[i];
173
- if (arg === '--json') {
174
- result.json = true;
175
- }
176
- else if (arg === '--all-unmarked') {
177
- result.allUnmarked = true;
178
- }
179
- else if (arg === '--type' && args[i + 1]) {
180
- result.type = args[i + 1];
181
- i++;
182
- }
183
- else if (arg.startsWith('--type=')) {
184
- result.type = arg.slice('--type='.length);
185
- }
186
- else if (arg === '--link' && args[i + 1]) {
187
- result.link.push(args[i + 1]);
188
- i++;
189
- }
190
- else if (arg.startsWith('--link=')) {
191
- result.link.push(arg.slice('--link='.length));
192
- }
193
- else if (arg === '--store' && args[i + 1]) {
194
- const store = args[i + 1];
195
- if (store === 'mod' || store === 'inline') {
196
- result.store = store;
197
- }
198
- i++;
199
- }
200
- else if (arg.startsWith('--store=')) {
201
- const store = arg.slice('--store='.length);
202
- if (store === 'mod' || store === 'inline') {
203
- result.store = store;
204
- }
205
- }
206
- else if (!arg.startsWith('-')) {
207
- // Parse file:line format
208
- const colonIndex = arg.lastIndexOf(':');
209
- if (colonIndex > 0 && !isNaN(parseInt(arg.slice(colonIndex + 1)))) {
210
- result.file = arg.slice(0, colonIndex);
211
- result.line = parseInt(arg.slice(colonIndex + 1));
212
- }
213
- else {
214
- result.file = arg;
215
- }
216
- }
217
- }
218
- return result;
219
- }
220
- function parseLinkArgs(args) {
221
- const result = { json: false };
222
- const nonFlags = args.filter(a => !a.startsWith('-'));
223
- if (nonFlags.length >= 1)
224
- result.source = nonFlags[0];
225
- if (nonFlags.length >= 2)
226
- result.target = nonFlags[1];
227
- result.json = args.includes('--json');
228
- return result;
229
- }
230
- function parseDiffArgs(args) {
231
- const result = { json: false };
232
- for (const arg of args) {
233
- if (arg === '--json') {
234
- result.json = true;
235
- }
236
- else if (!arg.startsWith('-') && arg.includes('..')) {
237
- result.refRange = arg;
238
- }
239
- }
240
- return result;
241
- }
242
- async function requireModWorkspaceConnection(repo) {
243
- const cwd = process.cwd();
244
- const connection = readWorkspaceConnection(cwd);
245
- if (!connection) {
246
- throw new Error('Not connected to a workspace. Run "mod connect" first.');
247
- }
248
- const workspace = createModWorkspace(repo);
249
- const opened = await workspace.openWorkspace(connection.workspaceId);
250
- return { workspace, opened };
251
- }
252
- /**
253
- * Extract line number from a Trace's location
254
- */
255
- function getTraceLineNumber(trace) {
256
- if (trace.location && trace.location.type === 'inline' && typeof trace.location.line === 'number') {
257
- return trace.location.line;
258
- }
259
- return 0;
260
- }
261
- // =============================================================================
262
- // Command Handlers (Glassware-based)
263
- // =============================================================================
264
- const NODE_TYPES = ['requirement', 'specification', 'implementation', 'test'];
265
- // glassware[type="implementation", id="impl-trace-list-cmd--cfe82af5", specifications="specification-spec-traces-cli-list--2a13a560,specification-spec-traces-cli-list-type--16dc8808,specification-spec-traces-cli-list-file--33a4a34c,specification-spec-traces-cli-list-all--8ee25150,specification-spec-traces-cli-list-mod--70ded4f0,specification-spec-traces-cli-list-inline--42f73201,specification-spec-traces-cli-list-mod-filehandle--7bd5079f,specification-spec-traces-cli-json-complete--90d4f6fe"]
266
- async function handleListTraces(args, repo) {
267
- const { json, type, file, source } = parseListArgs(args);
268
- let allTraces = [];
269
- // Get inline traces from glassware
270
- if (source === 'inline' || source === 'all') {
271
- const typesToQuery = type ? [type] : NODE_TYPES;
272
- for (const nodeType of typesToQuery) {
273
- const queryName = nodeType === 'specification' ? 'specs'
274
- : nodeType === 'implementation' ? 'impls'
275
- : nodeType === 'test' ? 'tests'
276
- : null;
277
- if (queryName) {
278
- try {
279
- const result = runGlasswareQuery(queryName);
280
- for (const n of result.nodes) {
281
- allTraces.push({
282
- id: n.id,
283
- nodeType: n.type,
284
- file: n.file,
285
- line: n.line,
286
- text: n.text,
287
- source: 'inline',
288
- links: Object.entries(n.attributes || {}).flatMap(([edgeType, targets]) => targets.map(t => ({ edgeType, targetId: t }))),
289
- });
290
- }
291
- }
292
- catch {
293
- // Query might not exist, skip
294
- }
295
- }
296
- }
297
- }
298
- // Get Mod storage traces
299
- if (source === 'mod' || source === 'all') {
300
- try {
301
- const { opened } = await requireModWorkspaceConnection(repo);
302
- const files = await opened.file.list();
303
- for (const fileRef of files) {
304
- try {
305
- const handle = await opened.file.getHandle(fileRef.id);
306
- if (!handle)
307
- continue;
308
- const traces = await handle.traces.list();
309
- for (const trace of traces) {
310
- // Filter by type if specified
311
- if (type && trace.nodeType !== type)
312
- continue;
313
- allTraces.push({
314
- id: trace.id,
315
- nodeType: trace.nodeType,
316
- file: fileRef.name,
317
- line: getTraceLineNumber(trace),
318
- text: trace.text,
319
- source: 'mod',
320
- links: trace.links.map(l => ({ edgeType: l.edgeType, targetId: l.targetId })),
321
- });
322
- }
323
- }
324
- catch {
325
- // Skip files that fail to load
326
- }
327
- }
328
- }
329
- catch (error) {
330
- if (source === 'mod') {
331
- throw error; // Re-throw if mod-only was requested
332
- }
333
- // For 'all', just continue with inline traces if workspace not connected
334
- }
335
- }
336
- // Filter by file if specified
337
- if (file) {
338
- allTraces = allTraces.filter(t => t.file.includes(file));
339
- }
340
- if (json) {
341
- console.log(JSON.stringify({
342
- traces: allTraces,
343
- total: allTraces.length,
344
- }, null, 2));
345
- }
346
- else {
347
- // Text output grouped by type
348
- const byType = new Map();
349
- for (const trace of allTraces) {
350
- const list = byType.get(trace.nodeType) || [];
351
- list.push(trace);
352
- byType.set(trace.nodeType, list);
353
- }
354
- console.log('Traces');
355
- console.log('═'.repeat(70));
356
- console.log('');
357
- for (const [nodeType, traces] of byType) {
358
- console.log(`${nodeType} (${traces.length})`);
359
- console.log('─'.repeat(40));
360
- for (const trace of traces) {
361
- const linkCount = trace.links.length;
362
- const linkInfo = linkCount > 0 ? ` (${linkCount} links)` : '';
363
- const sourceTag = trace.source === 'mod' ? ' [mod]' : '';
364
- console.log(` ${trace.id}${linkInfo}${sourceTag}`);
365
- console.log(` └─ ${trace.file}:${trace.line}`);
366
- }
367
- console.log('');
368
- }
369
- console.log(`Total: ${allTraces.length} traces`);
370
- }
371
- }
372
- async function handleTraceReport(args) {
373
- const { json, file } = parseReportArgs(args);
374
- if (!file) {
375
- console.error('Usage: mod trace report <file> [--json]');
376
- process.exit(1);
377
- }
378
- // Get all specs from the file
379
- let specs = [];
380
- try {
381
- const result = runGlasswareQuery('specs');
382
- specs = result.nodes.filter(n => n.file.includes(file));
383
- }
384
- catch (error) {
385
- console.error('Error querying specs:', error.message);
386
- process.exit(1);
387
- }
388
- // Use glassware's grouping to get implementations grouped by what they implement
389
- // This lets glassware resolve the spec references correctly
390
- const implsGrouped = runGlasswareGroupedQuery('impls', 'outgoing.implements');
391
- const testsGrouped = runGlasswareGroupedQuery('tests', 'outgoing.tests');
392
- // Build lookup maps from grouped results (key is the resolved spec/impl node)
393
- const implsBySpecId = new Map();
394
- for (const group of implsGrouped.groups) {
395
- if (group.key?.id) {
396
- implsBySpecId.set(group.key.id, group.nodes);
397
- }
398
- }
399
- const testsByImplId = new Map();
400
- for (const group of testsGrouped.groups) {
401
- if (group.key?.id) {
402
- testsByImplId.set(group.key.id, group.nodes);
403
- }
404
- }
405
- // Workaround for glassware edge resolution bug: manually build test->impl mapping
406
- // by parsing test attributes and stripping type prefixes
407
- try {
408
- const testsResult = runGlasswareQuery('tests');
409
- for (const test of testsResult.nodes) {
410
- const testRefs = test.attributes?.tests || [];
411
- for (const ref of testRefs) {
412
- // Strip type prefix (e.g., "implementation-impl-foo" -> "impl-foo")
413
- let implId = ref;
414
- const prefixes = ['implementation-', 'specification-', 'requirement-', 'test-'];
415
- for (const prefix of prefixes) {
416
- if (implId.startsWith(prefix)) {
417
- implId = implId.slice(prefix.length);
418
- break;
419
- }
420
- }
421
- // Add to mapping if not already present
422
- if (!testsByImplId.has(implId)) {
423
- testsByImplId.set(implId, []);
424
- }
425
- const existing = testsByImplId.get(implId);
426
- if (!existing.some(t => t.id === test.id)) {
427
- existing.push(test);
428
- }
429
- }
430
- }
431
- }
432
- catch {
433
- // Ignore errors - continue with glassware's grouping
434
- }
435
- if (json) {
436
- console.log(JSON.stringify({
437
- file,
438
- specs: specs.map(s => ({
439
- id: s.id,
440
- text: s.text,
441
- file: s.file,
442
- line: s.line,
443
- implementations: implsBySpecId.get(s.id) || [],
444
- tests: (implsBySpecId.get(s.id) || []).flatMap(impl => testsByImplId.get(impl.id) || []),
445
- })),
446
- total: specs.length,
447
- }, null, 2));
448
- }
449
- else {
450
- const fileName = file.split('/').pop() || file;
451
- console.log(`${fileName} - Trace Report`);
452
- console.log('═'.repeat(55));
453
- console.log('');
454
- let fullyLinked = 0;
455
- let partiallyLinked = 0;
456
- let unlinked = 0;
457
- for (const spec of specs) {
458
- // Use glassware-resolved lookups (no prefix needed)
459
- const specImpls = implsBySpecId.get(spec.id) || [];
460
- const hasImpl = specImpls.length > 0;
461
- const hasTests = specImpls.some(impl => (testsByImplId.get(impl.id) || []).length > 0);
462
- const statusIcon = hasImpl && hasTests ? '✓' : hasImpl ? '⚠' : '✗';
463
- const implStatus = hasImpl ? '✓ Implemented' : '✗ Not implemented';
464
- const testStatus = hasImpl ? (hasTests ? '✓ Tested' : '✗ No tests') : '';
465
- if (hasImpl && hasTests)
466
- fullyLinked++;
467
- else if (hasImpl)
468
- partiallyLinked++;
469
- else
470
- unlinked++;
471
- const shortId = spec.id.split('--')[0];
472
- const title = spec.text.slice(0, 40);
473
- console.log(`${statusIcon} ${shortId}: ${title}`);
474
- console.log(` ${implStatus} ${testStatus}`);
475
- // glassware[type="implementation", id="impl-trace-report-impl-display--8e040441", specifications="specification-spec-traces-cli-report-impl--44744aac"]
476
- for (const impl of specImpls) {
477
- console.log(` └─ impl: ${impl.file}:${impl.line}`);
478
- // glassware[type="implementation", id="impl-trace-report-tests-display--9c17e61c", specifications="specification-spec-traces-cli-report-tests--4ec72e90"]
479
- const implTests = testsByImplId.get(impl.id) || [];
480
- for (const test of implTests) {
481
- console.log(` └─ test: ${test.file}:${test.line}`);
482
- }
483
- }
484
- console.log('');
485
- }
486
- // glassware[type="implementation", id="impl-trace-report-summary--0654d7ca", specifications="specification-spec-traces-cli-report-summary--b466878d"]
487
- console.log('─'.repeat(55));
488
- console.log(`Summary: ${fullyLinked}/${specs.length} fully linked, ` +
489
- `${partiallyLinked} partial, ${unlinked} unlinked`);
490
- }
491
- }
492
- // glassware[type="implementation", id="impl-trace-coverage-cmd--a1f6fa4e", specifications="specification-spec-traces-cli-coverage-reqs--dacbac70,specification-spec-traces-cli-coverage-impl--e5a265c6"]
493
- async function handleCoverageAnalysis(args) {
494
- const { json } = parseCoverageArgs(args);
495
- // Get counts from glassware queries
496
- let specs = [];
497
- let impls = [];
498
- let tests = [];
499
- let unmetReqs = [];
500
- let unimplSpecs = [];
501
- let untestedSpecs = [];
502
- try {
503
- specs = runGlasswareQuery('specs').nodes;
504
- }
505
- catch { /* query might not exist */ }
506
- try {
507
- impls = runGlasswareQuery('impls').nodes;
508
- }
509
- catch { /* query might not exist */ }
510
- try {
511
- tests = runGlasswareQuery('tests').nodes;
512
- }
513
- catch { /* query might not exist */ }
514
- try {
515
- unmetReqs = runGlasswareQuery('unmet-requirements').nodes;
516
- }
517
- catch { /* query might not exist */ }
518
- try {
519
- unimplSpecs = runGlasswareQuery('unimplemented-specs').nodes;
520
- }
521
- catch { /* query might not exist */ }
522
- try {
523
- untestedSpecs = runGlasswareQuery('untested-specs').nodes;
524
- }
525
- catch { /* query might not exist */ }
526
- const coverage = {
527
- specifications: {
528
- total: specs.length,
529
- implemented: specs.length - unimplSpecs.length,
530
- tested: specs.length - untestedSpecs.length,
531
- },
532
- implementations: {
533
- total: impls.length,
534
- },
535
- tests: {
536
- total: tests.length,
537
- },
538
- unmet: unmetReqs.length,
539
- };
540
- if (json) {
541
- console.log(JSON.stringify({ coverage }, null, 2));
542
- }
543
- else {
544
- console.log('Trace Coverage');
545
- console.log('═'.repeat(55));
546
- console.log('');
547
- // Specifications coverage
548
- const specPct = specs.length > 0
549
- ? Math.round((coverage.specifications.implemented / specs.length) * 100)
550
- : 0;
551
- const specBar = '█'.repeat(Math.floor(specPct / 6.25)) + '░'.repeat(16 - Math.floor(specPct / 6.25));
552
- console.log(`specification:`);
553
- console.log(` ${specBar} ${specPct}% (${coverage.specifications.implemented}/${specs.length} implemented)`);
554
- // Test coverage
555
- const testPct = specs.length > 0
556
- ? Math.round((coverage.specifications.tested / specs.length) * 100)
557
- : 0;
558
- const testBar = '█'.repeat(Math.floor(testPct / 6.25)) + '░'.repeat(16 - Math.floor(testPct / 6.25));
559
- console.log(`tested:`);
560
- console.log(` ${testBar} ${testPct}% (${coverage.specifications.tested}/${specs.length} tested)`);
561
- console.log('');
562
- console.log('─'.repeat(55));
563
- console.log(`Total: ${specs.length} specs, ${impls.length} impls, ${tests.length} tests`);
564
- if (unmetReqs.length > 0) {
565
- console.log(`⚠ Unmet requirements: ${unmetReqs.length}`);
566
- }
567
- if (unimplSpecs.length > 0) {
568
- console.log(`⚠ Unimplemented specs: ${unimplSpecs.length}`);
569
- }
570
- }
571
- }
572
- async function handleUnmetRequirements(args) {
573
- const json = args.includes('--json');
574
- let unmet = [];
575
- try {
576
- unmet = runGlasswareQuery('unmet-requirements').nodes;
577
- }
578
- catch (error) {
579
- console.error('Error querying unmet requirements:', error.message);
580
- process.exit(1);
581
- }
582
- if (json) {
583
- console.log(JSON.stringify({
584
- unmet: unmet.map(n => ({
585
- id: n.id,
586
- title: n.text,
587
- file: n.file,
588
- line: n.line,
589
- })),
590
- total: unmet.length,
591
- }, null, 2));
592
- }
593
- else {
594
- if (unmet.length === 0) {
595
- console.log('✓ All requirements have implementations');
596
- return;
597
- }
598
- console.log('Unmet Requirements');
599
- console.log('═'.repeat(55));
600
- console.log('');
601
- for (const req of unmet) {
602
- const shortId = req.id.split('--')[0];
603
- console.log(`✗ ${shortId}`);
604
- console.log(` "${req.text.slice(0, 50)}"`);
605
- console.log(` └─ ${req.file}:${req.line}`);
606
- console.log('');
607
- }
608
- console.log(`Total: ${unmet.length} unmet requirements`);
609
- }
610
- // Exit non-zero if unmet requirements exist
611
- if (unmet.length > 0) {
612
- process.exit(1);
613
- }
614
- }
615
- // glassware[type="implementation", id="impl-trace-diff-cmd--wip", specifications="spec-traces-cli-diff--wip,spec-traces-cli-diff-passthrough--wip,spec-traces-cli-diff-report--wip,spec-traces-cli-diff-suggest--wip,spec-traces-cli-diff-json--wip"]
616
- async function handleTraceDiff(args) {
617
- const { json, refRange } = parseDiffArgs(args);
618
- let base;
619
- let head;
620
- let branchInfo = '';
621
- if (refRange && refRange.includes('..')) {
622
- // Explicit range provided
623
- [base, head] = refRange.split('..');
624
- }
625
- else {
626
- // Auto-detect: find merge-base with main/master
627
- try {
628
- const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
629
- encoding: 'utf-8',
630
- stdio: ['pipe', 'pipe', 'pipe'],
631
- }).trim();
632
- // Try main, then master
633
- let defaultBranch = 'main';
634
- try {
635
- execSync('git rev-parse --verify main', { stdio: 'ignore' });
636
- }
637
- catch {
638
- try {
639
- execSync('git rev-parse --verify master', { stdio: 'ignore' });
640
- defaultBranch = 'master';
641
- }
642
- catch {
643
- console.error('Error: Could not find main or master branch');
644
- process.exit(1);
645
- }
646
- }
647
- // Find where current branch diverged from default
648
- const mergeBase = execSync(`git merge-base ${defaultBranch} HEAD`, {
649
- encoding: 'utf-8',
650
- stdio: ['pipe', 'pipe', 'pipe'],
651
- }).trim();
652
- base = mergeBase;
653
- head = 'HEAD';
654
- branchInfo = `Branch: ${currentBranch} (compared to ${defaultBranch})`;
655
- }
656
- catch (error) {
657
- console.error('Error detecting git branch:', error.message);
658
- process.exit(1);
659
- }
660
- }
661
- // Get changed files
662
- let changedFiles;
663
- try {
664
- const output = execSync(`git diff --name-only ${base}..${head}`, {
665
- encoding: 'utf-8',
666
- stdio: ['pipe', 'pipe', 'pipe'],
667
- });
668
- changedFiles = output.trim().split('\n').filter(Boolean);
669
- }
670
- catch (error) {
671
- console.error('Error getting changed files:', error.message);
672
- process.exit(1);
673
- }
674
- if (changedFiles.length === 0) {
675
- if (json) {
676
- console.log(JSON.stringify({
677
- branch: branchInfo,
678
- traced: [],
679
- untraced: [],
680
- total: 0,
681
- }, null, 2));
682
- }
683
- else {
684
- if (branchInfo)
685
- console.log(branchInfo);
686
- console.log('\nNo files changed.');
687
- }
688
- return;
689
- }
690
- // Query all traces to build a file->trace map
691
- const fileTraceMap = new Map();
692
- // Get inline traces from glassware
693
- for (const nodeType of NODE_TYPES) {
694
- const queryName = nodeType === 'specification' ? 'specs'
695
- : nodeType === 'implementation' ? 'impls'
696
- : nodeType === 'test' ? 'tests'
697
- : null;
698
- if (queryName) {
699
- try {
700
- const result = runGlasswareQuery(queryName);
701
- for (const n of result.nodes) {
702
- const existing = fileTraceMap.get(n.file) || [];
703
- existing.push({ id: n.id, type: n.type });
704
- fileTraceMap.set(n.file, existing);
705
- }
706
- }
707
- catch {
708
- // Query might not exist, skip
709
- }
710
- }
711
- }
712
- // Group changed files into traced vs untraced
713
- const traced = [];
714
- const untraced = [];
715
- for (const file of changedFiles) {
716
- const traces = fileTraceMap.get(file);
717
- if (traces && traces.length > 0) {
718
- traced.push({ file, traceId: traces[0].id });
719
- }
720
- else {
721
- // Only flag source files as untraced, skip non-code files
722
- const ext = path.extname(file);
723
- const codeExtensions = ['.ts', '.tsx', '.js', '.jsx', '.md'];
724
- const ignorePaths = ['node_modules', 'dist', '.git', 'package.json', 'package-lock.json', 'pnpm-lock.yaml', '.gitignore', 'tsconfig'];
725
- if (codeExtensions.includes(ext) && !ignorePaths.some(p => file.includes(p))) {
726
- untraced.push(file);
727
- }
728
- }
729
- }
730
- // Output
731
- if (json) {
732
- console.log(JSON.stringify({
733
- branch: branchInfo,
734
- traced,
735
- untraced,
736
- total: changedFiles.length,
737
- }, null, 2));
738
- }
739
- else {
740
- if (branchInfo) {
741
- console.log(branchInfo);
742
- console.log('');
743
- }
744
- if (traced.length > 0) {
745
- console.log('Traced:');
746
- for (const t of traced) {
747
- const shortId = t.traceId.split('--')[0];
748
- console.log(` ${t.file.padEnd(40)} → ${shortId}`);
749
- }
750
- }
751
- if (untraced.length > 0) {
752
- console.log('');
753
- console.log('⚠ Untraced:');
754
- for (const f of untraced) {
755
- console.log(` ${f}`);
756
- }
757
- console.log('');
758
- console.log('Suggestion: mod trace add <file> --type=utility');
759
- }
760
- if (traced.length > 0 && untraced.length === 0) {
761
- console.log('');
762
- console.log('✓ All changed files are traced');
763
- }
764
- }
765
- // Exit non-zero if untraced files exist
766
- if (untraced.length > 0) {
767
- process.exit(1);
768
- }
769
- }
770
- // =============================================================================
771
- // Add/Link Commands (File modification - kept from original)
772
- // =============================================================================
773
- // glassware[type="implementation", id="impl-trace-add-cmd--6893858a", specifications="specification-spec-traces-cli-add--daf76036,specification-spec-traces-cli-add-mod-store--522d5263,specification-spec-traces-cli-add-mod-filehandle--9d81aee5,specification-spec-traces-cli-add-store-mod--be78263b,specification-spec-traces-cli-add-store-inline--783bd8a6,specification-spec-traces-cli-add-link--ee46baf2,specification-spec-traces-cli-add-title-from-line--e24dc753,specification-spec-traces-cli-add-insert--d956f641,specification-spec-traces-cli-add-mod-anchor--fb9b7f41"]
774
- async function handleAddTrace(args, repo) {
775
- const { json, type, file, line, link, allUnmarked, store } = parseAddArgs(args);
776
- if (!type) {
777
- console.error('Usage: mod trace add <file>:<line> --type=<type> [--link=<id>] [--store=mod|inline]');
778
- console.error(' mod trace add <file> --type=<type> --all-unmarked [--store=inline]');
779
- process.exit(1);
780
- }
781
- if (!file) {
782
- console.error('Error: File path required');
783
- process.exit(1);
784
- }
785
- if (!NODE_TYPES.includes(type)) {
786
- console.error(`Error: Invalid node type "${type}". Valid types: ${NODE_TYPES.join(', ')}`);
787
- process.exit(1);
788
- }
789
- // Bulk add only supports inline storage
790
- if (allUnmarked) {
791
- if (store === 'mod') {
792
- console.error('Error: --all-unmarked only supports --store=inline');
793
- process.exit(1);
794
- }
795
- await handleBulkAdd(file, type, json);
796
- return;
797
- }
798
- if (!line) {
799
- console.error('Error: Line number required (use file:line format)');
800
- process.exit(1);
801
- }
802
- const fullPath = path.resolve(process.cwd(), file);
803
- if (!fs.existsSync(fullPath)) {
804
- console.error(`Error: File not found: ${file}`);
805
- process.exit(1);
806
- }
807
- const content = fs.readFileSync(fullPath, 'utf-8');
808
- const lines = content.split('\n');
809
- if (line < 1 || line > lines.length) {
810
- console.error(`Error: Line ${line} out of range (file has ${lines.length} lines)`);
811
- process.exit(1);
812
- }
813
- // Get text at line for title generation
814
- const lineText = lines[line - 1].trim();
815
- const title = generateTitle(lineText);
816
- const id = generateTraceId(title);
817
- if (store === 'mod') {
818
- // Store in Mod file storage
819
- await handleAddTraceMod(repo, file, line, type, id, lineText, link || [], json);
820
- }
821
- else {
822
- // Store as inline annotation
823
- await handleAddTraceInline(file, line, type, id, link || [], json, lines);
824
- }
825
- }
826
- /**
827
- * Add trace to Mod file storage via FileHandle.traces API
828
- */
829
- async function handleAddTraceMod(repo, filePath, line, nodeType, id, text, links, json) {
830
- const { opened } = await requireModWorkspaceConnection(repo);
831
- // Find the file in workspace
832
- const files = await opened.file.list();
833
- const fileRef = files.find(f => f.name === filePath || f.name.endsWith(filePath) || filePath.endsWith(f.name));
834
- if (!fileRef) {
835
- console.error(`Error: File not found in workspace: ${filePath}`);
836
- console.error('Available files:');
837
- for (const f of files.slice(0, 10)) {
838
- console.error(` ${f.name}`);
839
- }
840
- process.exit(1);
841
- }
842
- const handle = await opened.file.getHandle(fileRef.id);
843
- if (!handle) {
844
- console.error(`Error: Could not get file handle for: ${filePath}`);
845
- process.exit(1);
846
- }
847
- // Create trace anchor with cursor-based positioning
848
- // For CLI, we use line-based placeholder cursors since we don't have document cursors
849
- const anchor = {
850
- fromCursor: `cli:${line}:0`,
851
- toCursor: `cli:${line}:${text.length}`,
852
- path: ['content'],
853
- quotedText: text.slice(0, 100),
854
- };
855
- // Create trace input with links
856
- const traceLinks = links.map(targetId => ({
857
- edgeType: getEdgeAttrName(nodeType),
858
- targetId,
859
- }));
860
- const traceId = await handle.traces.create(anchor, {
861
- nodeType,
862
- createdBy: 'cli',
863
- links: traceLinks,
864
- });
865
- // Flush to persist changes before CLI exits
866
- // Need to flush both the file document and the traces document
867
- const fileDoc = handle.doc?.() || {};
868
- const tracesDocId = fileDoc.tracesDocId;
869
- const docsToFlush = [fileRef.id];
870
- if (tracesDocId) {
871
- docsToFlush.push(tracesDocId);
872
- }
873
- await flushRepo(repo, docsToFlush);
874
- if (json) {
875
- console.log(JSON.stringify({
876
- created: { id: traceId, type: nodeType, file: filePath, line, links, store: 'mod' },
877
- }, null, 2));
878
- }
879
- else {
880
- const linkInfo = links.length > 0 ? `, linked to ${links.join(', ')}` : '';
881
- console.log(`Created trace (mod): ${traceId} at ${filePath}:${line}${linkInfo}`);
882
- }
883
- }
884
- /**
885
- * Flush repo to persist changes
886
- */
887
- async function flushRepo(repo, documentIds) {
888
- const flush = repo.flush;
889
- if (!flush)
890
- return;
891
- try {
892
- await flush.call(repo, documentIds);
893
- }
894
- catch (error) {
895
- console.error('Warning: Failed to flush changes');
896
- }
897
- }
898
- /**
899
- * Add trace as inline annotation in source file
900
- */
901
- async function handleAddTraceInline(file, line, type, id, links, json, lines) {
902
- // Build annotation
903
- const linkAttr = links.length > 0
904
- ? `, ${getEdgeAttrName(type)}="${links.join(',')}"`
905
- : '';
906
- const ext = path.extname(file);
907
- const fullPath = path.resolve(process.cwd(), file);
908
- if (ext === '.md') {
909
- // Note: Split strings to avoid glassware parsing this as an annotation
910
- const annotation = ` <glass` + `ware type="${type}" id="${id}"${linkAttr} />`;
911
- lines[line - 1] = lines[line - 1] + annotation;
912
- }
913
- else {
914
- const indent = lines[line - 1].match(/^(\s*)/)?.[1] || '';
915
- // Note: Split strings to avoid glassware parsing this as an annotation
916
- const annotation = `${indent}// glass` + `ware[type="${type}", id="${id}"${linkAttr}]`;
917
- lines.splice(line - 1, 0, annotation);
918
- }
919
- fs.writeFileSync(fullPath, lines.join('\n'));
920
- if (json) {
921
- console.log(JSON.stringify({
922
- created: { id, type, file, line, links, store: 'inline' },
923
- }, null, 2));
924
- }
925
- else {
926
- const linkInfo = links.length > 0 ? `, linked to ${links.join(', ')}` : '';
927
- console.log(`Created trace (inline): ${id} at ${file}:${line}${linkInfo}`);
928
- }
929
- }
930
- // glassware[type="implementation", id="impl-trace-link-handler--11d97052", specifications="specification-spec-traces-cli-link-validate--0f83e137,specification-spec-traces-cli-link-mod--e768fb7d,specification-spec-traces-cli-cross-link--60c5d89e,specification-spec-traces-cli-cross-resolve--7153750f"]
931
- async function handleLinkTraces(args, repo) {
932
- const { json, source, target } = parseLinkArgs(args);
933
- if (!source || !target) {
934
- console.error('Usage: mod trace link <source-id> <target-id>');
935
- process.exit(1);
936
- }
937
- // Try to find source trace in inline storage first, then Mod storage
938
- let sourceNode = runGlasswareShow(source);
939
- let sourceStorage = 'inline';
940
- let sourceModHandle = null;
941
- let sourceModTrace = null;
942
- let sourceModFile = '';
943
- if (!sourceNode && repo) {
944
- // Try Mod storage
945
- try {
946
- const { opened } = await requireModWorkspaceConnection(repo);
947
- const files = await opened.file.list();
948
- for (const fileRef of files) {
949
- const handle = await opened.file.getHandle(fileRef.id);
950
- if (!handle)
951
- continue;
952
- const traces = await handle.traces.list();
953
- const trace = traces.find(t => t.id === source);
954
- if (trace) {
955
- sourceModHandle = handle;
956
- sourceModTrace = trace;
957
- sourceModFile = fileRef.name;
958
- sourceStorage = 'mod';
959
- // Create a compatible node structure
960
- sourceNode = {
961
- id: trace.id,
962
- type: trace.nodeType,
963
- file: fileRef.name,
964
- line: getTraceLineNumber(trace),
965
- text: trace.text,
966
- attributes: {},
967
- };
968
- break;
969
- }
970
- }
971
- }
972
- catch {
973
- // Workspace not connected, continue with inline only
974
- }
975
- }
976
- if (!sourceNode) {
977
- console.error(`Error: Source trace not found: ${source}`);
978
- process.exit(1);
979
- }
980
- // Try to find target trace in inline storage first, then Mod storage
981
- let targetNode = runGlasswareShow(target);
982
- let targetStorage = 'inline';
983
- if (!targetNode && repo) {
984
- // Try Mod storage
985
- try {
986
- const { opened } = await requireModWorkspaceConnection(repo);
987
- const files = await opened.file.list();
988
- for (const fileRef of files) {
989
- const handle = await opened.file.getHandle(fileRef.id);
990
- if (!handle)
991
- continue;
992
- const traces = await handle.traces.list();
993
- const trace = traces.find(t => t.id === target);
994
- if (trace) {
995
- targetStorage = 'mod';
996
- targetNode = {
997
- id: trace.id,
998
- type: trace.nodeType,
999
- file: fileRef.name,
1000
- line: getTraceLineNumber(trace),
1001
- text: trace.text,
1002
- attributes: {},
1003
- };
1004
- break;
1005
- }
1006
- }
1007
- }
1008
- catch {
1009
- // Workspace not connected, continue with inline only
1010
- }
1011
- }
1012
- if (!targetNode) {
1013
- console.error(`Error: Target trace not found: ${target}`);
1014
- process.exit(1);
1015
- }
1016
- // Validate edge type constraints
1017
- const validEdges = {
1018
- specification: ['requirement'],
1019
- implementation: ['specification'],
1020
- test: ['implementation', 'specification'],
1021
- };
1022
- const allowedTargets = validEdges[sourceNode.type];
1023
- if (!allowedTargets || !allowedTargets.includes(targetNode.type)) {
1024
- console.error(`Error: Cannot link ${sourceNode.type} -> ${targetNode.type}`);
1025
- console.error(`Valid targets for ${sourceNode.type}: ${allowedTargets?.join(', ') || 'none'}`);
1026
- process.exit(1);
1027
- }
1028
- // Perform the link based on source storage type
1029
- try {
1030
- if (sourceStorage === 'mod' && sourceModHandle) {
1031
- // Link in Mod storage
1032
- const edgeType = getEdgeAttrName(sourceNode.type);
1033
- await sourceModHandle.traces.link(source, edgeType, target);
1034
- if (json) {
1035
- console.log(JSON.stringify({
1036
- linked: {
1037
- source,
1038
- target,
1039
- edgeType,
1040
- sourceStorage,
1041
- targetStorage,
1042
- },
1043
- }, null, 2));
1044
- }
1045
- else {
1046
- console.log(`Linked ${source} -> ${target} (${edgeType}) [${sourceStorage} -> ${targetStorage}]`);
1047
- }
1048
- }
1049
- else {
1050
- // Link in inline storage (modify source file)
1051
- await addLinkToSource(sourceNode, targetNode, json);
1052
- }
1053
- }
1054
- catch (error) {
1055
- console.error(`Error linking traces: ${error.message}`);
1056
- process.exit(1);
1057
- }
1058
- }
1059
- // =============================================================================
1060
- // Glassware Passthrough Commands
1061
- // =============================================================================
1062
- // glassware[type="implementation", id="impl-trace-query-passthrough--3c1755a4", specifications="specification-spec-traces-cli-query--6fa0b9f0"]
1063
- async function handleGlasswareQuery(args) {
1064
- // Pass through to glassware query
1065
- const output = runGlasswareDirect(['query', ...args]);
1066
- console.log(output);
1067
- }
1068
- // glassware[type="implementation", id="impl-trace-glassware-passthrough--1e79ba39", specifications="specification-spec-traces-cli-glassware--9c2ae5eb"]
1069
- async function handleGlasswareDirect(args) {
1070
- // Pass through to glassware directly
1071
- const output = runGlasswareDirect(args);
1072
- console.log(output);
1073
- }
1074
- // =============================================================================
1075
- // Bulk Add Implementation
1076
- // =============================================================================
1077
- // glassware[type="implementation", id="impl-trace-bulk-add--b80f6ac2", specifications="specification-spec-traces-cli-add-bulk--c9537f18"]
1078
- async function handleBulkAdd(file, type, json) {
1079
- const fullPath = path.resolve(process.cwd(), file);
1080
- if (!fs.existsSync(fullPath)) {
1081
- console.error(`Error: File not found: ${file}`);
1082
- process.exit(1);
1083
- }
1084
- const content = fs.readFileSync(fullPath, 'utf-8');
1085
- const lines = content.split('\n');
1086
- const ext = path.extname(file);
1087
- // Find candidate lines based on type and file extension
1088
- const candidates = findUnmarkedLines(lines, type, ext);
1089
- if (candidates.length === 0) {
1090
- if (json) {
1091
- console.log(JSON.stringify({ created: [], total: 0 }, null, 2));
1092
- }
1093
- else {
1094
- console.log(`No unmarked ${type} lines found in ${file}`);
1095
- }
1096
- return;
1097
- }
1098
- // Add annotations to candidates (work backwards to preserve line numbers)
1099
- const created = [];
1100
- for (let i = candidates.length - 1; i >= 0; i--) {
1101
- const { lineNum, text } = candidates[i];
1102
- const title = generateTitle(text);
1103
- const id = generateTraceId(title);
1104
- if (ext === '.md') {
1105
- // Append annotation to end of line
1106
- lines[lineNum - 1] = lines[lineNum - 1] + ` <glass` + `ware type="${type}" id="${id}" />`;
1107
- }
1108
- else {
1109
- // Insert annotation above the line
1110
- const indent = lines[lineNum - 1].match(/^(\s*)/)?.[1] || '';
1111
- const annotation = `${indent}// glass` + `ware[type="${type}", id="${id}"]`;
1112
- lines.splice(lineNum - 1, 0, annotation);
1113
- }
1114
- created.unshift({ id, type, file, line: lineNum });
1115
- }
1116
- fs.writeFileSync(fullPath, lines.join('\n'));
1117
- if (json) {
1118
- console.log(JSON.stringify({ created, total: created.length }, null, 2));
1119
- }
1120
- else {
1121
- console.log(`Created ${created.length} traces:`);
1122
- for (const trace of created) {
1123
- console.log(` ${trace.id} at ${trace.file}:${trace.line}`);
1124
- }
1125
- }
1126
- }
1127
- /**
1128
- * Find lines that match the type pattern and don't already have glassware annotations
1129
- */
1130
- function findUnmarkedLines(lines, type, ext) {
1131
- const candidates = [];
1132
- for (let i = 0; i < lines.length; i++) {
1133
- const line = lines[i];
1134
- const lineNum = i + 1;
1135
- // Skip if already has glassware annotation
1136
- if (line.includes('glassware') || line.includes('<glassware')) {
1137
- continue;
1138
- }
1139
- // Check if line matches pattern for the type
1140
- if (matchesTypePattern(line, type, ext, lines, i)) {
1141
- candidates.push({ lineNum, text: line.trim() });
1142
- }
1143
- }
1144
- return candidates;
1145
- }
1146
- /**
1147
- * Determine if a line matches the pattern for a given trace type
1148
- */
1149
- function matchesTypePattern(line, type, ext, lines, idx) {
1150
- const trimmed = line.trim();
1151
- // Skip empty lines, comments-only, and code blocks
1152
- if (!trimmed || trimmed.startsWith('```') || trimmed.startsWith('<!--')) {
1153
- return false;
1154
- }
1155
- if (ext === '.md') {
1156
- // Markdown patterns
1157
- switch (type) {
1158
- case 'requirement':
1159
- // Requirements often in lists with "must", "should", "shall"
1160
- return (/^[-*]\s+.*(must|should|shall|can|will)/i.test(trimmed) ||
1161
- /^[-*]\s+User\s+(can|must|should)/i.test(trimmed) ||
1162
- /^[-*]\s+The\s+system\s+(must|should|shall)/i.test(trimmed));
1163
- case 'specification':
1164
- // Specs start with description and often contain technical details
1165
- return (/^[-*]\s+.*(must|should|shall)/i.test(trimmed) &&
1166
- !trimmed.toLowerCase().includes('user'));
1167
- default:
1168
- return false;
1169
- }
1170
- }
1171
- else {
1172
- // Code file patterns
1173
- switch (type) {
1174
- case 'implementation':
1175
- // Functions, classes, exports
1176
- return (/^(export\s+)?(async\s+)?function\s+\w+/.test(trimmed) ||
1177
- /^(export\s+)?class\s+\w+/.test(trimmed) ||
1178
- /^(export\s+)?const\s+\w+\s*=\s*(async\s+)?\(/.test(trimmed) ||
1179
- /^(export\s+)?const\s+\w+\s*=\s*(async\s+)?function/.test(trimmed));
1180
- case 'test':
1181
- // Test functions
1182
- return (/^(it|test|describe)\s*\(/.test(trimmed) ||
1183
- /^(export\s+)?const\s+\w+Test\s*=/.test(trimmed));
1184
- default:
1185
- return false;
1186
- }
1187
- }
1188
- }
1189
- // =============================================================================
1190
- // Link Command Implementation
1191
- // =============================================================================
1192
- /**
1193
- * Add a link from source trace to target trace by modifying the source file
1194
- */
1195
- // glassware[type="implementation", id="impl-trace-link-cmd--c78fb60b", specifications="specification-spec-traces-cli-link--e92da014,specification-spec-traces-cli-link-update--0b94d120"]
1196
- async function addLinkToSource(sourceNode, targetNode, json) {
1197
- const fullPath = path.resolve(process.cwd(), sourceNode.file);
1198
- if (!fs.existsSync(fullPath)) {
1199
- throw new Error(`Source file not found: ${sourceNode.file}`);
1200
- }
1201
- const content = fs.readFileSync(fullPath, 'utf-8');
1202
- const lines = content.split('\n');
1203
- const ext = path.extname(sourceNode.file);
1204
- // Determine the edge type based on source node type
1205
- const edgeAttr = getEdgeAttrName(sourceNode.type);
1206
- // Find the annotation line
1207
- let annotationLine = -1;
1208
- let originalAnnotation = '';
1209
- for (let i = Math.max(0, sourceNode.line - 3); i < Math.min(lines.length, sourceNode.line + 1); i++) {
1210
- const line = lines[i];
1211
- if (line.includes(sourceNode.id)) {
1212
- annotationLine = i;
1213
- originalAnnotation = line;
1214
- break;
1215
- }
1216
- }
1217
- if (annotationLine === -1) {
1218
- throw new Error(`Could not find annotation for ${sourceNode.id} near line ${sourceNode.line}`);
1219
- }
1220
- // Parse and update the annotation
1221
- let updatedLine;
1222
- if (ext === '.md') {
1223
- // Markdown format: annotation tag with type, id, and attributes
1224
- if (originalAnnotation.includes(`${edgeAttr}="`)) {
1225
- // Add to existing attribute
1226
- updatedLine = originalAnnotation.replace(new RegExp(`(${edgeAttr}=")([^"]*)("`), `$1$2,${targetNode.id}$3`);
1227
- }
1228
- else {
1229
- // Add new attribute before />
1230
- updatedLine = originalAnnotation.replace(/\s*\/>/, ` ${edgeAttr}="${targetNode.id}" />`);
1231
- }
1232
- }
1233
- else {
1234
- // Code format: annotation with type, id, and attributes
1235
- if (originalAnnotation.includes(`${edgeAttr}="`)) {
1236
- // Add to existing attribute
1237
- updatedLine = originalAnnotation.replace(new RegExp(`(${edgeAttr}=")([^"]*)("`), `$1$2,${targetNode.id}$3`);
1238
- }
1239
- else {
1240
- // Add new attribute before ]
1241
- updatedLine = originalAnnotation.replace(/\]$/, `, ${edgeAttr}="${targetNode.id}"]`);
1242
- }
1243
- }
1244
- lines[annotationLine] = updatedLine;
1245
- fs.writeFileSync(fullPath, lines.join('\n'));
1246
- // Determine edge type name for output
1247
- const edgeTypeName = getEdgeTypeName(sourceNode.type, targetNode.type);
1248
- if (json) {
1249
- console.log(JSON.stringify({
1250
- linked: {
1251
- source: sourceNode.id,
1252
- target: targetNode.id,
1253
- edgeType: edgeTypeName,
1254
- file: sourceNode.file,
1255
- line: annotationLine + 1,
1256
- },
1257
- }, null, 2));
1258
- }
1259
- else {
1260
- console.log(`Linked ${sourceNode.id} -> ${targetNode.id} (${edgeTypeName})`);
1261
- }
1262
- }
1263
- /**
1264
- * Get the edge type name based on source and target node types
1265
- */
1266
- function getEdgeTypeName(sourceType, targetType) {
1267
- if (sourceType === 'specification' && targetType === 'requirement')
1268
- return 'specifies';
1269
- if (sourceType === 'implementation' && targetType === 'specification')
1270
- return 'implements';
1271
- if (sourceType === 'test' && targetType === 'implementation')
1272
- return 'tests';
1273
- if (sourceType === 'test' && targetType === 'specification')
1274
- return 'tests';
1275
- return 'links';
1276
- }
1277
- function parseGetArgs(args) {
1278
- const result = { json: false };
1279
- const nonFlags = args.filter(a => !a.startsWith('-'));
1280
- if (nonFlags.length >= 1)
1281
- result.id = nonFlags[0];
1282
- result.json = args.includes('--json');
1283
- return result;
1284
- }
1285
- function parseDeleteArgs(args) {
1286
- const result = { json: false, force: false };
1287
- const nonFlags = args.filter(a => !a.startsWith('-'));
1288
- if (nonFlags.length >= 1)
1289
- result.id = nonFlags[0];
1290
- result.json = args.includes('--json');
1291
- result.force = args.includes('--force') || args.includes('-f');
1292
- return result;
1293
- }
1294
- function parseUnlinkArgs(args) {
1295
- const result = { json: false };
1296
- for (let i = 0; i < args.length; i++) {
1297
- const arg = args[i];
1298
- if (arg === '--json') {
1299
- result.json = true;
1300
- }
1301
- else if (arg === '--edge-type' && args[i + 1]) {
1302
- result.edgeType = args[i + 1];
1303
- i++;
1304
- }
1305
- else if (arg.startsWith('--edge-type=')) {
1306
- result.edgeType = arg.slice('--edge-type='.length);
1307
- }
1308
- else if (!arg.startsWith('-')) {
1309
- if (!result.source) {
1310
- result.source = arg;
1311
- }
1312
- else if (!result.target) {
1313
- result.target = arg;
1314
- }
1315
- }
1316
- }
1317
- return result;
1318
- }
1319
- /**
1320
- * Get trace details from Mod storage
1321
- */
1322
- // glassware[type="implementation", id="impl-trace-get--ca299267", specifications="specification-spec-traces-cli-get--55d212ec,specification-spec-traces-cli-get-details--a42429ba,specification-spec-traces-cli-get-json--b662053f"]
1323
- async function handleGetTrace(args, repo) {
1324
- const { json, id } = parseGetArgs(args);
1325
- if (!id) {
1326
- console.error('Usage: mod trace get <trace-id> [--json]');
1327
- process.exit(1);
1328
- }
1329
- const { opened } = await requireModWorkspaceConnection(repo);
1330
- const files = await opened.file.list();
1331
- // Search for trace across all files
1332
- for (const fileRef of files) {
1333
- try {
1334
- const handle = await opened.file.getHandle(fileRef.id);
1335
- if (!handle)
1336
- continue;
1337
- const traces = await handle.traces.list();
1338
- const trace = traces.find(t => t.id === id);
1339
- if (trace) {
1340
- const lineNum = getTraceLineNumber(trace);
1341
- const locationInfo = trace.location?.type === 'inline'
1342
- ? `line ${lineNum}`
1343
- : trace.location?.quotedText
1344
- ? `"${trace.location.quotedText.slice(0, 30)}..."`
1345
- : 'cursor-based';
1346
- if (json) {
1347
- console.log(JSON.stringify({
1348
- trace: {
1349
- id: trace.id,
1350
- nodeType: trace.nodeType,
1351
- file: fileRef.name,
1352
- line: lineNum,
1353
- text: trace.text,
1354
- links: trace.links,
1355
- location: trace.location,
1356
- },
1357
- }, null, 2));
1358
- }
1359
- else {
1360
- console.log(`Trace: ${trace.id}`);
1361
- console.log('═'.repeat(55));
1362
- console.log(`Type: ${trace.nodeType}`);
1363
- console.log(`File: ${fileRef.name}`);
1364
- console.log(`Location: ${locationInfo}`);
1365
- console.log(`Text: ${trace.text}`);
1366
- if (trace.links.length > 0) {
1367
- console.log(`Links:`);
1368
- for (const link of trace.links) {
1369
- console.log(` → ${link.targetId} (${link.edgeType})`);
1370
- }
1371
- }
1372
- }
1373
- return;
1374
- }
1375
- }
1376
- catch {
1377
- // Skip files that fail to load
1378
- }
1379
- }
1380
- console.error(`Trace not found: ${id}`);
1381
- process.exit(1);
1382
- }
1383
- /**
1384
- * Delete a trace from Mod storage
1385
- */
1386
- // glassware[type="implementation", id="impl-trace-delete--20f630b7", specifications="specification-spec-traces-cli-delete--f27d3f33,specification-spec-traces-cli-delete-confirm--a8fbd99d"]
1387
- async function handleDeleteTrace(args, repo) {
1388
- const { json, id, force } = parseDeleteArgs(args);
1389
- if (!id) {
1390
- console.error('Usage: mod trace delete <trace-id> [--force] [--json]');
1391
- process.exit(1);
1392
- }
1393
- const { opened } = await requireModWorkspaceConnection(repo);
1394
- const files = await opened.file.list();
1395
- // Search for trace across all files
1396
- for (const fileRef of files) {
1397
- try {
1398
- const handle = await opened.file.getHandle(fileRef.id);
1399
- if (!handle)
1400
- continue;
1401
- const traces = await handle.traces.list();
1402
- const trace = traces.find(t => t.id === id);
1403
- if (trace) {
1404
- // Require confirmation unless --force is provided
1405
- if (!force && !json) {
1406
- const confirmed = await confirm(`Delete trace "${id}" from ${fileRef.name}?`);
1407
- if (!confirmed) {
1408
- console.log('Deletion cancelled.');
1409
- process.exit(0);
1410
- }
1411
- }
1412
- await handle.traces.delete(id);
1413
- if (json) {
1414
- console.log(JSON.stringify({
1415
- deleted: { id, file: fileRef.name },
1416
- }, null, 2));
1417
- }
1418
- else {
1419
- console.log(`Deleted trace: ${id} from ${fileRef.name}`);
1420
- }
1421
- return;
1422
- }
1423
- }
1424
- catch {
1425
- // Skip files that fail to load
1426
- }
1427
- }
1428
- console.error(`Trace not found: ${id}`);
1429
- process.exit(1);
1430
- }
1431
- /**
1432
- * Remove a link between traces in Mod storage
1433
- */
1434
- // glassware[type="implementation", id="impl-trace-unlink--09745920", specifications="specification-spec-traces-cli-unlink--ff5cb1d8"]
1435
- async function handleUnlinkTrace(args, repo) {
1436
- const { json, source, target, edgeType } = parseUnlinkArgs(args);
1437
- if (!source || !target) {
1438
- console.error('Usage: mod trace unlink <source-id> <target-id> [--edge-type=<type>] [--json]');
1439
- process.exit(1);
1440
- }
1441
- const { opened } = await requireModWorkspaceConnection(repo);
1442
- const files = await opened.file.list();
1443
- // Find source trace
1444
- for (const fileRef of files) {
1445
- try {
1446
- const handle = await opened.file.getHandle(fileRef.id);
1447
- if (!handle)
1448
- continue;
1449
- const traces = await handle.traces.list();
1450
- const trace = traces.find(t => t.id === source);
1451
- if (trace) {
1452
- // Find matching link
1453
- const linkIndex = trace.links.findIndex(l => l.targetId === target && (!edgeType || l.edgeType === edgeType));
1454
- if (linkIndex === -1) {
1455
- console.error(`Link not found: ${source} -> ${target}`);
1456
- process.exit(1);
1457
- }
1458
- const removedLink = trace.links[linkIndex];
1459
- await handle.traces.unlink(source, removedLink.edgeType, target);
1460
- if (json) {
1461
- console.log(JSON.stringify({
1462
- unlinked: {
1463
- source,
1464
- target,
1465
- edgeType: removedLink.edgeType,
1466
- file: fileRef.name,
1467
- },
1468
- }, null, 2));
1469
- }
1470
- else {
1471
- console.log(`Unlinked: ${source} -> ${target} (${removedLink.edgeType})`);
1472
- }
1473
- return;
1474
- }
1475
- }
1476
- catch {
1477
- // Skip files that fail to load
1478
- }
1479
- }
1480
- console.error(`Source trace not found: ${source}`);
1481
- process.exit(1);
1482
- }
1483
- // =============================================================================
1484
- // Utility Functions
1485
- // =============================================================================
1486
- function generateTitle(lineText) {
1487
- let text = lineText
1488
- .replace(/^#+\s*/, '')
1489
- .replace(/^[-*]\s*/, '')
1490
- .replace(/\*\*|__/g, '')
1491
- .replace(/\*|_/g, '')
1492
- .replace(/`/g, '')
1493
- .replace(/<[^>]+>/g, '')
1494
- .replace(/\/\/.*$/, '')
1495
- .trim();
1496
- if (text.length > 40) {
1497
- text = text.slice(0, 37) + '...';
1498
- }
1499
- return text || 'trace';
1500
- }
1501
- function generateTraceId(title) {
1502
- const slug = title
1503
- .toLowerCase()
1504
- .replace(/[^a-z0-9]+/g, '-')
1505
- .replace(/^-+|-+$/g, '')
1506
- .slice(0, 30);
1507
- return `${slug}--wip`;
1508
- }
1509
- function getEdgeAttrName(nodeType) {
1510
- switch (nodeType) {
1511
- case 'specification': return 'requirements';
1512
- case 'implementation': return 'specifications';
1513
- case 'test': return 'specifications';
1514
- default: return 'links';
1515
- }
1516
- }
1517
- // =============================================================================
1518
- // Help
1519
- // =============================================================================
1520
- function showHelp() {
1521
- console.log(`
1522
- mod trace - Manage requirement traces (powered by glassware)
1523
-
1524
- Usage:
1525
- mod trace list [--source=all|mod|inline] [--type=<type>] [--file=<path>] [--json]
1526
- mod trace report <file> [--json]
1527
- mod trace coverage [--json]
1528
- mod trace unmet [--json]
1529
- mod trace diff [<base>..<head>] [--json]
1530
- mod trace add <file>:<line> --type=<type> [--store=mod|inline] [--link=<id>] [--json]
1531
- mod trace get <trace-id> [--json]
1532
- mod trace delete <trace-id> [--json]
1533
- mod trace link <source-id> <target-id> [--json]
1534
- mod trace unlink <source-id> <target-id> [--edge-type=<type>] [--json]
1535
- mod trace query <name> [--format=json]
1536
- mod trace glassware <args>
1537
-
1538
- Commands:
1539
- list List all traces with optional filters (default: --source=all)
1540
- report Show trace report for a specific file
1541
- coverage Show workspace-wide trace coverage
1542
- unmet List requirements without implementations (exits non-zero if gaps)
1543
- diff Find untraced files on branch (exits non-zero if untraced)
1544
- add Add a trace (default: --store=mod for Mod file storage)
1545
- get Get trace details from Mod storage
1546
- delete Delete a trace from Mod storage
1547
- link Create a link between two traces (inline annotations)
1548
- unlink Remove a link between traces (Mod storage)
1549
- query Pass through to glassware query
1550
- glassware Pass through to glassware CLI
1551
-
1552
- Storage Options:
1553
- --source For list: where to read traces from (all|mod|inline, default: all)
1554
- --store For add: where to store trace (mod|inline, default: mod)
1555
- - mod: Store in Mod file storage (FileHandle.traces API)
1556
- - inline: Store as glassware annotation in source file
1557
-
1558
- Options:
1559
- --json Output in JSON format for agent consumption
1560
- --type Filter by node type (requirement, specification, implementation, test)
1561
- --file Filter by file path
1562
- --link Link new trace to existing trace (for add command)
1563
- --edge-type Specify edge type for unlink command
1564
-
1565
- Diff Command:
1566
- mod trace diff # Auto-detect: compare current branch to main
1567
- mod trace diff main..HEAD # Explicit range
1568
- mod trace diff main..feat/auth # Compare branches
1569
- mod trace diff --json # JSON output
1570
-
1571
- Examples:
1572
- mod trace list # List from all sources
1573
- mod trace list --source=mod # List only from Mod storage
1574
- mod trace list --source=inline --type=spec # List inline specs only
1575
- mod trace add spec.md:10 --type=spec # Add to Mod storage (default)
1576
- mod trace add src/foo.ts:20 --type=impl --store=inline # Add inline
1577
- mod trace get my-trace-id--wip # Get trace details
1578
- mod trace delete my-trace-id--wip # Delete from Mod storage
1579
- mod trace unlink src-id tgt-id # Remove link
1580
- mod trace report specs/auth.md
1581
- mod trace coverage --json
1582
-
1583
- Pre-merge validation:
1584
- mod trace diff && mod trace unmet && git push
1585
- `);
1586
- }
1587
- // =============================================================================
1588
- // Main Command
1589
- // =============================================================================
1590
- export async function traceCommand(args, repo) {
1591
- const [subcommand, ...rest] = args;
1592
- try {
1593
- switch (subcommand) {
1594
- case 'list':
1595
- await handleListTraces(rest, repo);
1596
- break;
1597
- case 'report':
1598
- await handleTraceReport(rest);
1599
- break;
1600
- case 'coverage':
1601
- await handleCoverageAnalysis(rest);
1602
- break;
1603
- case 'unmet':
1604
- await handleUnmetRequirements(rest);
1605
- break;
1606
- case 'diff':
1607
- await handleTraceDiff(rest);
1608
- break;
1609
- case 'add':
1610
- await handleAddTrace(rest, repo);
1611
- break;
1612
- case 'get':
1613
- await handleGetTrace(rest, repo);
1614
- break;
1615
- case 'delete':
1616
- await handleDeleteTrace(rest, repo);
1617
- break;
1618
- case 'link':
1619
- await handleLinkTraces(rest, repo);
1620
- break;
1621
- case 'unlink':
1622
- await handleUnlinkTrace(rest, repo);
1623
- break;
1624
- case 'query':
1625
- await handleGlasswareQuery(rest);
1626
- break;
1627
- case 'glassware':
1628
- await handleGlasswareDirect(rest);
1629
- break;
1630
- case 'help':
1631
- case '--help':
1632
- case '-h':
1633
- showHelp();
1634
- break;
1635
- default:
1636
- if (!subcommand) {
1637
- showHelp();
1638
- }
1639
- else {
1640
- console.error(`Unknown subcommand: ${subcommand}`);
1641
- console.error('Run "mod trace help" for usage');
1642
- process.exit(1);
1643
- }
1644
- }
1645
- }
1646
- catch (error) {
1647
- console.error('Error:', error.message);
1648
- process.exit(1);
1649
- }
1650
- process.exit(0);
1651
- }
1652
- // =============================================================================
1653
- // COMMENTED OUT: Original mod-core TraceService approach
1654
- // This works but duplicates glassware's file parsing. Kept for reference.
1655
- // =============================================================================
1656
- /*
1657
- import {
1658
- TraceService,
1659
- createTraceService,
1660
- InlineAdapter,
1661
- createInlineAdapter,
1662
- DefaultAdapterRegistry,
1663
- } from '@mod/mod-core';
1664
- import type {
1665
- Trace,
1666
- TraceSchema,
1667
- TraceFilter,
1668
- TraceReport,
1669
- CoverageReport,
1670
- InlineAdapterConfig,
1671
- } from '@mod/mod-core';
1672
- import {
1673
- formatTracesTable,
1674
- formatTracesJson,
1675
- formatTraceReport,
1676
- formatTraceReportJson,
1677
- formatCoverageReport,
1678
- formatCoverageJson,
1679
- formatUnmetRequirements,
1680
- formatUnmetJson,
1681
- } from '../lib/trace-formatters.js';
1682
-
1683
- const DEFAULT_SCHEMA: TraceSchema = {
1684
- nodeTypes: ['requirement', 'specification', 'implementation', 'test'],
1685
- edgeTypes: {
1686
- specifies: { attribute: 'specifies', from: 'specification', to: 'requirement' },
1687
- implements: { attribute: 'requirements', from: 'implementation', to: 'specification' },
1688
- tests: { attribute: 'tests', from: 'test', to: 'implementation' },
1689
- },
1690
- };
1691
-
1692
- function createTraceServiceWithAdapter(): TraceService {
1693
- const workspaceRoot = process.cwd();
1694
- const registry = new DefaultAdapterRegistry();
1695
-
1696
- const config: InlineAdapterConfig = {
1697
- paths: ['**\/*.ts', '**\/*.js', '**\/*.md', '**\/*.tsx', '**\/*.jsx'],
1698
- rootDir: workspaceRoot,
1699
- watchEnabled: false,
1700
- };
1701
-
1702
- const adapter = createInlineAdapter(config, DEFAULT_SCHEMA);
1703
- registry.register(adapter);
1704
-
1705
- scanFilesForTraces(adapter, workspaceRoot);
1706
-
1707
- return createTraceService(workspaceRoot, registry);
1708
- }
1709
-
1710
- function scanFilesForTraces(adapter: InlineAdapter, rootDir: string): void {
1711
- const patterns = ['**\/*.ts', '**\/*.js', '**\/*.md', '**\/*.tsx', '**\/*.jsx'];
1712
- const ignorePatterns = ['node_modules', 'dist', '.git', 'coverage', '.next', 'build'];
1713
-
1714
- function walkDir(dir: string): void {
1715
- try {
1716
- const entries = fs.readdirSync(dir, { withFileTypes: true });
1717
-
1718
- for (const entry of entries) {
1719
- const fullPath = path.join(dir, entry.name);
1720
- const relativePath = path.relative(rootDir, fullPath);
1721
-
1722
- if (ignorePatterns.some(p => relativePath.includes(p))) {
1723
- continue;
1724
- }
1725
-
1726
- if (entry.isDirectory()) {
1727
- walkDir(fullPath);
1728
- } else if (entry.isFile()) {
1729
- const ext = path.extname(entry.name);
1730
- if (['.ts', '.js', '.md', '.tsx', '.jsx'].includes(ext)) {
1731
- try {
1732
- const content = fs.readFileSync(fullPath, 'utf-8');
1733
- adapter.parseFileContent(relativePath, content);
1734
- } catch {
1735
- // Skip files that can't be read
1736
- }
1737
- }
1738
- }
1739
- }
1740
- } catch {
1741
- // Skip directories that can't be read
1742
- }
1743
- }
1744
-
1745
- walkDir(rootDir);
1746
- }
1747
-
1748
- // Original handlers used formatters from trace-formatters.ts
1749
- // and the TraceService from mod-core. See trace-formatters.ts
1750
- // for the formatting logic which is still valid if you want
1751
- // to use the mod-core approach.
1752
- */