@git-stunts/git-warp 10.4.2 → 10.7.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.
@@ -17,7 +17,26 @@ import { formatOpSummary } from './opSummary.js';
17
17
 
18
18
  /**
19
19
  * @typedef {{ ticks: number[], tipSha?: string, tickShas?: Record<number, string> }} WriterInfo
20
- * @typedef {{ graph: string, tick: number, maxTick: number, ticks: number[], nodes: number, edges: number, patchCount: number, perWriter: Map<string, WriterInfo> | Record<string, WriterInfo>, diff?: { nodes?: number, edges?: number }, tickReceipt?: Record<string, any> }} SeekPayload
20
+ */
21
+
22
+ /**
23
+ * @typedef {Object} SeekPayload
24
+ * @property {string} graph
25
+ * @property {number} tick
26
+ * @property {number} maxTick
27
+ * @property {number[]} ticks
28
+ * @property {number} nodes
29
+ * @property {number} edges
30
+ * @property {number} patchCount
31
+ * @property {Map<string, WriterInfo> | Record<string, WriterInfo>} perWriter
32
+ * @property {{ nodes?: number, edges?: number }} [diff]
33
+ * @property {Record<string, any>} [tickReceipt]
34
+ * @property {import('../../../domain/services/StateDiff.js').StateDiffResult | null} [structuralDiff]
35
+ * @property {string} [diffBaseline]
36
+ * @property {number | null} [baselineTick]
37
+ * @property {boolean} [truncated]
38
+ * @property {number} [totalChanges]
39
+ * @property {number} [shownChanges]
21
40
  */
22
41
 
23
42
  /** Maximum number of tick columns shown in the windowed view. */
@@ -265,14 +284,45 @@ function buildTickPoints(ticks, tick) {
265
284
  return { allPoints, currentIdx };
266
285
  }
267
286
 
287
+ // ============================================================================
288
+ // Structural Diff
289
+ // ============================================================================
290
+
291
+ /** Maximum structural diff lines shown in ASCII view. */
292
+ const MAX_DIFF_LINES = 20;
293
+
268
294
  /**
269
- * Builds the body lines for the seek dashboard.
270
- *
271
- * @param {SeekPayload} payload - Seek payload from the CLI handler
272
- * @returns {string[]} Lines for the box body
295
+ * Builds the state summary, receipt, and structural diff footer lines.
296
+ * @param {SeekPayload} payload
297
+ * @returns {string[]}
273
298
  */
299
+ function buildFooterLines(payload) {
300
+ const { tick, nodes, edges, patchCount, diff, tickReceipt } = payload;
301
+ const lines = [];
302
+ lines.push('');
303
+ const nodesStr = `${nodes} ${pluralize(nodes, 'node', 'nodes')}${formatDelta(diff?.nodes ?? 0)}`;
304
+ const edgesStr = `${edges} ${pluralize(edges, 'edge', 'edges')}${formatDelta(diff?.edges ?? 0)}`;
305
+ lines.push(` ${colors.bold('State:')} ${nodesStr}, ${edgesStr}, ${patchCount} ${pluralize(patchCount, 'patch', 'patches')}`);
306
+
307
+ const receiptLines = buildReceiptLines(tickReceipt);
308
+ if (receiptLines.length > 0) {
309
+ lines.push('');
310
+ lines.push(` ${colors.bold(`Tick ${tick}:`)}`);
311
+ lines.push(...receiptLines);
312
+ }
313
+
314
+ const sdLines = buildStructuralDiffLines(payload, MAX_DIFF_LINES);
315
+ if (sdLines.length > 0) {
316
+ lines.push('');
317
+ lines.push(...sdLines);
318
+ }
319
+ lines.push('');
320
+ return lines;
321
+ }
322
+
323
+ /** @param {SeekPayload} payload @returns {string[]} */
274
324
  function buildSeekBodyLines(payload) {
275
- const { graph, tick, maxTick, ticks, nodes, edges, patchCount, perWriter, diff, tickReceipt } = payload;
325
+ const { graph, tick, maxTick, ticks, perWriter } = payload;
276
326
  const lines = [];
277
327
 
278
328
  lines.push('');
@@ -285,11 +335,8 @@ function buildSeekBodyLines(payload) {
285
335
  } else {
286
336
  const { allPoints, currentIdx } = buildTickPoints(ticks, tick);
287
337
  const win = computeWindow(allPoints, currentIdx);
288
-
289
- // Column headers with relative offsets
290
338
  lines.push(buildHeaderRow(win));
291
339
 
292
- // Per-writer swimlanes
293
340
  /** @type {Array<[string, WriterInfo]>} */
294
341
  const writerEntries = perWriter instanceof Map
295
342
  ? [...perWriter.entries()]
@@ -300,30 +347,133 @@ function buildSeekBodyLines(payload) {
300
347
  }
301
348
  }
302
349
 
303
- lines.push('');
304
- const edgeLabel = pluralize(edges, 'edge', 'edges');
305
- const nodeLabel = pluralize(nodes, 'node', 'nodes');
306
- const patchLabel = pluralize(patchCount, 'patch', 'patches');
350
+ lines.push(...buildFooterLines(payload));
351
+ return lines;
352
+ }
307
353
 
