@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
@@ -0,0 +1,344 @@
1
+ /**
2
+ * ASCII renderer for the `seek --view` command.
3
+ *
4
+ * Displays a swimlane dashboard: one horizontal track per writer, with
5
+ * relative-offset column headers that map directly to `--tick=+N/-N` CLI
6
+ * syntax. Included patches (at or before the cursor) render as filled
7
+ * dots on a solid line; excluded (future) patches render as open circles
8
+ * on a dotted line.
9
+ */
10
+
11
+ import boxen from 'boxen';
12
+ import { colors } from './colors.js';
13
+ import { padRight } from '../../utils/unicode.js';
14
+ import { formatSha, formatWriterName } from './formatters.js';
15
+ import { TIMELINE } from './symbols.js';
16
+ import { formatOpSummary } from './opSummary.js';
17
+
18
+ /**
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
21
+ */
22
+
23
+ /** Maximum number of tick columns shown in the windowed view. */
24
+ const MAX_COLS = 9;
25
+
26
+ /** Character width of each tick column (marker + connector gap). */
27
+ const COL_W = 6;
28
+
29
+ /** Character width reserved for the writer name column. */
30
+ const NAME_W = 10;
31
+
32
+ /** Middle-dot used for excluded-zone connectors. */
33
+ const DOT_MID = '\u00B7'; // ·
34
+
35
+ /** Open circle used for excluded-zone patch markers. */
36
+ const CIRCLE_OPEN = '\u25CB'; // ○
37
+
38
+ /** @param {number} n @returns {string} */
39
+ function formatDelta(n) {
40
+ if (typeof n !== 'number' || !Number.isFinite(n) || n === 0) {
41
+ return '';
42
+ }
43
+ const sign = n > 0 ? '+' : '';
44
+ return ` (${sign}${n})`;
45
+ }
46
+
47
+ /**
48
+ * @param {number} n
49
+ * @param {string} singular
50
+ * @param {string} plural
51
+ * @returns {string}
52
+ */
53
+ function pluralize(n, singular, plural) {
54
+ return n === 1 ? singular : plural;
55
+ }
56
+
57
+ /** @param {Record<string, any> | undefined} tickReceipt @returns {string[]} */
58
+ function buildReceiptLines(tickReceipt) {
59
+ if (!tickReceipt || typeof tickReceipt !== 'object') {
60
+ return [];
61
+ }
62
+
63
+ const entries = Object.entries(tickReceipt)
64
+ .filter(([writerId, entry]) => writerId && entry && typeof entry === 'object')
65
+ .sort(([a], [b]) => a.localeCompare(b));
66
+
67
+ const lines = [];
68
+ for (const [writerId, entry] of entries) {
69
+ const sha = typeof entry.sha === 'string' ? entry.sha : null;
70
+ const opSummary = entry.opSummary && typeof entry.opSummary === 'object' ? entry.opSummary : entry;
71
+ const name = padRight(formatWriterName(writerId, NAME_W), NAME_W);
72
+ const shaStr = sha ? ` ${formatSha(sha)}` : '';
73
+ lines.push(` ${name}${shaStr} ${formatOpSummary(opSummary, 40)}`);
74
+ }
75
+
76
+ return lines;
77
+ }
78
+
79
+ // ============================================================================
80
+ // Window
81
+ // ============================================================================
82
+
83
+ /**
84
+ * Computes a sliding window of tick positions centered on the current tick.
85
+ *
86
+ * When all points fit within {@link MAX_COLS}, the full array is returned.
87
+ * Otherwise a window of MAX_COLS entries is centered on `currentIdx`, with
88
+ * clamping at both ends.
89
+ *
90
+ * @param {number[]} allPoints - All tick positions (including virtual tick 0)
91
+ * @param {number} currentIdx - Index of the current tick in `allPoints`
92
+ * @returns {{ points: number[], currentCol: number, moreLeft: boolean, moreRight: boolean }}
93
+ */
94
+ function computeWindow(allPoints, currentIdx) {
95
+ if (allPoints.length <= MAX_COLS) {
96
+ return {
97
+ points: allPoints,
98
+ currentCol: currentIdx,
99
+ moreLeft: false,
100
+ moreRight: false,
101
+ };
102
+ }
103
+
104
+ const half = Math.floor(MAX_COLS / 2);
105
+ let start = currentIdx - half;
106
+ if (start < 0) {
107
+ start = 0;
108
+ }
109
+ let end = start + MAX_COLS;
110
+ if (end > allPoints.length) {
111
+ end = allPoints.length;
112
+ start = end - MAX_COLS;
113
+ }
114
+
115
+ return {
116
+ points: allPoints.slice(start, end),
117
+ currentCol: currentIdx - start,
118
+ moreLeft: start > 0,
119
+ moreRight: end < allPoints.length,
120
+ };
121
+ }
122
+
123
+ // ============================================================================
124
+ // Header row
125
+ // ============================================================================
126
+
127
+ /**
128
+ * Builds the column header row showing relative step offsets.
129
+ *
130
+ * The current tick is rendered as `[N]` (absolute tick number); all other
131
+ * columns show their signed step distance (`-2`, `-1`, `+1`, `+2`, etc.)
132
+ * matching the `--tick=+N/-N` CLI syntax.
133
+ *
134
+ * @param {{ points: number[], currentCol: number }} win - Computed window
135
+ * @returns {string} Formatted, indented header line
136
+ */
137
+ function buildHeaderRow(win) {
138
+ const { points, currentCol } = win;
139
+ let header = '';
140
+
141
+ for (let i = 0; i < points.length; i++) {
142
+ const rel = i - currentCol;
143
+ let label;
144
+ if (rel === 0) {
145
+ label = `[${points[i]}]`;
146
+ } else if (rel > 0) {
147
+ label = `+${rel}`;
148
+ } else {
149
+ label = String(rel);
150
+ }
151
+ header += label.padEnd(COL_W);
152
+ }
153
+
154
+ const margin = ' '.repeat(NAME_W + 2);
155
+ return ` ${margin}${header.trimEnd()}`;
156
+ }
157
+
158
+ // ============================================================================
159
+ // Writer swimlane
160
+ // ============================================================================
161
+
162
+ /**
163
+ * Renders a single cell (marker) in the swimlane grid.
164
+ *
165
+ * @param {boolean} hasPatch - Whether this writer has a patch at this tick
166
+ * @param {boolean} incl - Whether this tick is in the included zone
167
+ * @returns {string} A single styled character
168
+ */
169
+ function renderCell(hasPatch, incl) {
170
+ if (hasPatch) {
171
+ return incl ? colors.success(TIMELINE.dot) : colors.muted(CIRCLE_OPEN);
172
+ }
173
+ return incl ? TIMELINE.line : colors.muted(DOT_MID);
174
+ }
175
+
176
+ /**
177
+ * Builds the swimlane track string for a writer across the window columns.
178
+ *
179
+ * @param {Set<number>} patchSet - Set of ticks where this writer has patches
180
+ * @param {number[]} points - Window tick positions
181
+ * @param {number} currentTick - Active seek cursor tick
182
+ * @returns {string} Styled swimlane track
183
+ */
184
+ function buildLane(patchSet, points, currentTick) {
185
+ let lane = '';
186
+ for (let i = 0; i < points.length; i++) {
187
+ const t = points[i];
188
+ const incl = t <= currentTick;
189
+
190
+ if (i > 0) {
191
+ const n = COL_W - 1;
192
+ lane += incl
193
+ ? TIMELINE.line.repeat(n)
194
+ : colors.muted(DOT_MID.repeat(n));
195
+ }
196
+
197
+ lane += renderCell(patchSet.has(t), incl);
198
+ }
199
+ return lane;
200
+ }
201
+
202
+ /**
203
+ * Builds one writer's horizontal swimlane row.
204
+ *
205
+ * Each tick position in the window gets a marker character:
206
+ * - `●` (green) — writer has a patch here AND tick ≤ currentTick (included)
207
+ * - `○` (muted) — writer has a patch here AND tick > currentTick (excluded)
208
+ * - `─` (solid) — no patch, included zone
209
+ * - `·` (muted) — no patch, excluded zone
210
+ *
211
+ * Between consecutive columns, connector characters of the appropriate style
212
+ * fill the gap (COL_W − 1 chars).
213
+ *
214
+ * @param {Object} opts
215
+ * @param {string} opts.writerId
216
+ * @param {WriterInfo} opts.writerInfo - `{ ticks, tipSha, tickShas }`
217
+ * @param {{ points: number[] }} opts.win - Computed window
218
+ * @param {number} opts.currentTick - Active seek cursor tick
219
+ * @returns {string} Formatted, indented swimlane line
220
+ */
221
+ function buildWriterSwimRow({ writerId, writerInfo, win, currentTick }) {
222
+ const patchSet = new Set(writerInfo.ticks);
223
+ const tickShas = writerInfo.tickShas || {};
224
+ const lane = buildLane(patchSet, win.points, currentTick);
225
+
226
+ // SHA of the highest included patch
227
+ const included = writerInfo.ticks.filter((t) => t <= currentTick);
228
+ const maxIncl = included.length > 0 ? included[included.length - 1] : null;
229
+ const sha = maxIncl !== null
230
+ ? (tickShas[maxIncl] || writerInfo.tipSha)
231
+ : writerInfo.tipSha;
232
+
233
+ const name = padRight(formatWriterName(writerId, NAME_W), NAME_W);
234
+ const shaStr = sha ? ` ${formatSha(sha)}` : '';
235
+
236
+ return ` ${name} ${lane}${shaStr}`;
237
+ }
238
+
239
+ // ============================================================================
240
+ // Body assembly
241
+ // ============================================================================
242
+
243
+ /**
244
+ * Builds the tick-position array and index of the current tick.
245
+ *
246
+ * Ensures the current tick is always present: if `tick` is absent from
247
+ * `ticks` (e.g. saved cursor after writer refs changed), it is inserted
248
+ * at the correct sorted position so the window always centres on it.
249
+ *
250
+ * @param {number[]} ticks - Discovered Lamport ticks
251
+ * @param {number} tick - Current cursor tick
252
+ * @returns {{ allPoints: number[], currentIdx: number }}
253
+ */
254
+ function buildTickPoints(ticks, tick) {
255
+ const allPoints = (ticks[0] === 0) ? [...ticks] : [0, ...ticks];
256
+ let currentIdx = allPoints.indexOf(tick);
257
+ if (currentIdx === -1) {
258
+ let ins = allPoints.findIndex((t) => t > tick);
259
+ if (ins === -1) {
260
+ ins = allPoints.length;
261
+ }
262
+ allPoints.splice(ins, 0, tick);
263
+ currentIdx = ins;
264
+ }
265
+ return { allPoints, currentIdx };
266
+ }
267
+
268
+ /**
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
273
+ */
274
+ function buildSeekBodyLines(payload) {
275
+ const { graph, tick, maxTick, ticks, nodes, edges, patchCount, perWriter, diff, tickReceipt } = payload;
276
+ const lines = [];
277
+
278
+ lines.push('');
279
+ lines.push(` ${colors.bold('GRAPH:')} ${graph}`);
280
+ lines.push(` ${colors.bold('POSITION:')} tick ${tick} of ${maxTick}`);
281
+ lines.push('');
282
+
283
+ if (ticks.length === 0) {
284
+ lines.push(` ${colors.muted('(no ticks)')}`);
285
+ } else {
286
+ const { allPoints, currentIdx } = buildTickPoints(ticks, tick);
287
+ const win = computeWindow(allPoints, currentIdx);
288
+
289
+ // Column headers with relative offsets
290
+ lines.push(buildHeaderRow(win));
291
+
292
+ // Per-writer swimlanes
293
+ /** @type {Array<[string, WriterInfo]>} */
294
+ const writerEntries = perWriter instanceof Map
295
+ ? [...perWriter.entries()]
296
+ : Object.entries(perWriter).map(([k, v]) => [k, v]);
297
+
298
+ for (const [writerId, writerInfo] of writerEntries) {
299
+ lines.push(buildWriterSwimRow({ writerId, writerInfo, win, currentTick: tick }));
300
+ }
301
+ }
302
+
303
+ lines.push('');
304
+ const edgeLabel = pluralize(edges, 'edge', 'edges');
305
+ const nodeLabel = pluralize(nodes, 'node', 'nodes');
306
+ const patchLabel = pluralize(patchCount, 'patch', 'patches');
307
+
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}`);
311
+
312
+ const receiptLines = buildReceiptLines(tickReceipt);
313
+ if (receiptLines.length > 0) {
314
+ lines.push('');
315
+ lines.push(` ${colors.bold(`Tick ${tick}:`)}`);
316
+ lines.push(...receiptLines);
317
+ }
318
+ lines.push('');
319
+
320
+ return lines;
321
+ }
322
+
323
+ // ============================================================================
324
+ // Public API
325
+ // ============================================================================
326
+
327
+ /**
328
+ * Renders the seek view dashboard inside a double-bordered box.
329
+ *
330
+ * @param {SeekPayload} payload - Seek payload from the CLI handler
331
+ * @returns {string} Boxen-wrapped ASCII dashboard with trailing newline
332
+ */
333
+ export function renderSeekView(payload) {
334
+ const lines = buildSeekBodyLines(payload);
335
+ const body = lines.join('\n');
336
+
337
+ return `${boxen(body, {
338
+ title: ' SEEK ',
339
+ titleAlignment: 'center',
340
+ padding: 0,
341
+ borderStyle: 'double',
342
+ borderColor: 'cyan',
343
+ })}\n`;
344
+ }
@@ -6,7 +6,7 @@ import Table from 'cli-table3';
6
6
  * @param {Object} [options] - Options forwarded to cli-table3 constructor
7
7
  * @param {string[]} [options.head] - Column headers
8
8
  * @param {Object} [options.style] - Style overrides (defaults: head=cyan, border=gray)
9
- * @returns {import('cli-table3')} A cli-table3 instance
9
+ * @returns {InstanceType<typeof Table>} A cli-table3 instance
10
10
  */
11
11
  export function createTable(options = {}) {
12
12
  const defaultStyle = { head: ['cyan'], border: ['gray'] };
@@ -16,6 +16,7 @@ const PALETTE = {
16
16
  arrowFill: '#a6adc8',
17
17
  };
18
18
 
19
+ /** @param {string} str @returns {string} */
19
20
  function escapeXml(str) {
20
21
  return String(str)
21
22
  .replace(/&/g, '&amp;')
@@ -46,6 +47,7 @@ function renderStyle() {
46
47
  ].join('\n');
47
48
  }
48
49
 
50
+ /** @param {{ id: string, x: number, y: number, width: number, height: number, label?: string }} node @returns {string} */
49
51
  function renderNode(node) {
50
52
  const { x, y, width, height } = node;
51
53
  const label = escapeXml(node.label ?? node.id);
@@ -59,6 +61,7 @@ function renderNode(node) {
59
61
  ].join('\n');
60
62
  }
61
63
 
64
+ /** @param {{ startPoint?: { x: number, y: number }, bendPoints?: Array<{ x: number, y: number }>, endPoint?: { x: number, y: number } }} section @returns {Array<{ x: number, y: number }>} */
62
65
  function sectionToPoints(section) {
63
66
  const pts = [];
64
67
  if (section.startPoint) {
@@ -73,6 +76,7 @@ function sectionToPoints(section) {
73
76
  return pts;
74
77
  }
75
78
 
79
+ /** @param {{ sections?: Array<{ startPoint?: { x: number, y: number }, bendPoints?: Array<{ x: number, y: number }>, endPoint?: { x: number, y: number } }>, label?: string }} edge @returns {string} */
76
80
  function renderEdge(edge) {
77
81
  const { sections } = edge;
78
82
  if (!sections || sections.length === 0) {
@@ -115,7 +119,7 @@ function renderEdge(edge) {
115
119
  /**
116
120
  * Renders a PositionedGraph as an SVG string.
117
121
  *
118
- * @param {Object} positionedGraph - PositionedGraph from runLayout()
122
+ * @param {{ nodes?: Array<{ id: string, x: number, y: number, width: number, height: number, label?: string }>, edges?: Array<{ sections?: Array<any>, label?: string }>, width?: number, height?: number }} positionedGraph - PositionedGraph from runLayout()
119
123
  * @param {{ title?: string }} [options]
120
124
  * @returns {string} Complete SVG markup
121
125
  */