@git-stunts/git-warp 10.1.2 → 10.4.2

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 (106) hide show
  1. package/README.md +31 -4
  2. package/bin/warp-graph.js +1242 -59
  3. package/index.d.ts +31 -0
  4. package/index.js +4 -0
  5. package/package.json +13 -3
  6. package/src/domain/WarpGraph.js +487 -140
  7. package/src/domain/crdt/LWW.js +1 -1
  8. package/src/domain/crdt/ORSet.js +10 -6
  9. package/src/domain/crdt/VersionVector.js +5 -1
  10. package/src/domain/errors/EmptyMessageError.js +2 -4
  11. package/src/domain/errors/ForkError.js +4 -0
  12. package/src/domain/errors/IndexError.js +4 -0
  13. package/src/domain/errors/OperationAbortedError.js +4 -0
  14. package/src/domain/errors/QueryError.js +4 -0
  15. package/src/domain/errors/SchemaUnsupportedError.js +4 -0
  16. package/src/domain/errors/ShardCorruptionError.js +2 -6
  17. package/src/domain/errors/ShardLoadError.js +2 -6
  18. package/src/domain/errors/ShardValidationError.js +2 -7
  19. package/src/domain/errors/StorageError.js +2 -6
  20. package/src/domain/errors/SyncError.js +4 -0
  21. package/src/domain/errors/TraversalError.js +4 -0
  22. package/src/domain/errors/WarpError.js +2 -4
  23. package/src/domain/errors/WormholeError.js +4 -0
  24. package/src/domain/services/AnchorMessageCodec.js +1 -4
  25. package/src/domain/services/BitmapIndexBuilder.js +10 -6
  26. package/src/domain/services/BitmapIndexReader.js +27 -21
  27. package/src/domain/services/BoundaryTransitionRecord.js +22 -15
  28. package/src/domain/services/CheckpointMessageCodec.js +1 -7
  29. package/src/domain/services/CheckpointSerializerV5.js +20 -19
  30. package/src/domain/services/CheckpointService.js +18 -18
  31. package/src/domain/services/CommitDagTraversalService.js +13 -1
  32. package/src/domain/services/DagPathFinding.js +40 -18
  33. package/src/domain/services/DagTopology.js +7 -6
  34. package/src/domain/services/DagTraversal.js +5 -3
  35. package/src/domain/services/Frontier.js +7 -6
  36. package/src/domain/services/HealthCheckService.js +15 -14
  37. package/src/domain/services/HookInstaller.js +64 -13
  38. package/src/domain/services/HttpSyncServer.js +15 -14
  39. package/src/domain/services/IndexRebuildService.js +12 -12
  40. package/src/domain/services/IndexStalenessChecker.js +13 -6
  41. package/src/domain/services/JoinReducer.js +28 -27
  42. package/src/domain/services/LogicalTraversal.js +7 -6
  43. package/src/domain/services/MessageCodecInternal.js +2 -0
  44. package/src/domain/services/ObserverView.js +6 -6
  45. package/src/domain/services/PatchBuilderV2.js +9 -9
  46. package/src/domain/services/PatchMessageCodec.js +1 -7
  47. package/src/domain/services/ProvenanceIndex.js +6 -8
  48. package/src/domain/services/ProvenancePayload.js +1 -2
  49. package/src/domain/services/QueryBuilder.js +29 -23
  50. package/src/domain/services/StateDiff.js +7 -7
  51. package/src/domain/services/StateSerializerV5.js +8 -6
  52. package/src/domain/services/StreamingBitmapIndexBuilder.js +29 -23
  53. package/src/domain/services/SyncProtocol.js +23 -26
  54. package/src/domain/services/TemporalQuery.js +4 -3
  55. package/src/domain/services/TranslationCost.js +4 -4
  56. package/src/domain/services/WormholeService.js +19 -15
  57. package/src/domain/types/TickReceipt.js +10 -6
  58. package/src/domain/types/WarpTypesV2.js +2 -3
  59. package/src/domain/utils/CachedValue.js +1 -1
  60. package/src/domain/utils/LRUCache.js +3 -3
  61. package/src/domain/utils/MinHeap.js +2 -2
  62. package/src/domain/utils/RefLayout.js +106 -15
  63. package/src/domain/utils/WriterId.js +2 -2
  64. package/src/domain/utils/defaultCodec.js +9 -2
  65. package/src/domain/utils/defaultCrypto.js +36 -0
  66. package/src/domain/utils/parseCursorBlob.js +51 -0
  67. package/src/domain/utils/roaring.js +5 -5
  68. package/src/domain/utils/seekCacheKey.js +32 -0
  69. package/src/domain/warp/PatchSession.js +3 -3
  70. package/src/domain/warp/Writer.js +2 -2
  71. package/src/infrastructure/adapters/BunHttpAdapter.js +21 -8
  72. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +311 -0
  73. package/src/infrastructure/adapters/ClockAdapter.js +2 -2
  74. package/src/infrastructure/adapters/DenoHttpAdapter.js +22 -9
  75. package/src/infrastructure/adapters/GitGraphAdapter.js +16 -27
  76. package/src/infrastructure/adapters/NodeCryptoAdapter.js +16 -3
  77. package/src/infrastructure/adapters/NodeHttpAdapter.js +33 -11
  78. package/src/infrastructure/adapters/WebCryptoAdapter.js +21 -11
  79. package/src/infrastructure/codecs/CborCodec.js +16 -8
  80. package/src/ports/BlobPort.js +2 -2
  81. package/src/ports/CodecPort.js +2 -2
  82. package/src/ports/CommitPort.js +8 -21
  83. package/src/ports/ConfigPort.js +3 -3
  84. package/src/ports/CryptoPort.js +7 -7
  85. package/src/ports/GraphPersistencePort.js +12 -14
  86. package/src/ports/HttpServerPort.js +1 -5
  87. package/src/ports/IndexStoragePort.js +1 -0
  88. package/src/ports/LoggerPort.js +9 -9
  89. package/src/ports/RefPort.js +5 -5
  90. package/src/ports/SeekCachePort.js +73 -0
  91. package/src/ports/TreePort.js +3 -3
  92. package/src/visualization/layouts/converters.js +14 -7
  93. package/src/visualization/layouts/elkAdapter.js +24 -11
  94. package/src/visualization/layouts/elkLayout.js +23 -7
  95. package/src/visualization/layouts/index.js +3 -3
  96. package/src/visualization/renderers/ascii/check.js +30 -17
  97. package/src/visualization/renderers/ascii/graph.js +122 -16
  98. package/src/visualization/renderers/ascii/history.js +29 -90
  99. package/src/visualization/renderers/ascii/index.js +1 -1
  100. package/src/visualization/renderers/ascii/info.js +9 -7
  101. package/src/visualization/renderers/ascii/materialize.js +20 -16
  102. package/src/visualization/renderers/ascii/opSummary.js +81 -0
  103. package/src/visualization/renderers/ascii/path.js +1 -1
  104. package/src/visualization/renderers/ascii/seek.js +344 -0
  105. package/src/visualization/renderers/ascii/table.js +1 -1
  106. package/src/visualization/renderers/svg/index.js +5 -1
