@git-stunts/git-warp 10.3.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.
Files changed (108) hide show
  1. package/README.md +6 -3
  2. package/SECURITY.md +89 -1
  3. package/bin/warp-graph.js +574 -208
  4. package/index.d.ts +55 -0
  5. package/index.js +4 -0
  6. package/package.json +8 -4
  7. package/src/domain/WarpGraph.js +334 -161
  8. package/src/domain/crdt/LWW.js +1 -1
  9. package/src/domain/crdt/ORSet.js +10 -6
  10. package/src/domain/crdt/VersionVector.js +5 -1
  11. package/src/domain/errors/EmptyMessageError.js +2 -4
  12. package/src/domain/errors/ForkError.js +4 -0
  13. package/src/domain/errors/IndexError.js +4 -0
  14. package/src/domain/errors/OperationAbortedError.js +4 -0
  15. package/src/domain/errors/QueryError.js +4 -0
  16. package/src/domain/errors/SchemaUnsupportedError.js +4 -0
  17. package/src/domain/errors/ShardCorruptionError.js +2 -6
  18. package/src/domain/errors/ShardLoadError.js +2 -6
  19. package/src/domain/errors/ShardValidationError.js +2 -7
  20. package/src/domain/errors/StorageError.js +2 -6
  21. package/src/domain/errors/SyncError.js +4 -0
  22. package/src/domain/errors/TraversalError.js +4 -0
  23. package/src/domain/errors/WarpError.js +2 -4
  24. package/src/domain/errors/WormholeError.js +4 -0
  25. package/src/domain/services/AnchorMessageCodec.js +1 -4
  26. package/src/domain/services/BitmapIndexBuilder.js +10 -6
  27. package/src/domain/services/BitmapIndexReader.js +27 -21
  28. package/src/domain/services/BoundaryTransitionRecord.js +22 -15
  29. package/src/domain/services/CheckpointMessageCodec.js +1 -7
  30. package/src/domain/services/CheckpointSerializerV5.js +20 -19
  31. package/src/domain/services/CheckpointService.js +18 -18
  32. package/src/domain/services/CommitDagTraversalService.js +13 -1
  33. package/src/domain/services/DagPathFinding.js +40 -18
  34. package/src/domain/services/DagTopology.js +7 -6
  35. package/src/domain/services/DagTraversal.js +5 -3
  36. package/src/domain/services/Frontier.js +7 -6
  37. package/src/domain/services/HealthCheckService.js +15 -14
  38. package/src/domain/services/HookInstaller.js +64 -13
  39. package/src/domain/services/HttpSyncServer.js +88 -19
  40. package/src/domain/services/IndexRebuildService.js +12 -12
  41. package/src/domain/services/IndexStalenessChecker.js +13 -6
  42. package/src/domain/services/JoinReducer.js +28 -27
  43. package/src/domain/services/LogicalTraversal.js +7 -6
  44. package/src/domain/services/MessageCodecInternal.js +2 -0
  45. package/src/domain/services/ObserverView.js +6 -6
  46. package/src/domain/services/PatchBuilderV2.js +9 -9
  47. package/src/domain/services/PatchMessageCodec.js +1 -7
  48. package/src/domain/services/ProvenanceIndex.js +6 -8
  49. package/src/domain/services/ProvenancePayload.js +1 -2
  50. package/src/domain/services/QueryBuilder.js +29 -23
  51. package/src/domain/services/StateDiff.js +7 -7
  52. package/src/domain/services/StateSerializerV5.js +8 -6
  53. package/src/domain/services/StreamingBitmapIndexBuilder.js +29 -23
  54. package/src/domain/services/SyncAuthService.js +396 -0
  55. package/src/domain/services/SyncProtocol.js +23 -26
  56. package/src/domain/services/TemporalQuery.js +4 -3
  57. package/src/domain/services/TranslationCost.js +4 -4
  58. package/src/domain/services/WormholeService.js +19 -15
  59. package/src/domain/types/TickReceipt.js +10 -6
  60. package/src/domain/types/WarpTypesV2.js +2 -3
  61. package/src/domain/utils/CachedValue.js +1 -1
  62. package/src/domain/utils/LRUCache.js +3 -3
  63. package/src/domain/utils/MinHeap.js +2 -2
  64. package/src/domain/utils/RefLayout.js +19 -0
  65. package/src/domain/utils/WriterId.js +2 -2
  66. package/src/domain/utils/defaultCodec.js +9 -2
  67. package/src/domain/utils/defaultCrypto.js +36 -0
  68. package/src/domain/utils/roaring.js +5 -5
  69. package/src/domain/utils/seekCacheKey.js +32 -0
  70. package/src/domain/warp/PatchSession.js +3 -3
  71. package/src/domain/warp/Writer.js +2 -2
  72. package/src/infrastructure/adapters/BunHttpAdapter.js +21 -8
  73. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +311 -0
  74. package/src/infrastructure/adapters/ClockAdapter.js +2 -2
  75. package/src/infrastructure/adapters/DenoHttpAdapter.js +22 -9
  76. package/src/infrastructure/adapters/GitGraphAdapter.js +25 -83
  77. package/src/infrastructure/adapters/InMemoryGraphAdapter.js +488 -0
  78. package/src/infrastructure/adapters/NodeCryptoAdapter.js +16 -3
  79. package/src/infrastructure/adapters/NodeHttpAdapter.js +33 -11
  80. package/src/infrastructure/adapters/WebCryptoAdapter.js +21 -11
  81. package/src/infrastructure/adapters/adapterValidation.js +90 -0
  82. package/src/infrastructure/codecs/CborCodec.js +16 -8
  83. package/src/ports/BlobPort.js +2 -2
  84. package/src/ports/CodecPort.js +2 -2
  85. package/src/ports/CommitPort.js +8 -21
  86. package/src/ports/ConfigPort.js +3 -3
  87. package/src/ports/CryptoPort.js +7 -7
  88. package/src/ports/GraphPersistencePort.js +12 -14
  89. package/src/ports/HttpServerPort.js +1 -5
  90. package/src/ports/IndexStoragePort.js +1 -0
  91. package/src/ports/LoggerPort.js +9 -9
  92. package/src/ports/RefPort.js +5 -5
  93. package/src/ports/SeekCachePort.js +73 -0
  94. package/src/ports/TreePort.js +3 -3
  95. package/src/visualization/layouts/converters.js +14 -7
  96. package/src/visualization/layouts/elkAdapter.js +17 -4
  97. package/src/visualization/layouts/elkLayout.js +23 -7
  98. package/src/visualization/layouts/index.js +3 -3
  99. package/src/visualization/renderers/ascii/check.js +30 -17
  100. package/src/visualization/renderers/ascii/graph.js +92 -1
  101. package/src/visualization/renderers/ascii/history.js +28 -26
  102. package/src/visualization/renderers/ascii/info.js +9 -7
  103. package/src/visualization/renderers/ascii/materialize.js +20 -16
  104. package/src/visualization/renderers/ascii/opSummary.js +15 -7
  105. package/src/visualization/renderers/ascii/path.js +1 -1
  106. package/src/visualization/renderers/ascii/seek.js +187 -23
  107. package/src/visualization/renderers/ascii/table.js +1 -1
  108. package/src/visualization/renderers/svg/index.js +5 -1
