logicstamp-context 0.7.1 → 0.8.0

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 (81) hide show
  1. package/README.md +10 -1
  2. package/dist/cli/commands/clean.d.ts +2 -1
  3. package/dist/cli/commands/clean.d.ts.map +1 -1
  4. package/dist/cli/commands/clean.js +30 -2
  5. package/dist/cli/commands/clean.js.map +1 -1
  6. package/dist/cli/commands/compare/cleanup.d.ts +8 -0
  7. package/dist/cli/commands/compare/cleanup.d.ts.map +1 -0
  8. package/dist/cli/commands/compare/cleanup.js +27 -0
  9. package/dist/cli/commands/compare/cleanup.js.map +1 -0
  10. package/dist/cli/commands/compare/core.d.ts +29 -0
  11. package/dist/cli/commands/compare/core.d.ts.map +1 -0
  12. package/dist/cli/commands/compare/core.js +249 -0
  13. package/dist/cli/commands/compare/core.js.map +1 -0
  14. package/dist/cli/commands/compare/display.d.ts +9 -0
  15. package/dist/cli/commands/compare/display.d.ts.map +1 -0
  16. package/dist/cli/commands/compare/display.js +251 -0
  17. package/dist/cli/commands/compare/display.js.map +1 -0
  18. package/dist/cli/commands/compare/index.d.ts +26 -0
  19. package/dist/cli/commands/compare/index.d.ts.map +1 -0
  20. package/dist/cli/commands/compare/index.js +33 -0
  21. package/dist/cli/commands/compare/index.js.map +1 -0
  22. package/dist/cli/commands/compare/multiFile.d.ts +16 -0
  23. package/dist/cli/commands/compare/multiFile.d.ts.map +1 -0
  24. package/dist/cli/commands/compare/multiFile.js +113 -0
  25. package/dist/cli/commands/compare/multiFile.js.map +1 -0
  26. package/dist/cli/commands/compare/singleFile.d.ts +20 -0
  27. package/dist/cli/commands/compare/singleFile.d.ts.map +1 -0
  28. package/dist/cli/commands/compare/singleFile.js +341 -0
  29. package/dist/cli/commands/compare/singleFile.js.map +1 -0
  30. package/dist/cli/commands/{compare.d.ts → compare/types.d.ts} +21 -28
  31. package/dist/cli/commands/compare/types.d.ts.map +1 -0
  32. package/dist/cli/commands/compare/types.js +5 -0
  33. package/dist/cli/commands/compare/types.js.map +1 -0
  34. package/dist/cli/commands/compare/utils.d.ts +20 -0
  35. package/dist/cli/commands/compare/utils.d.ts.map +1 -0
  36. package/dist/cli/commands/compare/utils.js +94 -0
  37. package/dist/cli/commands/compare/utils.js.map +1 -0
  38. package/dist/cli/commands/context/watchMode/watchMode.d.ts.map +1 -1
  39. package/dist/cli/commands/context/watchMode/watchMode.js +2 -107
  40. package/dist/cli/commands/context/watchMode/watchMode.js.map +1 -1
  41. package/dist/cli/commands/context.d.ts +1 -0
  42. package/dist/cli/commands/context.d.ts.map +1 -1
  43. package/dist/cli/commands/context.js +3 -1
  44. package/dist/cli/commands/context.js.map +1 -1
  45. package/dist/cli/handlers/compareHandler.d.ts.map +1 -1
  46. package/dist/cli/handlers/compareHandler.js +297 -9
  47. package/dist/cli/handlers/compareHandler.js.map +1 -1
  48. package/dist/cli/index.js +1 -1
  49. package/dist/cli/index.js.map +1 -1
  50. package/dist/cli/parser/argumentParser.d.ts +2 -0
  51. package/dist/cli/parser/argumentParser.d.ts.map +1 -1
  52. package/dist/cli/parser/argumentParser.js +20 -2
  53. package/dist/cli/parser/argumentParser.js.map +1 -1
  54. package/dist/cli/parser/helpText.d.ts.map +1 -1
  55. package/dist/cli/parser/helpText.js +29 -0
  56. package/dist/cli/parser/helpText.js.map +1 -1
  57. package/dist/core/violations.d.ts +36 -0
  58. package/dist/core/violations.d.ts.map +1 -0
  59. package/dist/core/violations.js +292 -0
  60. package/dist/core/violations.js.map +1 -0
  61. package/dist/extractors/shared/propTypeNormalizer.d.ts.map +1 -1
  62. package/dist/extractors/shared/propTypeNormalizer.js +2 -1
  63. package/dist/extractors/shared/propTypeNormalizer.js.map +1 -1
  64. package/dist/index.d.ts +2 -2
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +1 -1
  67. package/dist/index.js.map +1 -1
  68. package/dist/utils/fileLock.d.ts.map +1 -1
  69. package/dist/utils/fileLock.js +79 -5
  70. package/dist/utils/fileLock.js.map +1 -1
  71. package/dist/utils/git.d.ts +135 -0
  72. package/dist/utils/git.d.ts.map +1 -0
  73. package/dist/utils/git.js +429 -0
  74. package/dist/utils/git.js.map +1 -0
  75. package/dist/utils/hash.d.ts.map +1 -1
  76. package/dist/utils/hash.js +20 -4
  77. package/dist/utils/hash.js.map +1 -1
  78. package/package.json +2 -1
  79. package/dist/cli/commands/compare.d.ts.map +0 -1
  80. package/dist/cli/commands/compare.js +0 -648
  81. package/dist/cli/commands/compare.js.map +0 -1