@@ -6,79 +6,20 @@
6
6
  import { colors } from './colors.js';
7
7
  import { createBox } from './box.js';
8
8
  import { padRight, padLeft } from '../../utils/unicode.js';
9
- import { truncate } from '../../utils/truncate.js';
10
9
  import { TIMELINE } from './symbols.js';
11
-
12
- // Default pagination settings
13
- const DEFAULT_PAGE_SIZE = 20;
14
-
15
- // Operation type to display info mapping
16
- const OP_DISPLAY = {
17
- NodeAdd: { symbol: '+', label: 'node', color: colors.success },
18
- NodeTombstone: { symbol: '-', label: 'node', color: colors.error },
19
- EdgeAdd: { symbol: '+', label: 'edge', color: colors.success },
20
- EdgeTombstone: { symbol: '-', label: 'edge', color: colors.error },
21
- PropSet: { symbol: '~', label: 'prop', color: colors.warning },
22
- BlobValue: { symbol: '+', label: 'blob', color: colors.primary },
23
- };
24
-
25
- // Default empty operation summary
26
- const EMPTY_OP_SUMMARY = Object.freeze({
27
- NodeAdd: 0,
28
- EdgeAdd: 0,
29
- PropSet: 0,
30
- NodeTombstone: 0,
31
- EdgeTombstone: 0,
32
- BlobValue: 0,
33
- });
10
+ import { OP_DISPLAY, EMPTY_OP_SUMMARY, summarizeOps, formatOpSummary } from './opSummary.js';
34
11
 