@@ -2,6 +2,16 @@
2
2
  * ELK adapter: converts normalised graph data into ELK JSON input.
3
3
  */
4
4
 
5
+ /**
6
+ * @typedef {{ id: string, label: string, props?: Record<string, any> }} GraphDataNode
7
+ * @typedef {{ from: string, to: string, label?: string }} GraphDataEdge
8
+ * @typedef {{ nodes: GraphDataNode[], edges: GraphDataEdge[] }} GraphData
9
+ * @typedef {{ text: string }} ElkLabel
10
+ * @typedef {{ id: string, sources: string[], targets: string[], labels?: ElkLabel[] }} ElkEdge
11
+ * @typedef {{ id: string, width: number, height: number, labels: ElkLabel[] }} ElkChild
12
+ * @typedef {{ id: string, layoutOptions: Record<string, string>, children: ElkChild[], edges: ElkEdge[] }} ElkGraph
13
+ */
14
+
5
15
  const LAYOUT_PRESETS = {
6
16
  query: {
7
17
  'elk.algorithm': 'layered',
@@ -29,7 +39,7 @@ const DEFAULT_PRESET = LAYOUT_PRESETS.query;
29
39
  * Returns ELK layout options for a given visualisation type.
30
40
  *
31
41
  * @param {'query'|'path'|'slice'} type
32
- * @returns {Object} ELK layout options
42
+ * @returns {Record<string, string>} ELK layout options
33
43
  */
34
44
  export function getDefaultLayoutOptions(type) {
35
45
  return LAYOUT_PRESETS[type] ?? DEFAULT_PRESET;
@@ -38,6 +48,8 @@ export function getDefaultLayoutOptions(type) {
38
48
  /**
39
49
  * Estimates pixel width for a node label.
40
50
  * Approximates monospace glyph width at ~9px with 24px padding.
51
+ * @param {string | undefined} label
52
+ * @returns {number}
41
53
  */
42
54
  function estimateNodeWidth(label) {
43
55
  const charWidth = 9;
@@ -51,9 +63,9 @@ const NODE_HEIGHT = 30;
51
63
  /**
52
64
  * Converts normalised graph data to an ELK graph JSON object.
53
65
  *
54
- * @param {{ nodes: Array, edges: Array }} graphData
55
- * @param {{ type?: string, layoutOptions?: Object }} [options]
56
- * @returns {Object} ELK-format graph
66
+ * @param {GraphData} graphData
67
+ * @param {{ type?: 'query'|'path'|'slice', layoutOptions?: Record<string, string> }} [options]
68
+ * @returns {ElkGraph} ELK-format graph
57
69
  */
58
70
  export function toElkGraph(graphData, options = {}) {
59
71
  const { type = 'query', layoutOptions } = options;
@@ -66,6 +78,7 @@ export function toElkGraph(graphData, options = {}) {
66
78
  }));
67
79
 
68
80
  const edges = (graphData.edges ?? []).map((e, i) => {
81
+ /** @type {ElkEdge} */
69
82
  const edge = {
70
83
  id: `e${i}`,
71
84
  sources: [e.from],
@@ -5,11 +5,22 @@
5
5
  * a layout is actually requested, keeping normal CLI startup fast.
6
6
  */
7
7
 
8
+ /**
9
+ * @typedef {{ id: string, x?: number, y?: number, width?: number, height?: number, labels?: Array<{ text: string }> }} ElkResultChild
10
+ * @typedef {{ id: string, sources?: string[], targets?: string[], labels?: Array<{ text: string }>, sections?: any[] }} ElkResultEdge
11
+ * @typedef {{ children?: ElkResultChild[], edges?: ElkResultEdge[], width?: number, height?: number }} ElkResult
12
+ * @typedef {{ id: string, x: number, y: number, width: number, height: number, label: string }} PosNode
13
+ * @typedef {{ id: string, source: string, target: string, label?: string, sections: any[] }} PosEdge
14
+ * @typedef {{ nodes: PosNode[], edges: PosEdge[], width: number, height: number }} PositionedGraph
15
+ * @typedef {{ id: string, children?: Array<{ id: string, width?: number, height?: number, labels?: Array<{ text: string }> }>, edges?: Array<{ id: string, sources?: string[], targets?: string[], labels?: Array<{ text: string }> }>, layoutOptions?: Record<string, string> }} ElkGraphInput
16
+ */
17
+
18
+ /** @type {Promise<any> | null} */
8
19
  let elkPromise = null;
9
20
 
10
21
  /**
11
22
  * Returns (or creates) a singleton ELK instance.
12
- * @returns {Promise<Object>} ELK instance
23
+ * @returns {Promise<any>} ELK instance
13
24
  */
14
25
  function getElk() {
15
26
  if (!elkPromise) {
@@ -21,10 +32,11 @@ function getElk() {
21
32
  /**
22
33
  * Runs ELK layout on a graph and returns a PositionedGraph.
23
34
  *
24
- * @param {Object} elkGraph - ELK-format graph from toElkGraph()
25
- * @returns {Promise<Object>} PositionedGraph
35
+ * @param {ElkGraphInput} elkGraph - ELK-format graph from toElkGraph()
36
+ * @returns {Promise<PositionedGraph>} PositionedGraph
26
37
  */
27
38
  export async function runLayout(elkGraph) {
39
+ /** @type {ElkResult | undefined} */
28
40
  let result;
29
41
  try {
30
42
  const elk = await getElk();
@@ -37,9 +49,11 @@ export async function runLayout(elkGraph) {
37
49
 
38
50
  /**
39
51
  * Converts ELK output to a PositionedGraph.
52
+ * @param {ElkResult | undefined} result
53
+ * @returns {PositionedGraph}
40
54
  */
41
55
  function toPositionedGraph(result) {
42
- const nodes = (result.children ?? []).map((c) => ({
56
+ const nodes = (result?.children ?? []).map((c) => ({
43
57
  id: c.id,
44
58
  x: c.x ?? 0,
45
59
  y: c.y ?? 0,
@@ -48,7 +62,7 @@ function toPositionedGraph(result) {
48
62
  label: c.labels?.[0]?.text ?? c.id,
49
63
  }));
50
64
 
51
- const edges = (result.edges ?? []).map((e) => ({
65
+ const edges = (result?.edges ?? []).map((e) => ({
52
66
  id: e.id,
53
67
  source: e.sources?.[0] ?? '',
54
68
  target: e.targets?.[0] ?? '',
@@ -59,13 +73,15 @@ function toPositionedGraph(result) {
59
73
  return {
60
74
  nodes,
61
75
  edges,
62
- width: result.width ?? 0,
63
- height: result.height ?? 0,
76
+ width: result?.width ?? 0,
77
+ height: result?.height ?? 0,
64
78
  };
65
79
  }
66
80
 
67
81
  /**
68
82
  * Fallback: line nodes up horizontally when ELK fails.
83
+ * @param {ElkGraphInput} elkGraph
84
+ * @returns {PositionedGraph}
69
85
  */
70
86
  function fallbackLayout(elkGraph) {
71
87
  let x = 20;
@@ -19,9 +19,9 @@ import { runLayout } from './elkLayout.js';
19
19
  /**
20
20
  * Full pipeline: graphData → PositionedGraph.
21
21
  *
22
- * @param {{ nodes: Array, edges: Array }} graphData - Normalised graph data
23
- * @param {{ type?: string, layoutOptions?: Object }} [options]
24
- * @returns {Promise<Object>} PositionedGraph
22
+ * @param {{ nodes: Array<{ id: string, label: string }>, edges: Array<{ from: string, to: string, label?: string }> }} graphData - Normalised graph data
23
+ * @param {{ type?: 'query'|'path'|'slice', layoutOptions?: Record<string, string> }} [options]
24
+ * @returns {Promise<{ nodes: any[], edges: any[], width: number, height: number }>} PositionedGraph
25
25
  */
26
26
  export async function layoutGraph(graphData, options = {}) {
27
27
  const elkGraph = toElkGraph(graphData, options);
@@ -12,6 +12,18 @@ import { colors } from './colors.js';
12
12
  import { padRight } from '../../utils/unicode.js';
13
13
  import { formatAge } from './formatters.js';
14
14
 
15
+ /**
16
+ * @typedef {{ cachedState?: string, tombstoneRatio?: number, patchesSinceCheckpoint?: number }} CheckStatus
17
+ * @typedef {{ writerId?: string, sha?: string }} WriterHead
18
+ * @typedef {{ sha?: string, ageSeconds?: number | null }} CheckpointInfo
19
+ * @typedef {{ installed?: boolean, foreign?: boolean, current?: boolean, version?: string }} HookInfo
20
+ * @typedef {{ sha?: string, missingWriters?: string[] }} CoverageInfo
21
+ * @typedef {{ status?: string }} HealthInfo
22
+ * @typedef {{ tombstoneRatio?: number }} GCInfo
23
+ * @typedef {{ heads?: WriterHead[] }} WritersInfo
24
+ * @typedef {{ graph: string, health: HealthInfo, status: CheckStatus, writers: WritersInfo, checkpoint: CheckpointInfo, coverage: CoverageInfo, gc: GCInfo, hook: HookInfo | null }} CheckPayload
25
+ */
26
+
15
27
  // Health thresholds
16
28
  const TOMBSTONE_HEALTHY_MAX = 0.15; // < 15% tombstones = healthy
17
29
  const TOMBSTONE_WARNING_MAX = 0.30; // < 30% tombstones = warning
@@ -19,7 +31,7 @@ const CACHE_STALE_PENALTY = 20; // Reduce "freshness" score for stale ca
19
31
 
20
32
  /**
21
33
  * Get cache freshness percentage and state.
22
- * @param {Object} status - The status object from check payload
34
+ * @param {CheckStatus | null} status - The status object from check payload
23
35
  * @returns {{ percent: number, label: string }}
24
36
  */
25
37
  function getCacheFreshness(status) {
@@ -78,7 +90,7 @@ function tombstoneBar(percent, width = 20) {
78
90
 
79
91
  /**
80
92
  * Format writer information for display.
81
- * @param {Object[]} heads - Writer heads array
93
+ * @param {WriterHead[] | undefined} heads - Writer heads array
82
94
  * @returns {string}
83
95
  */
84
96
  function formatWriters(heads) {
@@ -94,7 +106,7 @@ function formatWriters(heads) {
94
106
 
95
107
  /**
96
108
  * Format checkpoint status line.
97
- * @param {Object} checkpoint - Checkpoint info
109
+ * @param {CheckpointInfo | null} checkpoint - Checkpoint info
98
110
  * @returns {string}
99
111
  */
100
112
  function formatCheckpoint(checkpoint) {
@@ -103,13 +115,14 @@ function formatCheckpoint(checkpoint) {
103
115
  }
104
116
 
105
117
  const sha = colors.muted(checkpoint.sha.slice(0, 7));
106
- const age = formatAge(checkpoint.ageSeconds);
118
+ const ageSeconds = checkpoint.ageSeconds ?? null;
119
+ const age = formatAge(ageSeconds);
107
120
 
108
121
  // Add checkmark for recent checkpoints (< 5 min), warning for older
109
122
  let status;
110
- if (checkpoint.ageSeconds !== null && checkpoint.ageSeconds < 300) {
123
+ if (ageSeconds !== null && ageSeconds < 300) {
111
124
  status = colors.success('\u2713');
112
- } else if (checkpoint.ageSeconds !== null && checkpoint.ageSeconds < 3600) {
125
+ } else if (ageSeconds !== null && ageSeconds < 3600) {
113
126
  status = colors.warning('\u2713');
114
127
  } else {
115
128
  status = colors.muted('\u2713');
@@ -120,7 +133,7 @@ function formatCheckpoint(checkpoint) {
120
133
 
121
134
  /**
122
135
  * Format hook status line.
123
- * @param {Object|null} hook - Hook status
136
+ * @param {HookInfo|null} hook - Hook status
124
137
  * @returns {string}
125
138
  */
126
139
  function formatHook(hook) {
@@ -145,7 +158,7 @@ function formatHook(hook) {
145
158
 
146
159
  /**
147
160
  * Format coverage status line.
148
- * @param {Object} coverage - Coverage info
161
+ * @param {CoverageInfo | null} coverage - Coverage info
149
162
  * @returns {string}
150
163
  */
151
164
  function formatCoverage(coverage) {
@@ -164,7 +177,7 @@ function formatCoverage(coverage) {
164
177
 
165
178
  /**
166
179
  * Get overall health status with color and symbol.
167
- * @param {Object} health - Health object
180
+ * @param {HealthInfo | null} health - Health object
168
181
  * @returns {{ text: string, symbol: string, color: Function }}
169
182
  */
170
183
  function getOverallHealth(health) {
@@ -190,8 +203,8 @@ function getOverallHealth(health) {
190
203
 
191
204
  /**
192
205
  * Build the state section lines (cache, tombstones, patches).
193
- * @param {Object} status - Status object
194
- * @param {Object} gc - GC metrics
206
+ * @param {CheckStatus | null} status - Status object
207
+ * @param {GCInfo | null} gc - GC metrics
195
208
  * @returns {string[]}
196
209
  */
197
210
  function buildStateLines(status, gc) {
@@ -215,10 +228,10 @@ function buildStateLines(status, gc) {
215
228
  /**
216
229
  * Build the metadata section lines (writers, checkpoint, coverage, hooks).
217
230
  * @param {Object} opts - Metadata options
218
- * @param {Object} opts.writers - Writers info
219
- * @param {Object} opts.checkpoint - Checkpoint info
220
- * @param {Object} opts.coverage - Coverage info
221
- * @param {Object} opts.hook - Hook status
231
+ * @param {WritersInfo} opts.writers - Writers info
232
+ * @param {CheckpointInfo} opts.checkpoint - Checkpoint info
233
+ * @param {CoverageInfo} opts.coverage - Coverage info
234
+ * @param {HookInfo | null} opts.hook - Hook status
222
235
  * @returns {string[]}
223
236
  */
224
237
  function buildMetadataLines({ writers, checkpoint, coverage, hook }) {
@@ -232,7 +245,7 @@ function buildMetadataLines({ writers, checkpoint, coverage, hook }) {
232
245
 
233
246
  /**
234
247
  * Determine border color based on health status.
235
- * @param {Object} overall - Overall health info
248
+ * @param {{ text: string, symbol: string, color: Function }} overall - Overall health info
236
249
  * @returns {string}
237
250
  */
238
251
  function getBorderColor(overall) {
@@ -243,7 +256,7 @@ function getBorderColor(overall) {
243
256
 
244
257
  /**
245
258
  * Render the check view dashboard.
246
- * @param {Object} payload - The check command payload
259
+ * @param {CheckPayload} payload - The check command payload
247
260
  * @returns {string} Formatted dashboard string
248
261
  */
249
262
  export function renderCheckView(payload) {
@@ -11,6 +11,14 @@ import { createBox } from './box.js';
11
11
  import { colors } from './colors.js';
12
12
  import { ARROW } from './symbols.js';
13
13
 
14
+ /**
15
+ * @typedef {{ x: number, y: number }} Point
16
+ * @typedef {{ startPoint?: Point, endPoint?: Point, bendPoints?: Point[] }} Section
17
+ * @typedef {{ id: string, x: number, y: number, width: number, height: number, label?: string }} PositionedNode
18
+ * @typedef {{ id: string, source: string, target: string, label?: string, sections?: Section[] }} PositionedEdge
19
+ * @typedef {{ nodes: PositionedNode[], edges: PositionedEdge[], width: number, height: number }} PositionedGraph
20
+ */
21
+
14
22
  // ── Scaling constants ────────────────────────────────────────────────────────
15
23
 
16
24
  const CELL_W = 10;
@@ -30,23 +38,33 @@ const BOX = {
30
38
 
31
39
  // ── Grid helpers ─────────────────────────────────────────────────────────────
32
40
 
41
+ /** @param {number} px */
33
42
  function toCol(px) {
34
43
  return Math.round(px / CELL_W) + MARGIN;
35
44
  }
36
45
 
46
+ /** @param {number} px */
37
47
  function toRow(px) {
38
48
  return Math.round(px / CELL_H) + MARGIN;
39
49
  }
40
50
 
51
+ /** @param {number} px */
41
52
  function scaleW(px) {
42
53
  return Math.round(px / CELL_W);
43
54
  }
44
55
 
56
+ /** @param {number} px */
45
57
  function scaleH(px) {
46
58
  return Math.round(px / CELL_H);
47
59
  }
48
60
 
61
+ /**
62
+ * @param {number} rows
63
+ * @param {number} cols
64
+ * @returns {string[][]}
65
+ */
49
66
  function createGrid(rows, cols) {
67
+ /** @type {string[][]} */
50
68
  const grid = [];
51
69
  for (let r = 0; r < rows; r++) {
52
70
  grid.push(new Array(cols).fill(' '));
@@ -54,12 +72,24 @@ function createGrid(rows, cols) {
54
72
  return grid;
55
73
  }
56
74
 
75
+ /**
76
+ * @param {string[][]} grid
77
+ * @param {number} r
78
+ * @param {number} c
79
+ * @param {string} ch
80
+ */
57
81
  function writeChar(grid, r, c, ch) {
58
82
  if (r >= 0 && r < grid.length && c >= 0 && c < grid[0].length) {
59
83
  grid[r][c] = ch;
60
84
  }
61
85
  }
62
86
 
87
+ /**
88
+ * @param {string[][]} grid
89
+ * @param {number} r
90
+ * @param {number} c
91
+ * @returns {string}
92
+ */
63
93
  function readChar(grid, r, c) {
64
94
  if (r >= 0 && r < grid.length && c >= 0 && c < grid[0].length) {
65
95
  return grid[r][c];
@@ -67,6 +97,12 @@ function readChar(grid, r, c) {
67
97
  return ' ';
68
98
  }
69
99
 
100
+ /**
101
+ * @param {string[][]} grid
102
+ * @param {number} r
103
+ * @param {number} c
104
+ * @param {string} str
105
+ */
70
106
  function writeString(grid, r, c, str) {
71
107
  for (let i = 0; i < str.length; i++) {
72
108
  writeChar(grid, r, c + i, str[i]);
@@ -75,6 +111,10 @@ function writeString(grid, r, c, str) {
75
111
 
76
112
  // ── Node stamping ────────────────────────────────────────────────────────────
77
113
 
114
+ /**
115
+ * @param {string[][]} grid
116
+ * @param {PositionedNode} node
117
+ */
78
118
  function stampNode(grid, node) {
79
119
  const r = toRow(node.y);
80
120
  const c = toCol(node.x);
@@ -112,6 +152,11 @@ function stampNode(grid, node) {
112
152
 
113
153
  // ── Edge tracing ─────────────────────────────────────────────────────────────
114
154
 
155
+ /**
156
+ * @param {string[][]} grid
157
+ * @param {PositionedEdge} edge
158
+ * @param {Set<string>} nodeSet
159
+ */
115
160
  function traceEdge(grid, edge, nodeSet) {
116
161
  const { sections } = edge;
117
162
  if (!sections || sections.length === 0) {
@@ -136,6 +181,7 @@ function traceEdge(grid, edge, nodeSet) {
136
181
  }
137
182
  }
138
183
 
184
+ /** @param {Section} section @returns {Point[]} */
139
185
  function buildPointList(section) {
140
186
  const points = [];
141
187
  if (section.startPoint) {
@@ -150,6 +196,11 @@ function buildPointList(section) {
150
196
  return points;
151
197
  }
152
198
 
199
+ /**
200
+ * @param {string[][]} grid
201
+ * @param {Point[]} points
202
+ * @param {Set<string>} nodeSet
203
+ */
153
204
  function drawSegments(grid, points, nodeSet) {
154
205
  for (let i = 0; i < points.length - 1; i++) {
155
206
  const r1 = toRow(points[i].y);
@@ -160,6 +211,14 @@ function drawSegments(grid, points, nodeSet) {
160
211
  }
161
212
  }
162
213
 
214
+ /**
215
+ * @param {string[][]} grid
216
+ * @param {number} r1
217
+ * @param {number} c1
218
+ * @param {number} r2
219
+ * @param {number} c2
220
+ * @param {Set<string>} nodeSet
221
+ */
163
222
  function drawLine(grid, r1, c1, r2, c2, nodeSet) {
164
223
  if (r1 === r2) {
165
224
  drawHorizontal(grid, r1, c1, c2, nodeSet);
@@ -172,6 +231,13 @@ function drawLine(grid, r1, c1, r2, c2, nodeSet) {
172
231
  }
173
232
  }
174
233
 
234
+ /**
235
+ * @param {string[][]} grid
236
+ * @param {number} row
237
+ * @param {number} c1
238
+ * @param {number} c2
239
+ * @param {Set<string>} nodeSet
240
+ */
175
241
  function drawHorizontal(grid, row, c1, c2, nodeSet) {
176
242
  const start = Math.min(c1, c2);
177
243
  const end = Math.max(c1, c2);
@@ -187,6 +253,13 @@ function drawHorizontal(grid, row, c1, c2, nodeSet) {
187
253
  }
188
254
  }
189
255
 
256
+ /**
257
+ * @param {string[][]} grid
258
+ * @param {number} col
259
+ * @param {number} r1
260
+ * @param {number} r2
261
+ * @param {Set<string>} nodeSet
262
+ */
190
263
  function drawVertical(grid, col, r1, r2, nodeSet) {
191
264
  const start = Math.min(r1, r2);
192
265
  const end = Math.max(r1, r2);
@@ -202,6 +275,11 @@ function drawVertical(grid, col, r1, r2, nodeSet) {
202
275
  }
203
276
  }
204
277
 
278
+ /**
279
+ * @param {string[][]} grid
280
+ * @param {Section} section
281
+ * @param {Set<string>} nodeSet
282
+ */
205
283
  function drawArrowhead(grid, section, nodeSet) {
206
284
  const ep = section.endPoint;
207
285
  if (!ep) {
@@ -250,6 +328,12 @@ function drawArrowhead(grid, section, nodeSet) {
250
328
  }
251
329
  }
252
330
 
331
+ /**
332
+ * @param {string[][]} grid
333
+ * @param {Section[]} sections
334
+ * @param {string} label
335
+ * @param {Set<string>} nodeSet
336
+ */
253
337
  function placeEdgeLabel(grid, sections, label, nodeSet) {
254
338
  // Find midpoint of the full path
255
339
  const allPoints = [];
@@ -292,6 +376,7 @@ function placeEdgeLabel(grid, sections, label, nodeSet) {
292
376
 
293
377
  // ── Node occupancy set ───────────────────────────────────────────────────────
294
378
 
379
+ /** @param {PositionedNode[]} nodes @returns {Set<string>} */
295
380
  function buildNodeSet(nodes) {
296
381
  const set = new Set();
297
382
  for (const node of nodes) {
@@ -308,6 +393,12 @@ function buildNodeSet(nodes) {
308
393
  return set;
309
394
  }
310
395
 
396
+ /**
397
+ * @param {Set<string>} nodeSet
398
+ * @param {number} r
399
+ * @param {number} c
400
+ * @returns {boolean}
401
+ */
311
402
  function isNodeCell(nodeSet, r, c) {
312
403
  return nodeSet.has(`${r},${c}`);
313
404
  }
@@ -317,7 +408,7 @@ function isNodeCell(nodeSet, r, c) {
317
408
  /**
318
409
  * Renders a PositionedGraph (from ELK) as an ASCII box-drawing string.
319
410
  *
320
- * @param {Object} positionedGraph - PositionedGraph from runLayout()
411
+ * @param {PositionedGraph} positionedGraph - PositionedGraph from runLayout()
321
412
  * @param {{ title?: string }} [options]
322
413
  * @returns {string} Rendered ASCII art wrapped in a box
323
414
  */
@@ -9,13 +9,17 @@ import { padRight, padLeft } from '../../utils/unicode.js';
9
9
  import { TIMELINE } from './symbols.js';
10
10
  import { OP_DISPLAY, EMPTY_OP_SUMMARY, summarizeOps, formatOpSummary } from './opSummary.js';
11
11
 
12
+ /**
13
+ * @typedef {{ sha?: string, lamport?: number, writerId?: string, opSummary?: Record<string, number>, ops?: Array<{ type: string }> }} PatchEntry
14
+ */
15
+
12
16
  // Default pagination settings
13
17
  const DEFAULT_PAGE_SIZE = 20;
14
18
 
15
19
  /**
16
20
  * Ensures entry has an opSummary, computing one if needed.
17
- * @param {Object} entry - Patch entry
18
- * @returns {Object} Operation summary
21
+ * @param {PatchEntry} entry - Patch entry
22
+ * @returns {Record<string, number>} Operation summary
19
23
  */
20
24
  function ensureOpSummary(entry) {
21
25
  if (entry.opSummary) {
@@ -29,10 +33,10 @@ function ensureOpSummary(entry) {
29
33
 
30
34
  /**
31
35
  * Paginates entries, returning display entries and truncation info.
32
- * @param {Object[]} entries - All entries
36
+ * @param {PatchEntry[]} entries - All entries
33
37
  * @param {number} pageSize - Page size
34
38
  * @param {boolean} showAll - Whether to show all
35
- * @returns {{displayEntries: Object[], truncated: boolean, hiddenCount: number}}
39
+ * @returns {{displayEntries: PatchEntry[], truncated: boolean, hiddenCount: number}}
36
40
  */
37
41
  function paginateEntries(entries, pageSize, showAll) {
38
42
  if (showAll || entries.length <= pageSize) {
@@ -64,17 +68,22 @@ function renderTruncationIndicator(truncated, hiddenCount) {
64
68
  /**
65
69
  * Renders a single patch entry line.
66
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
67
76
  * @returns {string} Formatted entry line
68
77
  */
69
78
  function renderEntryLine({ entry, isLast, lamportWidth, writerStr, maxWriterIdLen }) {
70
79
  const connector = isLast ? TIMELINE.end : TIMELINE.connector;
71
80
  const shortSha = (entry.sha || '').slice(0, 7);
72
- const lamportStr = padLeft(String(entry.lamport), lamportWidth);
81
+ const lamportStr = padLeft(String(entry.lamport ?? 0), lamportWidth);
73
82
  const opSummary = ensureOpSummary(entry);
74
83
  const opSummaryStr = formatOpSummary(opSummary, writerStr ? 30 : 40);
75
84
 
76
85
  if (writerStr) {
77
- const paddedWriter = padRight(writerStr, maxWriterIdLen);
86
+ const paddedWriter = padRight(writerStr, maxWriterIdLen ?? 6);
78
87
  return ` ${connector}${TIMELINE.dot} ${colors.muted(`L${lamportStr}`)} ${colors.primary(paddedWriter)}:${colors.muted(shortSha)} ${opSummaryStr}`;
79
88
  }
80
89
  return ` ${connector}${TIMELINE.dot} ${colors.muted(`L${lamportStr}`)} ${colors.primary(shortSha)} ${opSummaryStr}`;
@@ -101,8 +110,8 @@ function renderSingleWriterFooter(totalCount) {
101
110
 
102
111
  /**
103
112
  * Renders single-writer timeline view.
104
- * @param {Object} payload - History payload
105
- * @param {Object} options - Rendering options
113
+ * @param {{ entries: PatchEntry[], writer: string }} payload - History payload
114
+ * @param {{ pageSize?: number, showAll?: boolean }} options - Rendering options
106
115
  * @returns {string[]} Lines for the timeline
107
116
  */
108
117
  function renderSingleWriterTimeline(payload, options) {
@@ -121,7 +130,7 @@ function renderSingleWriterTimeline(payload, options) {
121
130
  lines.push(colors.muted(' (no patches)'));
122
131
  return lines;
123
132
  }
124
- const maxLamport = Math.max(...displayEntries.map((e) => e.lamport));
133
+ const maxLamport = Math.max(...displayEntries.map((e) => e.lamport ?? 0));
125
134
  const lamportWidth = String(maxLamport).length;
126
135
 
127
136
  lines.push(...renderTruncationIndicator(truncated, hiddenCount));
@@ -137,8 +146,8 @@ function renderSingleWriterTimeline(payload, options) {
137
146
 
138
147
  /**
139
148
  * Merges and sorts entries from all writers by lamport timestamp.
140
- * @param {Object} writers - Map of writerId to entries
141
- * @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
142
151
  */
143
152
  function mergeWriterEntries(writers) {
144
153
  const allEntries = [];
@@ -147,7 +156,7 @@ function mergeWriterEntries(writers) {
147
156
  allEntries.push({ ...entry, writerId });
148
157
  }
149
158
  }
150
- 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 ?? ''));
151
160
  return allEntries;
152
161
  }
153
162
 
@@ -178,8 +187,8 @@ function renderMultiWriterFooter(totalCount, writerCount) {
178
187
 
179
188
  /**
180
189
  * Renders multi-writer timeline view with parallel columns.
181
- * @param {Object} payload - History payload with allWriters data
182
- * @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
183
192
  * @returns {string[]} Lines for the timeline
184
193
  */
185
194
  function renderMultiWriterTimeline(payload, options) {
@@ -206,7 +215,7 @@ function renderMultiWriterTimeline(payload, options) {
206
215
  lines.push(colors.muted(' (no patches)'));
207
216
  return lines;
208
217
  }
209
- const maxLamport = Math.max(...displayEntries.map((e) => e.lamport));
218
+ const maxLamport = Math.max(...displayEntries.map((e) => e.lamport ?? 0));
210
219
  const lamportWidth = String(maxLamport).length;
211
220
  const maxWriterIdLen = Math.max(...writerIds.map((id) => id.length), 6);
212
221
 
@@ -230,15 +239,8 @@ function renderMultiWriterTimeline(payload, options) {
230
239
 
231
240
  /**
232
241
  * Renders the history view with ASCII timeline.
233
- * @param {Object} payload - History payload from handleHistory
234
- * @param {string} payload.graph - Graph name
235
- * @param {string} [payload.writer] - Writer ID (single writer mode)
236
- * @param {string|null} [payload.nodeFilter] - Node filter if applied
237
- * @param {Object[]} [payload.entries] - Array of patch entries (single writer mode)
238
- * @param {Object} [payload.writers] - Map of writerId to entries (multi-writer mode)
239
- * @param {Object} [options] - Rendering options
240
- * @param {number} [options.pageSize=20] - Number of patches to show per page
241
- * @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
242
244
  * @returns {string} Formatted ASCII output
243
245
  */
244
246
  export function renderHistoryView(payload, options = {}) {
@@ -248,8 +250,8 @@ export function renderHistoryView(payload, options = {}) {
248
250
 
249
251
  const isMultiWriter = payload.writers && typeof payload.writers === 'object';
250
252
  const contentLines = isMultiWriter
251
- ? renderMultiWriterTimeline(payload, options)
252
- : renderSingleWriterTimeline(payload, options);
253
+ ? renderMultiWriterTimeline(/** @type {{ writers: Record<string, PatchEntry[]>, graph: string }} */ (payload), options)
254
+ : renderSingleWriterTimeline(/** @type {{ entries: PatchEntry[], writer: string }} */ (payload), options);
253
255
 
254
256
  // Add node filter indicator if present
255
257
  if (payload.nodeFilter) {