@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.
- package/package.json +3 -3
- package/dist/app.js +0 -227
- package/dist/cli.bundle.js.map +0 -7
- package/dist/cli.js +0 -132
- package/dist/commands/add.js +0 -245
- package/dist/commands/agents-run.js +0 -71
- package/dist/commands/auth.js +0 -259
- package/dist/commands/branch.js +0 -1411
- package/dist/commands/claude-sync.js +0 -772
- package/dist/commands/comment.js +0 -568
- package/dist/commands/diff.js +0 -182
- package/dist/commands/index.js +0 -73
- package/dist/commands/init.js +0 -597
- package/dist/commands/ls.js +0 -135
- package/dist/commands/members.js +0 -687
- package/dist/commands/mv.js +0 -282
- package/dist/commands/recover.js +0 -207
- package/dist/commands/rm.js +0 -257
- package/dist/commands/spec.js +0 -386
- package/dist/commands/status.js +0 -296
- package/dist/commands/sync.js +0 -119
- package/dist/commands/trace.js +0 -1752
- package/dist/commands/workspace.js +0 -447
- package/dist/components/conflict-resolution-ui.js +0 -120
- package/dist/components/messages.js +0 -5
- package/dist/components/thread.js +0 -8
- package/dist/config/features.js +0 -83
- package/dist/containers/branches-container.js +0 -140
- package/dist/containers/directory-container.js +0 -92
- package/dist/containers/thread-container.js +0 -214
- package/dist/containers/threads-container.js +0 -27
- package/dist/containers/workspaces-container.js +0 -27
- package/dist/daemon/conflict-resolution.js +0 -172
- package/dist/daemon/content-hash.js +0 -31
- package/dist/daemon/file-sync.js +0 -985
- package/dist/daemon/index.js +0 -203
- package/dist/daemon/mime-types.js +0 -166
- package/dist/daemon/offline-queue.js +0 -211
- package/dist/daemon/path-utils.js +0 -64
- package/dist/daemon/share-policy.js +0 -83
- package/dist/daemon/wasm-errors.js +0 -189
- package/dist/daemon/worker.js +0 -557
- package/dist/daemon-worker.js +0 -258
- package/dist/errors/workspace-errors.js +0 -48
- package/dist/lib/auth-server.js +0 -216
- package/dist/lib/browser.js +0 -35
- package/dist/lib/diff.js +0 -284
- package/dist/lib/formatters.js +0 -204
- package/dist/lib/git.js +0 -137
- package/dist/lib/local-fs.js +0 -201
- package/dist/lib/prompts.js +0 -56
- package/dist/lib/storage.js +0 -213
- package/dist/lib/trace-formatters.js +0 -314
- package/dist/services/add-service.js +0 -554
- package/dist/services/add-validation.js +0 -124
- package/dist/services/automatic-file-tracker.js +0 -303
- package/dist/services/cli-orchestrator.js +0 -227
- package/dist/services/feature-flags.js +0 -187
- package/dist/services/file-import-service.js +0 -283
- package/dist/services/file-transformation-service.js +0 -218
- package/dist/services/logger.js +0 -44
- package/dist/services/mod-config.js +0 -67
- package/dist/services/modignore-service.js +0 -328
- package/dist/services/sync-daemon.js +0 -244
- package/dist/services/thread-notification-service.js +0 -50
- package/dist/services/thread-service.js +0 -147
- package/dist/stores/use-directory-store.js +0 -96
- package/dist/stores/use-threads-store.js +0 -46
- package/dist/stores/use-workspaces-store.js +0 -54
- package/dist/types/add-types.js +0 -99
- package/dist/types/config.js +0 -16
- package/dist/types/index.js +0 -2
- package/dist/types/workspace-connection.js +0 -53
- package/dist/types.js +0 -1
package/dist/commands/trace.js
DELETED
|
@@ -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
|
-
*/
|