35
12
  /**
36
- * Summarizes operations in a patch.
37
- * @param {Object[]} ops - Array of patch operations
38
- * @returns {Object} Summary with counts by operation type
13
+ * @typedef {{ sha?: string, lamport?: number, writerId?: string, opSummary?: Record<string, number>, ops?: Array<{ type: string }> }} PatchEntry
39
14
  */
40
- function summarizeOps(ops) {
41
- const summary = { ...EMPTY_OP_SUMMARY };
42
- for (const op of ops) {
43
- if (op.type && summary[op.type] !== undefined) {
44
- summary[op.type]++;
45
- }
46
- }
47
- return summary;
48
- }
49
15
 
50
- /**
51
- * Formats operation summary as a colored string.
52
- * @param {Object} summary - Operation counts by type
53
- * @param {number} maxWidth - Maximum width for the summary string
54
- * @returns {string} Formatted summary string
55
- */
56
- function formatOpSummary(summary, maxWidth = 40) {
57
- const order = ['NodeAdd', 'EdgeAdd', 'PropSet', 'NodeTombstone', 'EdgeTombstone', 'BlobValue'];
58
- const parts = order
59
- .filter((opType) => summary[opType] > 0)
60
- .map((opType) => {
61
- const display = OP_DISPLAY[opType];
62
- return { text: `${display.symbol}${summary[opType]}${display.label}`, color: display.color };
63
- });
64
-
65
- if (parts.length === 0) {
66
- return colors.muted('(empty)');
67
- }
68
-
69
- // Truncate plain text first to avoid breaking ANSI escape sequences
70
- const plain = parts.map((p) => p.text).join(' ');
71
- const truncated = truncate(plain, maxWidth);
72
- if (truncated === plain) {
73
- return parts.map((p) => p.color(p.text)).join(' ');
74
- }
75
- return colors.muted(truncated);
76
- }
16
+ // Default pagination settings
17
+ const DEFAULT_PAGE_SIZE = 20;
77
18
 
78
19
  /**
79
20
  * Ensures entry has an opSummary, computing one if needed.
80
- * @param {Object} entry - Patch entry
81
- * @returns {Object} Operation summary
21
+ * @param {PatchEntry} entry - Patch entry
22
+ * @returns {Record<string, number>} Operation summary
82
23
  */