@@ -1,648 +0,0 @@
1
- /**
2
- * Compare command - Diffs two context.json files
3
- * Detects added/removed components and changed signatures
4
- */
5
- import { readFile, unlink } from 'node:fs/promises';
6
- import { join, dirname } from 'node:path';
7
- import { estimateGPT4Tokens, estimateClaudeTokens, formatTokenCount } from '../../utils/tokens.js';
8
- import { debugError } from '../../utils/debug.js';
9
- /**
10
- * Index bundles into a map of entryId -> LiteSig
11
- */
12
- function index(bundles) {
13
- const m = new Map();
14
- for (const b of bundles) {
15
- for (const n of b.graph.nodes) {
16
- const c = n.contract;
17
- m.set(c.entryId.toLowerCase(), {
18
- semanticHash: c.semanticHash,
19
- imports: c.composition?.imports ?? [],
20
- hooks: c.composition?.hooks ?? [],
21
- functions: c.composition?.functions ?? [],
22
- components: c.composition?.components ?? [],
23
- props: Object.keys(c.interface?.props ?? {}),
24
- emits: Object.keys(c.interface?.emits ?? {}),
25
- exportKind: typeof c.exports === 'string' ? 'default'
26
- : c.exports?.named?.length ? 'named' : 'none',
27
- });
28
- }
29
- }
30
- return m;
31
- }
32
- /**
33
- * Diff two indexed bundles with detailed change information
34
- */
35
- function diff(oldIdx, newIdx) {
36
- const added = [];
37
- const removed = [];
38
- const changed = [];
39
- // Find added components
40
- for (const id of newIdx.keys()) {
41
- if (!oldIdx.has(id)) {
42
- added.push(id);
43
- }
44
- }
45
- // Find removed components
46
- for (const id of oldIdx.keys()) {
47
- if (!newIdx.has(id)) {
48
- removed.push(id);
49
- }
50
- }
51
- // Find changed components with detailed deltas
52
- for (const id of newIdx.keys()) {
53
- if (oldIdx.has(id)) {
54
- const a = oldIdx.get(id);
55
- const b = newIdx.get(id);
56
- const deltas = [];
57
- if (a.semanticHash !== b.semanticHash) {
58
- deltas.push({ type: 'hash', old: a.semanticHash, new: b.semanticHash });
59
- }
60
- if (JSON.stringify(a.imports) !== JSON.stringify(b.imports)) {
61
- deltas.push({ type: 'imports', old: a.imports, new: b.imports });
62
- }
63
- if (JSON.stringify(a.hooks) !== JSON.stringify(b.hooks)) {
64
- deltas.push({ type: 'hooks', old: a.hooks, new: b.hooks });
65
- }
66
- if (JSON.stringify(a.functions) !== JSON.stringify(b.functions)) {
67
- deltas.push({ type: 'functions', old: a.functions, new: b.functions });
68
- }
69
- if (JSON.stringify(a.components) !== JSON.stringify(b.components)) {
70
- deltas.push({ type: 'components', old: a.components, new: b.components });
71
- }
72
- if (JSON.stringify(a.props) !== JSON.stringify(b.props)) {
73
- deltas.push({ type: 'props', old: a.props, new: b.props });
74
- }
75
- if (JSON.stringify(a.emits) !== JSON.stringify(b.emits)) {
76
- deltas.push({ type: 'emits', old: a.emits, new: b.emits });
77
- }
78
- if (a.exportKind !== b.exportKind) {
79
- deltas.push({ type: 'exports', old: a.exportKind, new: b.exportKind });
80
- }
81
- if (deltas.length > 0) {
82
- changed.push({ id, deltas });
83
- }
84
- }
85
- }
86
- const status = added.length === 0 && removed.length === 0 && changed.length === 0
87
- ? 'PASS'
88
- : 'DRIFT';
89
- return { status, added, removed, changed };
90
- }
91
- /**
92
- * Calculate token count for bundles
93
- */
94
- async function calculateTokens(bundles) {
95
- const text = JSON.stringify(bundles);
96
- return {
97
- gpt4: await estimateGPT4Tokens(text),
98
- claude: await estimateClaudeTokens(text),
99
- };
100
- }
101
- /**
102
- * Main compare command
103
- * Returns the comparison result instead of exiting, allowing caller to handle approval logic
104
- */
105
- export async function compareCommand(options) {
106
- // Load both files
107
- let oldContent;
108
- let newContent;
109
- try {
110
- oldContent = await readFile(options.oldFile, 'utf8');
111
- }
112
- catch (error) {
113
- const err = error;
114
- debugError('compare', 'compareCommand', {
115
- file: options.oldFile,
116
- message: err.message,
117
- code: err.code,
118
- });
119
- throw new Error(`Failed to read old file "${options.oldFile}": ${err.code === 'ENOENT' ? 'File not found' : err.message}`);
120
- }
121
- try {
122
- newContent = await readFile(options.newFile, 'utf8');
123
- }
124
- catch (error) {
125
- const err = error;
126
- debugError('compare', 'compareCommand', {
127
- file: options.newFile,
128
- message: err.message,
129
- code: err.code,
130
- });
131
- throw new Error(`Failed to read new file "${options.newFile}": ${err.code === 'ENOENT' ? 'File not found' : err.message}`);
132
- }
133
- let oldBundles;
134
- let newBundles;
135
- try {
136
- oldBundles = JSON.parse(oldContent);
137
- newBundles = JSON.parse(newContent);
138
- }
139
- catch (error) {
140
- const err = error;
141
- debugError('compare', 'compareCommand', {
142
- oldFile: options.oldFile,
143
- newFile: options.newFile,
144
- message: err.message,
145
- });
146
- throw new Error(`Failed to parse context files: ${err.message}`);
147
- }
148
- // Index bundles
149
- const oldIdx = index(oldBundles);
150
- const newIdx = index(newBundles);
151
- // Compute diff
152
- const result = diff(oldIdx, newIdx);
153
- // Output result (skip PASS status in quiet mode)
154
- if (options.quiet && result.status === 'PASS') {
155
- // Minimal output in quiet mode for PASS
156
- process.stdout.write('✓\n');
157
- }
158
- else if (!options.quiet || result.status === 'DRIFT') {
159
- console.log(`\n${result.status === 'PASS' ? '✅' : '⚠️'} ${result.status}\n`);
160
- }
161
- if (result.status === 'DRIFT') {
162
- if (result.added.length > 0) {
163
- if (!options.quiet) {
164
- console.log(`Added components: ${result.added.length}`);
165
- }
166
- result.added.forEach(id => console.log(` + ${id}`));
167
- if (!options.quiet) {
168
- console.log();
169
- }
170
- }
171
- if (result.removed.length > 0) {
172
- if (!options.quiet) {
173
- console.log(`Removed components: ${result.removed.length}`);
174
- }
175
- result.removed.forEach(id => console.log(` - ${id}`));
176
- if (!options.quiet) {
177
- console.log();
178
- }
179
- }
180
- if (result.changed.length > 0) {
181
- if (!options.quiet) {
182
- console.log(`Changed components: ${result.changed.length}`);
183
- }
184
- result.changed.forEach(({ id, deltas }) => {
185
- console.log(` ~ ${id}`);
186
- deltas.forEach(delta => {
187
- console.log(` Δ ${delta.type}`);
188
- if (delta.type === 'hash') {
189
- console.log(` old: ${delta.old}`);
190
- console.log(` new: ${delta.new}`);
191
- }
192
- else if (delta.type === 'imports' || delta.type === 'hooks' || delta.type === 'functions' ||
193
- delta.type === 'components' || delta.type === 'props' || delta.type === 'emits') {
194
- const oldSet = new Set(delta.old);
195
- const newSet = new Set(delta.new);
196
- // Find removed items
197
- const removed = delta.old.filter((item) => !newSet.has(item));
198
- // Find added items
199
- const added = delta.new.filter((item) => !oldSet.has(item));
200
- if (removed.length > 0) {
201
- removed.forEach((item) => console.log(` - ${item}`));
202
- }
203
- if (added.length > 0) {
204
- added.forEach((item) => console.log(` + ${item}`));
205
- }
206
- if (removed.length === 0 && added.length === 0) {
207
- // Order changed but items are the same
208
- console.log(` (order changed)`);
209
- }
210
- }
211
- else if (delta.type === 'exports') {
212
- console.log(` ${delta.old} → ${delta.new}`);
213
- }
214
- });
215
- });
216
- if (!options.quiet) {
217
- console.log();
218
- }
219
- }
220
- }
221
- // Show token stats if requested (skip in quiet mode)
222
- if (options.stats && !options.quiet) {
223
- const oldTokens = await calculateTokens(oldBundles);
224
- const newTokens = await calculateTokens(newBundles);
225
- const deltaStat = newTokens.gpt4 - oldTokens.gpt4;
226
- let deltaPercentStr = '0.00';
227
- let deltaPercentNum = 0;
228
- if (oldTokens.gpt4 > 0) {
229
- deltaPercentNum = (deltaStat / oldTokens.gpt4) * 100;
230
- deltaPercentStr = deltaPercentNum.toFixed(2);
231
- }
232
- const sign = deltaStat > 0 ? '+' : '';
233
- const percentSign = deltaPercentNum > 0 ? '+' : deltaPercentNum < 0 ? '' : '';
234
- console.log('Token Stats:');
235
- console.log(` ⚠️ Current mode = tokenizer-based.`);
236
- console.log(` Other modes / raw source = heuristic.`);
237
- console.log(` For precise per-mode breakdown, use "stamp context --compare-modes".`);
238
- console.log(` Old: ${formatTokenCount(oldTokens.gpt4)} (GPT-4o-mini) | ${formatTokenCount(oldTokens.claude)} (Claude)`);
239
- console.log(` New: ${formatTokenCount(newTokens.gpt4)} (GPT-4o-mini) | ${formatTokenCount(newTokens.claude)} (Claude)`);
240
- console.log(` Δ ${sign}${formatTokenCount(deltaStat)} (${percentSign}${deltaPercentStr}%)\n`);
241
- }
242
- return result;
243
- }
244
- /**
245
- * Load LogicStampIndex from file
246
- */
247
- async function loadIndex(indexPath) {
248
- let content;
249
- try {
250
- content = await readFile(indexPath, 'utf8');
251
- }
252
- catch (error) {
253
- const err = error;
254
- debugError('compare', 'loadIndex', {
255
- indexPath,
256
- message: err.message,
257
- code: err.code,
258
- });
259
- throw new Error(`Failed to load index from ${indexPath}: ${err.code === 'ENOENT' ? 'File not found' : err.message}`);
260
- }
261
- try {
262
- const index = JSON.parse(content);
263
- if (index.type !== 'LogicStampIndex') {
264
- throw new Error(`Invalid index file: expected type 'LogicStampIndex', got '${index.type}'`);
265
- }
266
- // Backward compatibility: warn about old schema version
267
- if (index.schemaVersion === '0.1') {
268
- console.warn(`⚠️ Warning: context_main.json uses schema version 0.1 (legacy format).`);
269
- console.warn(``);
270
- console.warn(` Consider recompiling with "stamp context" to upgrade to version 0.2 (relative paths).`);
271
- console.warn(``);
272
- console.warn(` Optional cleanup: "stamp context clean --all --yes".`);
273
- console.warn(``);
274
- console.warn(` See docs/MIGRATION_0.3.2.md for details.\n`);
275
- }
276
- else if (index.schemaVersion !== '0.2') {
277
- console.warn(`⚠️ Warning: Unknown schema version "${index.schemaVersion}". Expected '0.1' or '0.2'.`);
278
- }
279
- return index;
280
- }
281
- catch (error) {
282
- const err = error;
283
- debugError('compare', 'loadIndex', {
284
- indexPath,
285
- message: err.message,
286
- });
287
- throw new Error(`Failed to load index from ${indexPath}: ${err.message}`);
288
- }
289
- }
290
- /**
291
- * Discover orphaned context files on disk that are not in the new index
292
- */
293
- async function findOrphanedFiles(oldIndex, newIndex, baseDir) {
294
- const orphaned = [];
295
- const newContextFiles = new Set(newIndex.folders.map(f => f.contextFile));
296
- // Check each old folder's context file
297
- for (const folder of oldIndex.folders) {
298
- if (!newContextFiles.has(folder.contextFile)) {
299
- // Check if file still exists on disk
300
- const contextPath = join(baseDir, folder.contextFile);
301
- try {
302
- await readFile(contextPath, 'utf8');
303
- orphaned.push(folder.contextFile);
304
- }
305
- catch (error) {
306
- // File doesn't exist, not orphaned (already deleted)
307
- const err = error;
308
- if (err.code !== 'ENOENT') {
309
- debugError('compare', 'findOrphanedFiles', {
310
- contextPath,
311
- message: err.message,
312
- code: err.code,
313
- });
314
- }
315
- }
316
- }
317
- }
318
- return orphaned;
319
- }
320
- /**
321
- * Compare a single folder's context file
322
- */
323
- async function compareFolderContext(oldContextPath, newContextPath, stats, quiet) {
324
- // Load both files
325
- let oldContent;
326
- let newContent;
327
- try {
328
- oldContent = await readFile(oldContextPath, 'utf8');
329
- }
330
- catch (error) {
331
- const err = error;
332
- debugError('compare', 'compareFolderContext', {
333
- oldContextPath,
334
- message: err.message,
335
- code: err.code,
336
- });
337
- throw new Error(`Failed to read old context file "${oldContextPath}": ${err.code === 'ENOENT' ? 'File not found' : err.message}`);
338
- }
339
- try {
340
- newContent = await readFile(newContextPath, 'utf8');
341
- }
342
- catch (error) {
343
- const err = error;
344
- debugError('compare', 'compareFolderContext', {
345
- newContextPath,
346
- message: err.message,
347
- code: err.code,
348
- });
349
- throw new Error(`Failed to read new context file "${newContextPath}": ${err.code === 'ENOENT' ? 'File not found' : err.message}`);
350
- }
351
- let oldBundles;
352
- let newBundles;
353
- try {
354
- oldBundles = JSON.parse(oldContent);
355
- newBundles = JSON.parse(newContent);
356
- }
357
- catch (error) {
358
- const err = error;
359
- debugError('compare', 'compareFolderContext', {
360
- oldContextPath,
361
- newContextPath,
362
- message: err.message,
363
- });
364
- throw new Error(`Failed to parse context files for folder: ${err.message}`);
365
- }
366
- // Index bundles
367
- const oldIdx = index(oldBundles);
368
- const newIdx = index(newBundles);
369
- // Compute diff
370
- const result = diff(oldIdx, newIdx);
371
- // Calculate token delta if stats requested and not in quiet mode
372
- let tokenDelta;
373
- if (stats && !quiet) {
374
- const oldTokens = await calculateTokens(oldBundles);
375
- const newTokens = await calculateTokens(newBundles);
376
- tokenDelta = {
377
- gpt4: newTokens.gpt4 - oldTokens.gpt4,
378
- claude: newTokens.claude - oldTokens.claude,
379
- };
380
- }
381
- return { result, tokenDelta };
382
- }
383
- /**
384
- * Multi-file comparison - compares all context files using context_main.json indices
385
- * This is the comprehensive comparison that handles:
386
- * 1. context_main.json as root index
387
- * 2. All folder context.json files
388
- * 3. ADDED FILE detection (new folders)
389
- * 4. ORPHANED FILE detection (deleted folders)
390
- * 5. DRIFT detection (changed files)
391
- * 6. PASS detection (unchanged files)
392
- */
393
- export async function multiFileCompare(options) {
394
- const oldBaseDir = dirname(options.oldIndexFile);
395
- const newBaseDir = dirname(options.newIndexFile);
396
- // Load both index files
397
- const oldIndex = await loadIndex(options.oldIndexFile);
398
- const newIndex = await loadIndex(options.newIndexFile);
399
- // Create maps for quick lookup
400
- const oldFolderMap = new Map(oldIndex.folders.map(f => [f.contextFile, f]));
401
- const newFolderMap = new Map(newIndex.folders.map(f => [f.contextFile, f]));
402
- const folderResults = [];
403
- let totalComponentsAdded = 0;
404
- let totalComponentsRemoved = 0;
405
- let totalComponentsChanged = 0;
406
- // Compare folders that exist in both old and new
407
- const allContextFiles = new Set([
408
- ...oldIndex.folders.map(f => f.contextFile),
409
- ...newIndex.folders.map(f => f.contextFile),
410
- ]);
411
- for (const contextFile of allContextFiles) {
412
- const oldFolder = oldFolderMap.get(contextFile);
413
- const newFolder = newFolderMap.get(contextFile);
414
- if (oldFolder && newFolder) {
415
- // Folder exists in both - compare context files
416
- const oldPath = join(oldBaseDir, oldFolder.contextFile);
417
- const newPath = join(newBaseDir, newFolder.contextFile);
418
- try {
419
- const { result, tokenDelta } = await compareFolderContext(oldPath, newPath, options.stats || false, options.quiet);
420
- folderResults.push({
421
- folderPath: newFolder.path,
422
- contextFile: newFolder.contextFile,
423
- status: result.status,
424
- componentResult: result,
425
- tokenDelta,
426
- });
427
- if (result.status === 'DRIFT') {
428
- totalComponentsAdded += result.added.length;
429
- totalComponentsRemoved += result.removed.length;
430
- totalComponentsChanged += result.changed.length;
431
- }
432
- }
433
- catch (error) {
434
- // If comparison fails, treat as drift
435
- console.error(`⚠️ Failed to compare ${contextFile}: ${error.message}`);
436
- folderResults.push({
437
- folderPath: newFolder.path,
438
- contextFile: newFolder.contextFile,
439
- status: 'DRIFT',
440
- });
441
- }
442
- }
443
- else if (!oldFolder && newFolder) {
444
- // New folder - ADDED FILE
445
- folderResults.push({
446
- folderPath: newFolder.path,
447
- contextFile: newFolder.contextFile,
448
- status: 'ADDED',
449
- });
450
- totalComponentsAdded += newFolder.bundles;
451
- }
452
- else if (oldFolder && !newFolder) {
453
- // Removed folder - ORPHANED FILE
454
- folderResults.push({
455
- folderPath: oldFolder.path,
456
- contextFile: oldFolder.contextFile,
457
- status: 'ORPHANED',
458
- });
459
- totalComponentsRemoved += oldFolder.bundles;
460
- }
461
- }
462
- // Find orphaned files on disk
463
- const orphanedFiles = await findOrphanedFiles(oldIndex, newIndex, oldBaseDir);
464
- // Calculate summary
465
- const addedFolders = folderResults.filter(f => f.status === 'ADDED').length;
466
- const orphanedFolders = folderResults.filter(f => f.status === 'ORPHANED').length;
467
- const driftFolders = folderResults.filter(f => f.status === 'DRIFT').length;
468
- const passFolders = folderResults.filter(f => f.status === 'PASS').length;
469
- const status = addedFolders > 0 || orphanedFolders > 0 || driftFolders > 0 ? 'DRIFT' : 'PASS';
470
- // Sort folder results by path for consistent output
471
- folderResults.sort((a, b) => a.folderPath.localeCompare(b.folderPath));
472
- return {
473
- status,
474
- folders: folderResults,
475
- summary: {
476
- totalFolders: folderResults.length,
477
- addedFolders,
478
- orphanedFolders,
479
- driftFolders,
480
- passFolders,
481
- totalComponentsAdded,
482
- totalComponentsRemoved,
483
- totalComponentsChanged,
484
- },
485
- orphanedFiles: orphanedFiles.length > 0 ? orphanedFiles : undefined,
486
- };
487
- }
488
- /**
489
- * Format and display multi-file comparison results
490
- */
491
- export function displayMultiFileCompareResult(result, stats, quiet) {
492
- // Skip status header in quiet mode unless there's drift
493
- if (quiet && result.status === 'PASS') {
494
- // Minimal output in quiet mode for PASS
495
- process.stdout.write('✓\n');
496
- }
497
- else if (!quiet || result.status === 'DRIFT') {
498
- console.log(`\n${result.status === 'PASS' ? '✅' : '⚠️'} ${result.status}\n`);
499
- }
500
- // Skip summaries in quiet mode
501
- if (!quiet) {
502
- // Display folder-level summary
503
- console.log('📁 Folder Summary:');
504
- console.log(` Total folders: ${result.summary.totalFolders}`);
505
- if (result.summary.addedFolders > 0) {
506
- console.log(` ➕ Added folders: ${result.summary.addedFolders}`);
507
- }
508
- if (result.summary.orphanedFolders > 0) {
509
- console.log(` 🗑️ Orphaned folders: ${result.summary.orphanedFolders}`);
510
- }
511
- if (result.summary.driftFolders > 0) {
512
- console.log(` ~ Changed folders: ${result.summary.driftFolders}`);
513
- }
514
- if (result.summary.passFolders > 0) {
515
- console.log(` ✓ Unchanged folders: ${result.summary.passFolders}`);
516
- }
517
- console.log();
518
- // Display component-level summary
519
- if (result.status === 'DRIFT') {
520
- console.log('📦 Component Summary:');
521
- if (result.summary.totalComponentsAdded > 0) {
522
- console.log(` + Added: ${result.summary.totalComponentsAdded}`);
523
- }
524
- if (result.summary.totalComponentsRemoved > 0) {
525
- console.log(` - Removed: ${result.summary.totalComponentsRemoved}`);
526
- }
527
- if (result.summary.totalComponentsChanged > 0) {
528
- console.log(` ~ Changed: ${result.summary.totalComponentsChanged}`);
529
- }
530
- console.log();
531
- }
532
- // Display detailed folder results
533
- console.log('📂 Folder Details:');
534
- if (stats) {
535
- console.log(' ⚠️ Current mode = tokenizer-based.');
536
- console.log(' Other modes / raw source = heuristic.');
537
- console.log(' For precise per-mode breakdown, use "stamp context --compare-modes".');
538
- }
539
- console.log();
540
- }
541
- for (const folder of result.folders) {
542
- if (folder.status === 'ADDED') {
543
- console.log(` ➕ ADDED FILE: ${folder.contextFile}`);
544
- console.log(` Path: ${folder.folderPath}`);
545
- console.log();
546
- }
547
- else if (folder.status === 'ORPHANED') {
548
- console.log(` 🗑️ ORPHANED FILE: ${folder.contextFile}`);
549
- console.log(` Path: ${folder.folderPath}`);
550
- console.log();
551
- }
552
- else if (folder.status === 'DRIFT') {
553
- console.log(` ⚠️ DRIFT: ${folder.contextFile}`);
554
- console.log(` Path: ${folder.folderPath}`);
555
- if (folder.componentResult) {
556
- const cr = folder.componentResult;
557
- if (cr.added.length > 0) {
558
- console.log(` + Added components (${cr.added.length}):`);
559
- cr.added.forEach(id => console.log(` + ${id}`));
560
- }
561
- if (cr.removed.length > 0) {
562
- console.log(` - Removed components (${cr.removed.length}):`);
563
- cr.removed.forEach(id => console.log(` - ${id}`));
564
- }
565
- if (cr.changed.length > 0) {
566
- console.log(` ~ Changed components (${cr.changed.length}):`);
567
- cr.changed.forEach(({ id, deltas }) => {
568
- console.log(` ~ ${id}`);
569
- deltas.forEach(delta => {
570
- console.log(` Δ ${delta.type}`);
571
- if (delta.type === 'hash') {
572
- console.log(` old: ${delta.old}`);
573
- console.log(` new: ${delta.new}`);
574
- }
575
- else if (delta.type === 'imports' || delta.type === 'hooks' || delta.type === 'functions' ||
576
- delta.type === 'components' || delta.type === 'props' || delta.type === 'emits') {
577
- const oldSet = new Set(delta.old);
578
- const newSet = new Set(delta.new);
579
- const removed = delta.old.filter((item) => !newSet.has(item));
580
- const added = delta.new.filter((item) => !oldSet.has(item));
581
- if (removed.length > 0) {
582
- removed.forEach((item) => console.log(` - ${item}`));
583
- }
584
- if (added.length > 0) {
585
- added.forEach((item) => console.log(` + ${item}`));
586
- }
587
- if (removed.length === 0 && added.length === 0) {
588
- console.log(` (order changed)`);
589
- }
590
- }
591
- else if (delta.type === 'exports') {
592
- console.log(` ${delta.old} → ${delta.new}`);
593
- }
594
- });
595
- });
596
- }
597
- }
598
- if (stats && !quiet && folder.tokenDelta) {
599
- const sign = folder.tokenDelta.gpt4 > 0 ? '+' : '';
600
- console.log(` Token Δ: ${sign}${formatTokenCount(folder.tokenDelta.gpt4)} (GPT-4) | ${sign}${formatTokenCount(folder.tokenDelta.claude)} (Claude)`);
601
- }
602
- console.log();
603
- }
604
- else if (folder.status === 'PASS') {
605
- // Skip PASS folders in quiet mode
606
- if (!quiet) {
607
- console.log(` ✅ PASS: ${folder.contextFile}`);
608
- console.log(` Path: ${folder.folderPath}`);
609
- console.log();
610
- }
611
- }
612
- }
613
- // Display orphaned files on disk (only if not in quiet mode, or show as diff)
614
- if (result.orphanedFiles && result.orphanedFiles.length > 0) {
615
- if (!quiet) {
616
- console.log('🗑️ Orphaned Files on Disk:');
617
- console.log(' (These files exist on disk but are not in the new index)\n');
618
- }
619
- result.orphanedFiles.forEach(file => {
620
- console.log(` 🗑️ ${file}`);
621
- });
622
- if (!quiet) {
623
- console.log();
624
- }
625
- }
626
- }
627
- /**
628
- * Clean up orphaned files
629
- */
630
- export async function cleanOrphanedFiles(orphanedFiles, baseDir, quiet) {
631
- let deletedCount = 0;
632
- for (const file of orphanedFiles) {
633
- const filePath = join(baseDir, file);
634
- try {
635
- await unlink(filePath);
636
- if (!quiet) {
637
- console.log(` 🗑️ Deleted: ${file}`);
638
- }
639
- deletedCount++;
640
- }
641
- catch (error) {
642
- // Always show errors even in quiet mode
643
- console.error(` ⚠️ Failed to delete ${file}: ${error.message}`);
644
- }
645
- }
646
- return deletedCount;
647
- }
648
- //# sourceMappingURL=compare.js.map