308
- const nodesStr = `${nodes} ${nodeLabel}${formatDelta(diff?.nodes ?? 0)}`;
309
- const edgesStr = `${edges} ${edgeLabel}${formatDelta(diff?.edges ?? 0)}`;
310
- lines.push(` ${colors.bold('State:')} ${nodesStr}, ${edgesStr}, ${patchCount} ${patchLabel}`);
354
+ /**
355
+ * Builds a truncation hint line when entries exceed the display or data limit.
356
+ * @param {{totalEntries: number, shown: number, maxLines: number, truncated?: boolean, totalChanges?: number, shownChanges?: number}} opts
357
+ * @returns {string|null}
358
+ */
359
+ function buildTruncationHint(opts) {
360
+ const { totalEntries, shown, maxLines, truncated, totalChanges, shownChanges } = opts;
361
+ if (totalEntries > maxLines && truncated) {
362
+ const remaining = Math.max(0, (totalChanges || 0) - shown);
363
+ return `... and ${remaining} more changes (${totalChanges} total, use --diff-limit to increase)`;
364
+ }
365
+ if (totalEntries > maxLines) {
366
+ return `... and ${Math.max(0, totalEntries - maxLines)} more changes`;
367
+ }
368
+ if (truncated) {
369
+ return `... and ${Math.max(0, (totalChanges || 0) - (shownChanges || 0))} more changes (use --diff-limit to increase)`;
370
+ }
371
+ return null;
372
+ }
311
373
 
312
- const receiptLines = buildReceiptLines(tickReceipt);
313
- if (receiptLines.length > 0) {
314
- lines.push('');
315
- lines.push(` ${colors.bold(`Tick ${tick}:`)}`);
316
- lines.push(...receiptLines);
374
+ /**
375
+ * @param {SeekPayload} payload
376
+ * @param {number} maxLines
377
+ * @returns {string[]}
378
+ */
379
+ function buildStructuralDiffLines(payload, maxLines) {
380
+ const { structuralDiff, diffBaseline, baselineTick, truncated, totalChanges, shownChanges } = payload;
381
+ if (!structuralDiff) {
382
+ return [];
383
+ }
384
+
385
+ const lines = [];
386
+ const baselineLabel = diffBaseline === 'tick'
387
+ ? `baseline: tick ${baselineTick}`
388
+ : 'baseline: empty';
389
+
390
+ lines.push(` ${colors.bold(`Changes (${baselineLabel}):`)}`);
391
+
392
+ let shown = 0;
393
+ const entries = collectDiffEntries(structuralDiff);
394
+
395
+ for (const entry of entries) {
396
+ if (shown >= maxLines) {
397
+ break;
398
+ }
399
+ lines.push(` ${entry}`);
400
+ shown++;
401
+ }
402
+
403
+ const hint = buildTruncationHint({ totalEntries: entries.length, shown, maxLines, truncated, totalChanges, shownChanges });
404
+ if (hint) {
405
+ lines.push(` ${colors.muted(hint)}`);
317
406
  }
318
- lines.push('');
319
407
 
320
408
  return lines;
321
409
  }
322
410
 
411
+ /**
412
+ * Collects formatted diff entries from a structural diff result.
413
+ *
414
+ * @param {import('../../../domain/services/StateDiff.js').StateDiffResult} diff
415
+ * @returns {string[]} Formatted entries with +/-/~ prefixes
416
+ */
417
+ function collectDiffEntries(diff) {
418
+ const entries = [];
419
+
420
+ for (const nodeId of diff.nodes.added) {
421
+ entries.push(colors.success(`+ node ${nodeId}`));
422
+ }
423
+ for (const nodeId of diff.nodes.removed) {
424
+ entries.push(colors.error(`- node ${nodeId}`));
425
+ }
426
+ for (const edge of diff.edges.added) {
427
+ entries.push(colors.success(`+ edge ${edge.from} -[${edge.label}]-> ${edge.to}`));
428
+ }
429
+ for (const edge of diff.edges.removed) {
430
+ entries.push(colors.error(`- edge ${edge.from} -[${edge.label}]-> ${edge.to}`));
431
+ }
432
+ for (const prop of diff.props.set) {
433
+ const old = prop.oldValue !== undefined ? formatPropValue(prop.oldValue) : null;
434
+ const arrow = old !== null ? `${old} -> ${formatPropValue(prop.newValue)}` : formatPropValue(prop.newValue);
435
+ entries.push(colors.warning(`~ ${prop.nodeId}.${prop.propKey}: ${arrow}`));
436
+ }
437
+ for (const prop of diff.props.removed) {
438
+ entries.push(colors.error(`- ${prop.nodeId}.${prop.propKey}: ${formatPropValue(prop.oldValue)}`));
439
+ }
440
+
441
+ return entries;
442
+ }
443
+
444
+ /**
445
+ * Formats a property value for display (truncated if too long).
446
+ * @param {*} value
447
+ * @returns {string}
448
+ */
449
+ function formatPropValue(value) {
450
+ if (value === undefined) {
451
+ return 'undefined';
452
+ }
453
+ const s = typeof value === 'string' ? `"${value}"` : String(value);
454
+ return s.length > 40 ? `${s.slice(0, 37)}...` : s;
455
+ }
456
+
323
457
  // ============================================================================
324
458
  // Public API
325
459
  // ============================================================================
326
460
 
461
+ /**
462
+ * Formats a structural diff as a plain-text string (no boxen).
463
+ *
464
+ * Used by the non-view renderSeek() path in the CLI.
465
+ *
466
+ * @param {SeekPayload} payload - Seek payload containing structuralDiff
467
+ * @returns {string} Formatted diff section, or empty string if no diff
468
+ */
469
+ export function formatStructuralDiff(payload) {
470
+ const lines = buildStructuralDiffLines(payload, MAX_DIFF_LINES);
471
+ if (lines.length === 0) {
472
+ return '';
473
+ }
474
+ return `${lines.join('\n')}\n`;
475
+ }
476
+
327
477
  /**
328
478
  * Renders the seek view dashboard inside a double-bordered box.
329
479
  *