83
24
  function ensureOpSummary(entry) {
84
25
  if (entry.opSummary) {
@@ -92,10 +33,10 @@ function ensureOpSummary(entry) {
92
33
 
93
34
  /**
94
35
  * Paginates entries, returning display entries and truncation info.
95
- * @param {Object[]} entries - All entries
36
+ * @param {PatchEntry[]} entries - All entries
96
37
  * @param {number} pageSize - Page size
97
38
  * @param {boolean} showAll - Whether to show all
98
- * @returns {{displayEntries: Object[], truncated: boolean, hiddenCount: number}}
39
+ * @returns {{displayEntries: PatchEntry[], truncated: boolean, hiddenCount: number}}
99
40
  */
100
41
  function paginateEntries(entries, pageSize, showAll) {
101
42
  if (showAll || entries.length <= pageSize) {
@@ -127,17 +68,22 @@ function renderTruncationIndicator(truncated, hiddenCount) {
127
68
  /**
128
69
  * Renders a single patch entry line.
129
70
  * @param {Object} params - Entry parameters
71
+ * @param {PatchEntry} params.entry - Patch entry
72
+ * @param {boolean} params.isLast - Whether this is the last entry
73
+ * @param {number} params.lamportWidth - Width for lamport timestamp padding
74
+ * @param {string} [params.writerStr] - Writer string
75
+ * @param {number} [params.maxWriterIdLen] - Max writer ID length for padding
130
76
  * @returns {string} Formatted entry line
131
77
  */
132
78
  function renderEntryLine({ entry, isLast, lamportWidth, writerStr, maxWriterIdLen }) {
133
79
  const connector = isLast ? TIMELINE.end : TIMELINE.connector;
134
80
  const shortSha = (entry.sha || '').slice(0, 7);
135
- const lamportStr = padLeft(String(entry.lamport), lamportWidth);
81
+ const lamportStr = padLeft(String(entry.lamport ?? 0), lamportWidth);
136
82
  const opSummary = ensureOpSummary(entry);
137
83
  const opSummaryStr = formatOpSummary(opSummary, writerStr ? 30 : 40);
138
84
 
139
85
  if (writerStr) {
140
- const paddedWriter = padRight(writerStr, maxWriterIdLen);
86
+ const paddedWriter = padRight(writerStr, maxWriterIdLen ?? 6);
141
87
  return ` ${connector}${TIMELINE.dot} ${colors.muted(`L${lamportStr}`)} ${colors.primary(paddedWriter)}:${colors.muted(shortSha)} ${opSummaryStr}`;
142
88
  }
143
89
  return ` ${connector}${TIMELINE.dot} ${colors.muted(`L${lamportStr}`)} ${colors.primary(shortSha)} ${opSummaryStr}`;
@@ -164,8 +110,8 @@ function renderSingleWriterFooter(totalCount) {
164
110
 
165
111
  /**
166
112
  * Renders single-writer timeline view.
167
- * @param {Object} payload - History payload
168
- * @param {Object} options - Rendering options
113
+ * @param {{ entries: PatchEntry[], writer: string }} payload - History payload
114
+ * @param {{ pageSize?: number, showAll?: boolean }} options - Rendering options
169
115
  * @returns {string[]} Lines for the timeline
170
116
  */
171
117
  function renderSingleWriterTimeline(payload, options) {
@@ -184,7 +130,7 @@ function renderSingleWriterTimeline(payload, options) {
184
130
  lines.push(colors.muted(' (no patches)'));
185
131
  return lines;
186
132
  }
187
- const maxLamport = Math.max(...displayEntries.map((e) => e.lamport));
133
+ const maxLamport = Math.max(...displayEntries.map((e) => e.lamport ?? 0));
188
134
  const lamportWidth = String(maxLamport).length;
189
135
 
190
136
  lines.push(...renderTruncationIndicator(truncated, hiddenCount));
@@ -200,8 +146,8 @@ function renderSingleWriterTimeline(payload, options) {
200
146
 
201
147
  /**
202
148
  * Merges and sorts entries from all writers by lamport timestamp.
203
- * @param {Object} writers - Map of writerId to entries
204
- * @returns {Object[]} Sorted entries with writerId attached
149
+ * @param {Record<string, PatchEntry[]>} writers - Map of writerId to entries
150
+ * @returns {PatchEntry[]} Sorted entries with writerId attached
205
151
  */
206
152
  function mergeWriterEntries(writers) {
207
153
  const allEntries = [];
@@ -210,7 +156,7 @@ function mergeWriterEntries(writers) {
210
156
  allEntries.push({ ...entry, writerId });
211
157
  }
212
158
  }
213
- allEntries.sort((a, b) => a.lamport - b.lamport || a.writerId.localeCompare(b.writerId));
159
+ allEntries.sort((a, b) => (a.lamport ?? 0) - (b.lamport ?? 0) || (a.writerId ?? '').localeCompare(b.writerId ?? ''));
214
160
  return allEntries;
215
161
  }
216
162
 
@@ -241,8 +187,8 @@ function renderMultiWriterFooter(totalCount, writerCount) {
241
187
 
242
188
  /**
243
189
  * Renders multi-writer timeline view with parallel columns.
244
- * @param {Object} payload - History payload with allWriters data
245
- * @param {Object} options - Rendering options
190
+ * @param {{ writers: Record<string, PatchEntry[]>, graph: string }} payload - History payload with allWriters data
191
+ * @param {{ pageSize?: number, showAll?: boolean }} options - Rendering options
246
192
  * @returns {string[]} Lines for the timeline
247
193
  */
248
194
  function renderMultiWriterTimeline(payload, options) {
@@ -269,7 +215,7 @@ function renderMultiWriterTimeline(payload, options) {
269
215
  lines.push(colors.muted(' (no patches)'));
270
216
  return lines;
271
217
  }
272
- const maxLamport = Math.max(...displayEntries.map((e) => e.lamport));
218
+ const maxLamport = Math.max(...displayEntries.map((e) => e.lamport ?? 0));
273
219
  const lamportWidth = String(maxLamport).length;
274
220
  const maxWriterIdLen = Math.max(...writerIds.map((id) => id.length), 6);
275
221
 
@@ -293,15 +239,8 @@ function renderMultiWriterTimeline(payload, options) {
293
239
 
294
240
  /**
295
241
  * Renders the history view with ASCII timeline.
296
- * @param {Object} payload - History payload from handleHistory
297
- * @param {string} payload.graph - Graph name
298
- * @param {string} [payload.writer] - Writer ID (single writer mode)
299
- * @param {string|null} [payload.nodeFilter] - Node filter if applied
300
- * @param {Object[]} [payload.entries] - Array of patch entries (single writer mode)
301
- * @param {Object} [payload.writers] - Map of writerId to entries (multi-writer mode)
302
- * @param {Object} [options] - Rendering options
303
- * @param {number} [options.pageSize=20] - Number of patches to show per page
304
- * @param {boolean} [options.showAll=false] - Show all patches (no pagination)
242
+ * @param {{ graph: string, writer?: string, nodeFilter?: string | null, entries?: PatchEntry[], writers?: Record<string, PatchEntry[]> }} payload - History payload from handleHistory
243
+ * @param {{ pageSize?: number, showAll?: boolean }} [options] - Rendering options
305
244
  * @returns {string} Formatted ASCII output
306
245
  */
307
246
  export function renderHistoryView(payload, options = {}) {
@@ -311,8 +250,8 @@ export function renderHistoryView(payload, options = {}) {
311
250
 
312
251
  const isMultiWriter = payload.writers && typeof payload.writers === 'object';
313
252
  const contentLines = isMultiWriter
314
- ? renderMultiWriterTimeline(payload, options)
315
- : renderSingleWriterTimeline(payload, options);
253
+ ? renderMultiWriterTimeline(/** @type {{ writers: Record<string, PatchEntry[]>, graph: string }} */ (payload), options)
254
+ : renderSingleWriterTimeline(/** @type {{ entries: PatchEntry[], writer: string }} */ (payload), options);
316
255
 
317
256
  // Add node filter indicator if present
318
257
  if (payload.nodeFilter) {
@@ -330,6 +269,6 @@ export function renderHistoryView(payload, options = {}) {
330
269
  return `${box}\n`;
331
270
  }
332
271
 
333
- export { summarizeOps };
272
+ export { summarizeOps, formatOpSummary, OP_DISPLAY, EMPTY_OP_SUMMARY };
334
273
 
335
274
  export default { renderHistoryView, summarizeOps };
@@ -9,6 +9,6 @@ export { progressBar } from './progress.js';
9
9
  export { renderInfoView } from './info.js';
10
10
  export { renderCheckView } from './check.js';
11
11
  export { renderMaterializeView } from './materialize.js';
12
- export { renderHistoryView, summarizeOps } from './history.js';
12
+ export { renderHistoryView, summarizeOps, formatOpSummary, OP_DISPLAY, EMPTY_OP_SUMMARY } from './history.js';
13
13
  export { renderPathView } from './path.js';
14
14
  export { renderGraphView } from './graph.js';
@@ -10,6 +10,10 @@ import { padRight } from '../../utils/unicode.js';
10
10
  import { timeAgo } from '../../utils/time.js';
11
11
  import { TIMELINE } from './symbols.js';
12
12
 
13
+ /**
14
+ * @typedef {{ name: string, writers?: { count?: number, ids?: string[] }, checkpoint?: { sha?: string, date?: string | Date }, coverage?: { sha?: string }, writerPatches?: Record<string, number> }} GraphInfo
15
+ */
16
+
13
17
  // Box drawing characters (info.js uses verbose key names for card rendering)
14
18
  const BOX = {
15
19
  topLeft: '\u250C', // ┌
@@ -77,7 +81,7 @@ function formatWriterNames(writerIds) {
77
81
 
78
82
  /**
79
83
  * Renders the header lines for a graph card.
80
- * @param {Object} graph - Graph info object
84
+ * @param {GraphInfo} graph - Graph info object
81
85
  * @param {number} contentWidth - Available content width
82
86
  * @returns {string[]} Header lines
83
87
  */
@@ -102,7 +106,7 @@ function renderCardHeader(graph, contentWidth) {
102
106
 
103
107
  /**
104
108
  * Renders writer timeline lines for a graph card.
105
- * @param {Object} writerPatches - Map of writerId to patch count
109
+ * @param {Record<string, number> | undefined} writerPatches - Map of writerId to patch count
106
110
  * @param {number} contentWidth - Available content width
107
111
  * @returns {string[]} Timeline lines
108
112
  */
@@ -133,7 +137,7 @@ function renderWriterTimelines(writerPatches, contentWidth) {
133
137
 
134
138
  /**
135
139
  * Renders checkpoint and coverage lines for a graph card.
136
- * @param {Object} graph - Graph info object
140
+ * @param {GraphInfo} graph - Graph info object
137
141
  * @param {number} contentWidth - Available content width
138
142
  * @returns {string[]} Status lines
139
143
  */
@@ -169,7 +173,7 @@ function renderCardStatus(graph, contentWidth) {
169
173
 
170
174
  /**
171
175
  * Renders a single graph card.
172
- * @param {Object} graph - Graph info object
176
+ * @param {GraphInfo} graph - Graph info object
173
177
  * @param {number} innerWidth - Available width inside the card
174
178
  * @returns {string[]} Array of lines for this graph card
175
179
  */
@@ -186,9 +190,7 @@ function renderGraphCard(graph, innerWidth) {
186
190
 
187
191
  /**
188
192
  * Renders the info view with ASCII box art.
189
- * @param {Object} data - Info payload from handleInfo
190
- * @param {string} data.repo - Repository path
191
- * @param {Object[]} data.graphs - Array of graph info objects
193
+ * @param {{ repo?: string, graphs: GraphInfo[] }} data - Info payload from handleInfo
192
194
  * @returns {string} Formatted ASCII output
193
195
  */
194
196
  export function renderInfoView(data) {
@@ -12,6 +12,11 @@ import { padRight } from '../../utils/unicode.js';
12
12
  import { truncate } from '../../utils/truncate.js';
13
13
  import { formatNumber, formatSha } from './formatters.js';
14
14
 
15
+ /**
16
+ * @typedef {{ graph: string, error?: string, noOp?: boolean, patchCount?: number, nodes?: number, edges?: number, properties?: number, writers?: Record<string, number>, checkpoint?: string | null }} GraphResult
17
+ * @typedef {{ maxNodes: number, maxEdges: number, maxProps: number }} MaxValues
18
+ */
19
+
15
20
  // Bar chart settings
16
21
  const BAR_WIDTH = 20;
17
22
  const STAT_LABEL_WIDTH = 12;
@@ -55,15 +60,15 @@ function renderErrorState(errorMessage) {
55
60
 
56
61
  /**
57
62
  * Render no-op state (already materialized).
58
- * @param {Object} graph - Graph data
63
+ * @param {GraphResult} graph - Graph data
59
64
  * @returns {string[]} No-op state lines
60
65
  */
61
66
  function renderNoOpState(graph) {
62
67
  const lines = [
63
68
  ` ${colors.success('\u2713')} Already materialized (no new patches)`,
64
69
  '',
65
- ` ${padRight('Nodes:', STAT_LABEL_WIDTH)} ${formatNumber(graph.nodes)}`,
66
- ` ${padRight('Edges:', STAT_LABEL_WIDTH)} ${formatNumber(graph.edges)}`,
70
+ ` ${padRight('Nodes:', STAT_LABEL_WIDTH)} ${formatNumber(graph.nodes || 0)}`,
71
+ ` ${padRight('Edges:', STAT_LABEL_WIDTH)} ${formatNumber(graph.edges || 0)}`,
67
72
  ];
68
73
  if (typeof graph.properties === 'number') {
69
74
  lines.push(` ${padRight('Properties:', STAT_LABEL_WIDTH)} ${formatNumber(graph.properties)}`);
@@ -73,7 +78,7 @@ function renderNoOpState(graph) {
73
78
 
74
79
  /**
75
80
  * Render empty graph state (0 patches).
76
- * @param {Object} graph - Graph data
81
+ * @param {GraphResult} graph - Graph data
77
82
  * @returns {string[]} Empty state lines
78
83
  */
79
84
  function renderEmptyState(graph) {
@@ -86,7 +91,7 @@ function renderEmptyState(graph) {
86
91
 
87
92
  /**
88
93
  * Render writer progress section.
89
- * @param {Object} writers - Writer patch counts
94
+ * @param {Record<string, number> | undefined} writers - Writer patch counts
90
95
  * @returns {string[]} Writer lines
91
96
  */
92
97
  function renderWriterSection(writers) {
@@ -108,15 +113,15 @@ function renderWriterSection(writers) {
108
113
 
109
114
  /**
110
115
  * Render statistics section with bar charts.
111
- * @param {Object} graph - Graph data
112
- * @param {Object} maxValues - Max values for scaling
116
+ * @param {GraphResult} graph - Graph data
117
+ * @param {MaxValues} maxValues - Max values for scaling
113
118
  * @returns {string[]} Statistics lines
114
119
  */
115
120
  function renderStatsSection(graph, { maxNodes, maxEdges, maxProps }) {
116
121
  const lines = [
117
122
  ` ${colors.dim('Statistics:')}`,
118
- ` ${padRight('Nodes:', STAT_LABEL_WIDTH)} ${statBar(graph.nodes, maxNodes)} ${formatNumber(graph.nodes)}`,
119
- ` ${padRight('Edges:', STAT_LABEL_WIDTH)} ${statBar(graph.edges, maxEdges)} ${formatNumber(graph.edges)}`,
123
+ ` ${padRight('Nodes:', STAT_LABEL_WIDTH)} ${statBar(graph.nodes || 0, maxNodes)} ${formatNumber(graph.nodes || 0)}`,
124
+ ` ${padRight('Edges:', STAT_LABEL_WIDTH)} ${statBar(graph.edges || 0, maxEdges)} ${formatNumber(graph.edges || 0)}`,
120
125
  ];
121
126
  if (typeof graph.properties === 'number') {
122
127
  lines.push(` ${padRight('Properties:', STAT_LABEL_WIDTH)} ${statBar(graph.properties, maxProps)} ${formatNumber(graph.properties)}`);
@@ -139,8 +144,8 @@ function renderCheckpointInfo(checkpoint) {
139
144
 
140
145
  /**
141
146
  * Render a single graph's materialization result.
142
- * @param {Object} graph - Graph result from materialize
143
- * @param {Object} maxValues - Max values for scaling bars
147
+ * @param {GraphResult} graph - Graph result from materialize
148
+ * @param {MaxValues} maxValues - Max values for scaling bars
144
149
  * @returns {string[]} Array of lines for this graph
145
150
  */
146
151
  function renderGraphResult(graph, maxValues) {
@@ -158,14 +163,14 @@ function renderGraphResult(graph, maxValues) {
158
163
 
159
164
  lines.push(...renderWriterSection(graph.writers));
160
165
  lines.push(...renderStatsSection(graph, maxValues));
161
- lines.push(...renderCheckpointInfo(graph.checkpoint));
166
+ lines.push(...renderCheckpointInfo(graph.checkpoint ?? null));
162
167
  return lines;
163
168
  }
164
169
 
165
170
  /**
166
171
  * Calculate max values for scaling bar charts.
167
- * @param {Object[]} graphs - Array of graph results
168
- * @returns {Object} Max values object
172
+ * @param {GraphResult[]} graphs - Array of graph results
173
+ * @returns {MaxValues} Max values object
169
174
  */
170
175
  function calculateMaxValues(graphs) {
171
176
  const successfulGraphs = graphs.filter((g) => !g.error);
@@ -212,8 +217,7 @@ function getBorderColor(successCount, errorCount) {
212
217
 
213
218
  /**
214
219
  * Render the materialize view dashboard.
215
- * @param {Object} payload - The materialize command payload
216
- * @param {Object[]} payload.graphs - Array of graph results
220
+ * @param {{ graphs: GraphResult[] }} payload - The materialize command payload
217
221
  * @returns {string} Formatted dashboard string
218
222
  */
219
223
  export function renderMaterializeView(payload) {
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Shared operation summary utilities for ASCII renderers.
3
+ *
4
+ * Extracted from history.js so other views (e.g. seek) can reuse the same
5
+ * op-type ordering, symbols, and formatting.
6
+ */
7
+
8
+ import { colors } from './colors.js';
9
+ import { truncate } from '../../utils/truncate.js';
10
+
11
+ /**
12
+ * @typedef {'NodeAdd' | 'EdgeAdd' | 'PropSet' | 'NodeTombstone' | 'EdgeTombstone' | 'BlobValue'} OpType
13
+ * @typedef {Record<OpType, number>} OpSummary
14
+ */
15
+
16
+ // Operation type to display info mapping
17
+ export const OP_DISPLAY = Object.freeze({
18
+ NodeAdd: { symbol: '+', label: 'node', color: colors.success },
19
+ NodeTombstone: { symbol: '-', label: 'node', color: colors.error },
20
+ EdgeAdd: { symbol: '+', label: 'edge', color: colors.success },
21
+ EdgeTombstone: { symbol: '-', label: 'edge', color: colors.error },
22
+ PropSet: { symbol: '~', label: 'prop', color: colors.warning },
23
+ BlobValue: { symbol: '+', label: 'blob', color: colors.primary },
24
+ });
25
+
26
+ // Default empty operation summary
27
+ export const EMPTY_OP_SUMMARY = Object.freeze({
28
+ NodeAdd: 0,
29
+ EdgeAdd: 0,
30
+ PropSet: 0,
31
+ NodeTombstone: 0,
32
+ EdgeTombstone: 0,
33
+ BlobValue: 0,
34
+ });
35
+
36
+ /**
37
+ * Summarizes operations in a patch.
38
+ * @param {Array<{ type: string }>} ops - Array of patch operations
39
+ * @returns {OpSummary} Summary with counts by operation type
40
+ */
41
+ export function summarizeOps(ops) {
42
+ /** @type {OpSummary} */
43
+ const summary = { ...EMPTY_OP_SUMMARY };
44
+ for (const op of ops) {
45
+ const t = /** @type {OpType} */ (op.type);
46
+ if (t && summary[t] !== undefined) {
47
+ summary[t]++;
48
+ }
49
+ }
50
+ return summary;
51
+ }
52
+
53
+ /**
54
+ * Formats operation summary as a colored string.
55
+ * @param {OpSummary | Record<string, number>} summary - Operation counts by type
56
+ * @param {number} maxWidth - Maximum width for the summary string
57
+ * @returns {string} Formatted summary string
58
+ */
59
+ export function formatOpSummary(summary, maxWidth = 40) {
60
+ /** @type {OpType[]} */
61
+ const order = ['NodeAdd', 'EdgeAdd', 'PropSet', 'NodeTombstone', 'EdgeTombstone', 'BlobValue'];
62
+ const parts = order
63
+ .filter((opType) => (/** @type {Record<string, number>} */ (summary))[opType] > 0)
64
+ .map((opType) => {
65
+ const display = OP_DISPLAY[opType];
66
+ return { text: `${display.symbol}${(/** @type {Record<string, number>} */ (summary))[opType]}${display.label}`, color: display.color };
67
+ });
68
+
69
+ if (parts.length === 0) {
70
+ return colors.muted('(empty)');
71
+ }
72
+
73
+ // Truncate plain text first to avoid breaking ANSI escape sequences
74
+ const plain = parts.map((p) => p.text).join(' ');
75
+ const truncated = truncate(plain, maxWidth);
76
+ if (truncated === plain) {
77
+ return parts.map((p) => p.color(p.text)).join(' ');
78
+ }
79
+ return colors.muted(truncated);
80
+ }
81
+
@@ -75,7 +75,7 @@ function createPathSegment({ nodeId, index, pathLength, edges }) {
75
75
  * Builds path segments that fit within the terminal width.
76
76
  * Wraps long paths to multiple lines.
77
77
  * @param {string[]} path - Array of node IDs
78
- * @param {string[]} [edges] - Optional array of edge labels (one fewer than nodes)
78
+ * @param {string[] | undefined} edges - Optional array of edge labels (one fewer than nodes)
79
79
  * @param {number} maxWidth - Maximum line width
80
80
  * @returns {string[]} Array of line strings
81
81
  */