@git-stunts/git-warp 11.2.1 → 11.5.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 (114) hide show
  1. package/README.md +24 -1
  2. package/bin/cli/commands/check.js +2 -2
  3. package/bin/cli/commands/doctor/checks.js +12 -12
  4. package/bin/cli/commands/doctor/index.js +2 -2
  5. package/bin/cli/commands/doctor/types.js +1 -1
  6. package/bin/cli/commands/history.js +12 -5
  7. package/bin/cli/commands/install-hooks.js +5 -5
  8. package/bin/cli/commands/materialize.js +2 -2
  9. package/bin/cli/commands/patch.js +142 -0
  10. package/bin/cli/commands/path.js +4 -4
  11. package/bin/cli/commands/query.js +54 -13
  12. package/bin/cli/commands/registry.js +4 -0
  13. package/bin/cli/commands/seek.js +17 -11
  14. package/bin/cli/commands/tree.js +230 -0
  15. package/bin/cli/commands/trust.js +3 -3
  16. package/bin/cli/commands/verify-audit.js +8 -7
  17. package/bin/cli/commands/view.js +6 -5
  18. package/bin/cli/infrastructure.js +26 -12
  19. package/bin/cli/shared.js +2 -2
  20. package/bin/cli/types.js +19 -8
  21. package/bin/presenters/index.js +35 -9
  22. package/bin/presenters/json.js +14 -12
  23. package/bin/presenters/text.js +155 -33
  24. package/index.d.ts +118 -22
  25. package/index.js +2 -0
  26. package/package.json +5 -3
  27. package/src/domain/WarpGraph.js +4 -1
  28. package/src/domain/crdt/ORSet.js +8 -8
  29. package/src/domain/errors/EmptyMessageError.js +2 -2
  30. package/src/domain/errors/ForkError.js +1 -1
  31. package/src/domain/errors/IndexError.js +1 -1
  32. package/src/domain/errors/OperationAbortedError.js +1 -1
  33. package/src/domain/errors/QueryError.js +1 -1
  34. package/src/domain/errors/SchemaUnsupportedError.js +1 -1
  35. package/src/domain/errors/ShardCorruptionError.js +2 -2
  36. package/src/domain/errors/ShardLoadError.js +2 -2
  37. package/src/domain/errors/ShardValidationError.js +4 -4
  38. package/src/domain/errors/StorageError.js +2 -2
  39. package/src/domain/errors/SyncError.js +1 -1
  40. package/src/domain/errors/TraversalError.js +1 -1
  41. package/src/domain/errors/TrustError.js +1 -1
  42. package/src/domain/errors/WarpError.js +2 -2
  43. package/src/domain/errors/WormholeError.js +1 -1
  44. package/src/domain/services/AuditReceiptService.js +6 -6
  45. package/src/domain/services/AuditVerifierService.js +52 -38
  46. package/src/domain/services/BitmapIndexBuilder.js +3 -3
  47. package/src/domain/services/BitmapIndexReader.js +28 -19
  48. package/src/domain/services/BoundaryTransitionRecord.js +18 -17
  49. package/src/domain/services/CheckpointSerializerV5.js +17 -16
  50. package/src/domain/services/CheckpointService.js +22 -3
  51. package/src/domain/services/CommitDagTraversalService.js +13 -13
  52. package/src/domain/services/DagPathFinding.js +7 -7
  53. package/src/domain/services/DagTopology.js +1 -1
  54. package/src/domain/services/DagTraversal.js +1 -1
  55. package/src/domain/services/HealthCheckService.js +1 -1
  56. package/src/domain/services/HookInstaller.js +1 -1
  57. package/src/domain/services/HttpSyncServer.js +92 -41
  58. package/src/domain/services/IndexRebuildService.js +7 -7
  59. package/src/domain/services/IndexStalenessChecker.js +4 -3
  60. package/src/domain/services/JoinReducer.js +26 -11
  61. package/src/domain/services/KeyCodec.js +7 -0
  62. package/src/domain/services/LogicalTraversal.js +1 -1
  63. package/src/domain/services/MessageCodecInternal.js +1 -1
  64. package/src/domain/services/MigrationService.js +1 -1
  65. package/src/domain/services/ObserverView.js +8 -8
  66. package/src/domain/services/PatchBuilderV2.js +96 -30
  67. package/src/domain/services/ProvenanceIndex.js +1 -1
  68. package/src/domain/services/ProvenancePayload.js +1 -1
  69. package/src/domain/services/QueryBuilder.js +3 -3
  70. package/src/domain/services/StateDiff.js +14 -11
  71. package/src/domain/services/StateSerializerV5.js +2 -2
  72. package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
  73. package/src/domain/services/SyncAuthService.js +3 -2
  74. package/src/domain/services/SyncProtocol.js +25 -11
  75. package/src/domain/services/TemporalQuery.js +9 -6
  76. package/src/domain/services/TranslationCost.js +7 -5
  77. package/src/domain/services/WormholeService.js +16 -7
  78. package/src/domain/trust/TrustCanonical.js +3 -3
  79. package/src/domain/trust/TrustEvaluator.js +18 -3
  80. package/src/domain/trust/TrustRecordService.js +30 -23
  81. package/src/domain/trust/TrustStateBuilder.js +21 -8
  82. package/src/domain/trust/canonical.js +6 -6
  83. package/src/domain/types/TickReceipt.js +1 -1
  84. package/src/domain/types/WarpErrors.js +45 -0
  85. package/src/domain/types/WarpOptions.js +29 -0
  86. package/src/domain/types/WarpPersistence.js +41 -0
  87. package/src/domain/types/WarpTypes.js +2 -2
  88. package/src/domain/types/WarpTypesV2.js +2 -2
  89. package/src/domain/utils/MinHeap.js +6 -5
  90. package/src/domain/utils/canonicalStringify.js +5 -4
  91. package/src/domain/utils/roaring.js +31 -5
  92. package/src/domain/warp/PatchSession.js +40 -18
  93. package/src/domain/warp/_wiredMethods.d.ts +199 -45
  94. package/src/domain/warp/checkpoint.methods.js +5 -1
  95. package/src/domain/warp/fork.methods.js +2 -2
  96. package/src/domain/warp/materialize.methods.js +55 -5
  97. package/src/domain/warp/materializeAdvanced.methods.js +15 -4
  98. package/src/domain/warp/patch.methods.js +54 -29
  99. package/src/domain/warp/provenance.methods.js +5 -3
  100. package/src/domain/warp/query.methods.js +89 -6
  101. package/src/domain/warp/sync.methods.js +16 -11
  102. package/src/globals.d.ts +64 -0
  103. package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
  104. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
  105. package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
  106. package/src/infrastructure/adapters/GitGraphAdapter.js +18 -13
  107. package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
  108. package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
  109. package/src/visualization/layouts/converters.js +2 -2
  110. package/src/visualization/layouts/elkAdapter.js +1 -1
  111. package/src/visualization/layouts/elkLayout.js +10 -7
  112. package/src/visualization/layouts/index.js +1 -1
  113. package/src/visualization/renderers/ascii/seek.js +16 -6
  114. package/src/visualization/renderers/svg/index.js +1 -1
@@ -30,6 +30,8 @@ import {
30
30
  renderSeek,
31
31
  renderVerifyAudit,
32
32
  renderTrust,
33
+ renderPatchShow,
34
+ renderPatchList,
33
35
  } from './text.js';
34
36
 
35
37
  // ── Color control ────────────────────────────────────────────────────────────
@@ -61,8 +63,29 @@ export function shouldStripColor() {
61
63
 
62
64
  // ── Text renderer map ────────────────────────────────────────────────────────
63
65
 
64
- /** @type {Map<string, function(*): string>} */
65
- const TEXT_RENDERERS = new Map(/** @type {[string, function(*): string][]} */ ([
66
+ /** @param {import('./text.js').PatchShowPayload & Partial<import('./text.js').PatchListPayload>} payload */
67
+ function renderPatch(payload) {
68
+ if (payload.ops) {
69
+ return renderPatchShow(payload);
70
+ }
71
+ return renderPatchList(/** @type {import('./text.js').PatchListPayload} */ (payload));
72
+ }
73
+
74
+ /** @param {{ graph: string, tree?: string, orphanCount?: number, orphans?: string[] }} payload */
75
+ function renderTree(payload) {
76
+ const lines = [`Graph: ${payload.graph}`];
77
+ if (payload.tree) {
78
+ lines.push(payload.tree);
79
+ }
80
+ if (payload.orphanCount && payload.orphanCount > 0 && payload.orphans) {
81
+ lines.push('');
82
+ lines.push(`Orphans (${payload.orphanCount}): ${payload.orphans.join(', ')}`);
83
+ }
84
+ return `${lines.join('\n')}\n`;
85
+ }
86
+
87
+ /** @type {Map<string, function(unknown): string>} */
88
+ const TEXT_RENDERERS = new Map(/** @type {[string, function(unknown): string][]} */ ([
66
89
  ['info', renderInfo],
67
90
  ['query', renderQuery],
68
91
  ['path', renderPath],
@@ -73,11 +96,13 @@ const TEXT_RENDERERS = new Map(/** @type {[string, function(*): string][]} */ ([
73
96
  ['seek', renderSeek],
74
97
  ['verify-audit', renderVerifyAudit],
75
98
  ['trust', renderTrust],
99
+ ['patch', renderPatch],
100
+ ['tree', renderTree],
76
101
  ['install-hooks', renderInstallHooks],
77
102
  ]));
78
103
 
79
- /** @type {Map<string, function(*): string>} */
80
- const VIEW_RENDERERS = new Map(/** @type {[string, function(*): string][]} */ ([
104
+ /** @type {Map<string, function(unknown): string>} */
105
+ const VIEW_RENDERERS = new Map(/** @type {[string, function(unknown): string][]} */ ([
81
106
  ['info', renderInfoView],
82
107
  ['check', renderCheckView],
83
108
  ['history', renderHistoryView],
@@ -102,7 +127,7 @@ function writeHtmlExport(filePath, svgContent) {
102
127
 
103
128
  /**
104
129
  * Handles svg:PATH and html:PATH view modes for commands that carry _renderedSvg.
105
- * @param {*} payload
130
+ * @param {{ _renderedSvg?: string }} payload
106
131
  * @param {string} view
107
132
  * @returns {boolean} true if handled
108
133
  */
@@ -146,13 +171,13 @@ function writeText(text, strip) {
146
171
  /**
147
172
  * Writes a command result to stdout/stderr in the requested format.
148
173
  *
149
- * @param {*} payload - Command result payload
174
+ * @param {Record<string, unknown>} payload - Command result payload
150
175
  * @param {{format: string, command: string, view: string|null|boolean}} options
151
176
  */
152
177
  export function present(payload, { format, command, view }) {
153
178
  // Error payloads always go to stderr as plain text
154
179
  if (payload?.error) {
155
- process.stderr.write(renderError(payload));
180
+ process.stderr.write(renderError(/** @type {import('./text.js').ErrorPayload} */ (payload)));
156
181
  return;
157
182
  }
158
183
 
@@ -186,7 +211,7 @@ export function present(payload, { format, command, view }) {
186
211
 
187
212
  /**
188
213
  * Handles --view output dispatch (ASCII view, SVG file, HTML file).
189
- * @param {*} payload
214
+ * @param {Record<string, unknown>} payload
190
215
  * @param {string} command
191
216
  * @param {string|boolean} view
192
217
  */
@@ -200,7 +225,8 @@ function presentView(payload, command, view) {
200
225
 
201
226
  // query is special: uses pre-rendered _renderedAscii
202
227
  if (command === 'query') {
203
- writeText(`${payload._renderedAscii ?? ''}\n`, strip);
228
+ const ascii = typeof payload._renderedAscii === 'string' ? payload._renderedAscii : '';
229
+ writeText(`${ascii}\n`, strip);
204
230
  return;
205
231
  }
206
232
 
@@ -8,18 +8,19 @@
8
8
 
9
9
  /**
10
10
  * Recursively sorts object keys for deterministic JSON output.
11
- * @param {*} input
12
- * @returns {*}
11
+ * @param {unknown} input
12
+ * @returns {unknown}
13
13
  */
14
14
  function normalize(input) {
15
15
  if (Array.isArray(input)) {
16
16
  return input.map(normalize);
17
17
  }
18
18
  if (input && typeof input === 'object') {
19
- /** @type {Record<string, *>} */
19
+ const rec = /** @type {Record<string, unknown>} */ (input);
20
+ /** @type {Record<string, unknown>} */
20
21
  const sorted = {};
21
- for (const key of Object.keys(input).sort()) {
22
- sorted[key] = normalize(input[key]);
22
+ for (const key of Object.keys(rec).sort()) {
23
+ sorted[key] = normalize(rec[key]);
23
24
  }
24
25
  return sorted;
25
26
  }
@@ -28,7 +29,7 @@ function normalize(input) {
28
29
 
29
30
  /**
30
31
  * Pretty-printed JSON with sorted keys (2-space indent).
31
- * @param {*} value
32
+ * @param {unknown} value
32
33
  * @returns {string}
33
34
  */
34
35
  export function stableStringify(value) {
@@ -37,7 +38,7 @@ export function stableStringify(value) {
37
38
 
38
39
  /**
39
40
  * Single-line JSON with sorted keys (no indent).
40
- * @param {*} value
41
+ * @param {unknown} value
41
42
  * @returns {string}
42
43
  */
43
44
  export function compactStringify(value) {
@@ -48,18 +49,19 @@ export function compactStringify(value) {
48
49
  * Shallow-clones a payload, removing all top-level underscore-prefixed keys.
49
50
  * These are internal rendering artifacts (e.g. _renderedSvg, _renderedAscii)
50
51
  * that should not leak into JSON/NDJSON output.
51
- * @param {*} payload
52
- * @returns {*}
52
+ * @param {Record<string, unknown> | unknown} payload
53
+ * @returns {Record<string, unknown> | unknown}
53
54
  */
54
55
  export function sanitizePayload(payload) {
55
56
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
56
57
  return payload;
57
58
  }
58
- /** @type {Record<string, *>} */
59
+ const rec = /** @type {Record<string, unknown>} */ (payload);
60
+ /** @type {Record<string, unknown>} */
59
61
  const clean = {};
60
- for (const key of Object.keys(payload)) {
62
+ for (const key of Object.keys(rec)) {
61
63
  if (!key.startsWith('_')) {
62
- clean[key] = payload[key];
64
+ clean[key] = rec[key];
63
65
  }
64
66
  }
65
67
  return clean;
@@ -7,6 +7,28 @@
7
7
 
8
8
  import { formatStructuralDiff } from '../../src/visualization/renderers/ascii/seek.js';
9
9
 
10
+ // ── Payload typedefs ────────────────────────────────────────────────────────
11
+
12
+ /**
13
+ * @typedef {{ installed: boolean, foreign?: boolean, current?: boolean, version?: string }} HookStatus
14
+ * @typedef {{ repo: string, graphs: Array<{ name: string, writers?: { count: number } | null, checkpoint?: { sha: string } | null, coverage?: { sha: string } | null, cursor?: { active: boolean, tick: number, mode: string } | null }> }} InfoPayload
15
+ * @typedef {{ graph: string, stateHash?: string, nodes: Array<{ id?: string, props?: Record<string, unknown>, edges?: NodeEdges }>, _renderedAscii?: string, _renderedSvg?: string }} QueryPayload
16
+ * @typedef {{ outgoing?: Array<{ label: string, to: string }>, incoming?: Array<{ label: string, from: string }> }} NodeEdges
17
+ * @typedef {{ graph: string, from: string, to: string, found: boolean, length?: number, path?: string[] }} PathPayload
18
+ * @typedef {{ graph: string, health: { status: string }, checkpoint?: { sha: string, ageSeconds: number | null } | null, writers: { count: number, heads: Array<{ writerId: string, sha: string }> }, coverage?: { sha: string, missingWriters: string[] } | null, gc?: { totalTombstones: number, tombstoneRatio: number } | null, hook?: HookStatus | null, status?: { cachedState: string, patchesSinceCheckpoint: number, tombstoneRatio: number, writers: number } | null }} CheckPayload
19
+ * @typedef {{ graph: string, writer: string, nodeFilter?: string | null, entries: Array<{ sha: string, lamport: number, opCount: number }> }} HistoryPayload
20
+ * @typedef {{ error: { message: string } }} ErrorPayload
21
+ * @typedef {{ graphs: Array<{ graph: string, nodes?: number, edges?: number, checkpoint?: string, error?: string }> }} MaterializePayload
22
+ * @typedef {{ action: string, hookPath?: string, version?: string, backupPath?: string, name?: string }} InstallHooksPayload
23
+ * @typedef {{ graph?: string, action: string, tick?: number, maxTick?: number, ticks?: number[], nodes?: number, edges?: number, patchCount?: number, perWriter?: Record<string, unknown>, diff?: { nodes?: number, edges?: number } | null, tickReceipt?: Record<string, unknown>, structuralDiff?: import('../../src/domain/services/StateDiff.js').StateDiffResult | null, cursor?: { active: boolean, tick?: number }, message?: string, cursors?: Array<{ name: string, tick: number }>, activeTick?: number | null, name?: string, diffBaseline?: string, baselineTick?: number | null, truncated?: boolean, totalChanges?: number, shownChanges?: number }} SeekPayload
24
+ * @typedef {{ graph: string, health: string, checkedAt: string, summary: { checksRun: number, findingsTotal: number, ok: number, warn: number, fail: number, priorityActions: string[] }, findings: Array<{ status: string, id: string, message: string, fix?: string }> }} DoctorPayload
25
+ * @typedef {{ graph: string, verifiedAt: string, summary: { total: number, valid: number, partial: number, invalid: number }, chains: Array<{ writerId: string, status: string, receiptsVerified: number, since?: string, errors: Array<{ code: string, message: string }>, warnings: Array<{ code: string, message: string }> }>, trustWarning?: { message: string } }} VerifyAuditPayload
26
+ * @typedef {{ graph: string, trustVerdict: string, mode: string, trust: { source: string, evidenceSummary: { activeKeys: number, revokedKeys: number, activeBindings: number }, explanations: Array<{ trusted: boolean, writerId: string, reasonCode: string, reason: string }>, untrustedWriters: string[] } }} TrustPayload
27
+ * @typedef {{ type: string, node?: string, from?: string, to?: string, label?: string, key?: string, value?: unknown }} PatchOp
28
+ * @typedef {{ graph: string, sha: string, writer: string, lamport: number, schema?: number, ops: PatchOp[] }} PatchShowPayload
29
+ * @typedef {{ graph: string, total: number, showing: number, writerFilter?: string | null, entries: Array<{ sha: string, writer: string, lamport: number, opCount: number, nodeIds: string[] }> }} PatchListPayload
30
+ */
31
+
10
32
  // ── ANSI helpers ─────────────────────────────────────────────────────────────
11
33
 
12
34
  const ANSI_GREEN = '\x1b[32m';
@@ -26,7 +48,7 @@ function colorCachedState(state) {
26
48
  return `${ANSI_RED}${ANSI_DIM}${state}${ANSI_RESET}`;
27
49
  }
28
50
 
29
- /** @param {*} hook */
51
+ /** @param {HookStatus} hook */
30
52
  function formatHookStatusLine(hook) {
31
53
  if (!hook.installed && hook.foreign) {
32
54
  return "Hook: foreign hook present — run 'git warp install-hooks'";
@@ -42,7 +64,7 @@ function formatHookStatusLine(hook) {
42
64
 
43
65
  // ── Simple renderers ─────────────────────────────────────────────────────────
44
66
 
45
- /** @param {*} payload */
67
+ /** @param {InfoPayload} payload */
46
68
  export function renderInfo(payload) {
47
69
  const lines = [`Repo: ${payload.repo}`];
48
70
  lines.push(`Graphs: ${payload.graphs.length}`);
@@ -62,7 +84,25 @@ export function renderInfo(payload) {
62
84
  return `${lines.join('\n')}\n`;
63
85
  }
64
86
 
65
- /** @param {*} payload */
87
+ /**
88
+ * Appends edge lines for a single node to the output array.
89
+ * @param {string[]} lines
90
+ * @param {NodeEdges} edges
91
+ */
92
+ function appendNodeEdges(lines, edges) {
93
+ if (edges.outgoing && edges.outgoing.length > 0) {
94
+ for (const e of edges.outgoing) {
95
+ lines.push(` -> ${e.label} -> ${e.to}`);
96
+ }
97
+ }
98
+ if (edges.incoming && edges.incoming.length > 0) {
99
+ for (const e of edges.incoming) {
100
+ lines.push(` <- ${e.label} <- ${e.from}`);
101
+ }
102
+ }
103
+ }
104
+
105
+ /** @param {QueryPayload} payload */
66
106
  export function renderQuery(payload) {
67
107
  const lines = [
68
108
  `Graph: ${payload.graph}`,
@@ -76,21 +116,27 @@ export function renderQuery(payload) {
76
116
  if (node.props && Object.keys(node.props).length > 0) {
77
117
  lines.push(` props: ${JSON.stringify(node.props)}`);
78
118
  }
119
+ if (node.edges) {
120
+ appendNodeEdges(lines, node.edges);
121
+ }
79
122
  }
80
123
 
81
124
  return `${lines.join('\n')}\n`;
82
125
  }
83
126
 
84
- /** @param {*} payload */
127
+ /** @param {PathPayload} payload */
85
128
  export function renderPath(payload) {
86
129
  const lines = [
87
130
  `Graph: ${payload.graph}`,
88
131
  `From: ${payload.from}`,
89
132
  `To: ${payload.to}`,
90
133
  `Found: ${payload.found ? 'yes' : 'no'}`,
91
- `Length: ${payload.length}`,
92
134
  ];
93
135
 
136
+ if (payload.found) {
137
+ lines.push(`Length: ${payload.length}`);
138
+ }
139
+
94
140
  if (payload.path && payload.path.length > 0) {
95
141
  lines.push(`Path: ${payload.path.join(' -> ')}`);
96
142
  }
@@ -101,7 +147,7 @@ export function renderPath(payload) {
101
147
  /**
102
148
  * Appends checkpoint and writer lines to check output.
103
149
  * @param {string[]} lines
104
- * @param {*} payload
150
+ * @param {CheckPayload} payload
105
151
  */
106
152
  function appendCheckpointAndWriters(lines, payload) {
107
153
  if (payload.checkpoint?.sha) {
@@ -124,7 +170,7 @@ function appendCheckpointAndWriters(lines, payload) {
124
170
  /**
125
171
  * Appends coverage, gc, and hook lines to check output.
126
172
  * @param {string[]} lines
127
- * @param {*} payload
173
+ * @param {CheckPayload} payload
128
174
  */
129
175
  function appendCoverageAndExtras(lines, payload) {
130
176
  if (payload.coverage?.sha) {
@@ -146,7 +192,7 @@ function appendCoverageAndExtras(lines, payload) {
146
192
  }
147
193
  }
148
194
 
149
- /** @param {*} payload */
195
+ /** @param {CheckPayload} payload */
150
196
  export function renderCheck(payload) {
151
197
  const lines = [
152
198
  `Graph: ${payload.graph}`,
@@ -165,7 +211,7 @@ export function renderCheck(payload) {
165
211
  return `${lines.join('\n')}\n`;
166
212
  }
167
213
 
168
- /** @param {*} payload */
214
+ /** @param {HistoryPayload} payload */
169
215
  export function renderHistory(payload) {
170
216
  const lines = [
171
217
  `Graph: ${payload.graph}`,
@@ -184,12 +230,12 @@ export function renderHistory(payload) {
184
230
  return `${lines.join('\n')}\n`;
185
231
  }
186
232
 
187
- /** @param {*} payload */
233
+ /** @param {ErrorPayload} payload */
188
234
  export function renderError(payload) {
189
235
  return `Error: ${payload.error.message}\n`;
190
236
  }
191
237
 
192
- /** @param {*} payload */
238
+ /** @param {MaterializePayload} payload */
193
239
  export function renderMaterialize(payload) {
194
240
  if (payload.graphs.length === 0) {
195
241
  return 'No graphs found in repo.\n';
@@ -206,7 +252,7 @@ export function renderMaterialize(payload) {
206
252
  return `${lines.join('\n')}\n`;
207
253
  }
208
254
 
209
- /** @param {*} payload */
255
+ /** @param {InstallHooksPayload} payload */
210
256
  export function renderInstallHooks(payload) {
211
257
  if (payload.action === 'up-to-date') {
212
258
  return `Hook: already up to date (v${payload.version}) at ${payload.hookPath}\n`;
@@ -225,10 +271,10 @@ export function renderInstallHooks(payload) {
225
271
 
226
272
  /**
227
273
  * Formats a numeric delta as " (+N)" or " (-N)", or empty string for zero/non-finite.
228
- * @param {*} n
274
+ * @param {unknown} n
229
275
  * @returns {string}
230
276
  */
231
- function formatDelta(n) { // TODO(ts-cleanup): type CLI payload
277
+ function formatDelta(n) {
232
278
  if (typeof n !== 'number' || !Number.isFinite(n) || n === 0) {
233
279
  return '';
234
280
  }
@@ -238,10 +284,10 @@ function formatDelta(n) { // TODO(ts-cleanup): type CLI payload
238
284
 
239
285
  /**
240
286
  * Formats an operation summary object as a compact plain-text string.
241
- * @param {*} summary
287
+ * @param {Record<string, number> | null | undefined} summary
242
288
  * @returns {string}
243
289
  */
244
- function formatOpSummaryPlain(summary) { // TODO(ts-cleanup): type CLI payload
290
+ function formatOpSummaryPlain(summary) {
245
291
  const order = [
246
292
  ['NodeAdd', '+', 'node'],
247
293
  ['EdgeAdd', '+', 'edge'],
@@ -264,7 +310,7 @@ function formatOpSummaryPlain(summary) { // TODO(ts-cleanup): type CLI payload
264
310
  /**
265
311
  * Appends a per-writer tick receipt summary below a base line.
266
312
  * @param {string} baseLine
267
- * @param {*} payload
313
+ * @param {SeekPayload} payload
268
314
  * @returns {string}
269
315
  */
270
316
  function appendReceiptSummary(baseLine, payload) {
@@ -284,8 +330,12 @@ function appendReceiptSummary(baseLine, payload) {
284
330
  const maxWriterLen = Math.max(5, ...entries.map(([writerId]) => writerId.length));
285
331
  const receiptLines = [` Tick ${payload.tick}:`];
286
332
  for (const [writerId, entry] of entries) {
287
- const sha = typeof entry.sha === 'string' ? entry.sha.slice(0, 7) : '';
288
- const opSummary = entry.opSummary && typeof entry.opSummary === 'object' ? entry.opSummary : entry;
333
+ /** @type {Record<string, unknown>} */
334
+ const rec = /** @type {Record<string, unknown>} */ (entry);
335
+ const sha = typeof rec.sha === 'string' ? rec.sha.slice(0, 7) : '';
336
+ const opSummary = rec.opSummary && typeof rec.opSummary === 'object'
337
+ ? /** @type {Record<string, number>} */ (rec.opSummary)
338
+ : /** @type {Record<string, number>} */ (rec);
289
339
  receiptLines.push(` ${writerId.padEnd(maxWriterLen)} ${sha.padEnd(7)} ${formatOpSummaryPlain(opSummary)}`);
290
340
  }
291
341
 
@@ -294,7 +344,7 @@ function appendReceiptSummary(baseLine, payload) {
294
344
 
295
345
  /**
296
346
  * Builds human-readable state count strings from a seek payload.
297
- * @param {*} payload
347
+ * @param {SeekPayload} payload
298
348
  * @returns {{nodesStr: string, edgesStr: string, patchesStr: string}}
299
349
  */
300
350
  function buildStateStrings(payload) {
@@ -310,20 +360,20 @@ function buildStateStrings(payload) {
310
360
 
311
361
  /**
312
362
  * Renders the "tick" / "latest" / "load" seek action with receipt + structural diff.
313
- * @param {*} payload
363
+ * @param {SeekPayload} payload
314
364
  * @param {string} headerLine
315
365
  * @returns {string}
316
366
  */
317
367
  function renderSeekWithDiff(payload, headerLine) {
318
368
  const base = appendReceiptSummary(headerLine, payload);
319
- return base + formatStructuralDiff(payload);
369
+ return base + formatStructuralDiff(/** @type {import('../../src/visualization/renderers/ascii/seek.js').SeekPayload} */ (payload));
320
370
  }
321
371
 
322
372
  // ── Seek simple-action renderers ─────────────────────────────────────────────
323
373
 
324
374
  /**
325
375
  * Renders seek actions that don't involve state counts: clear-cache, list, drop, save.
326
- * @param {*} payload
376
+ * @param {SeekPayload} payload
327
377
  * @returns {string|null} Rendered string, or null if action is not simple
328
378
  */
329
379
  function renderSeekSimple(payload) {
@@ -344,11 +394,11 @@ function renderSeekSimple(payload) {
344
394
 
345
395
  /**
346
396
  * Renders the cursor list action.
347
- * @param {*} payload
397
+ * @param {SeekPayload} payload
348
398
  * @returns {string}
349
399
  */
350
400
  function renderSeekList(payload) {
351
- if (payload.cursors.length === 0) {
401
+ if (!payload.cursors || payload.cursors.length === 0) {
352
402
  return 'No saved cursors.\n';
353
403
  }
354
404
  const lines = [];
@@ -363,7 +413,7 @@ function renderSeekList(payload) {
363
413
 
364
414
  /**
365
415
  * Renders seek actions that show state: latest, load, tick, status.
366
- * @param {*} payload
416
+ * @param {SeekPayload} payload
367
417
  * @returns {string}
368
418
  */
369
419
  function renderSeekState(payload) {
@@ -396,19 +446,19 @@ function renderSeekState(payload) {
396
446
  payload,
397
447
  );
398
448
  }
399
- return `${payload.graph}: no cursor active, ${payload.ticks.length} ticks available\n`;
449
+ return `${payload.graph}: no cursor active, ${(payload.ticks ?? []).length} ticks available\n`;
400
450
  }
401
451
 
402
452
  // ── Seek main renderer ──────────────────────────────────────────────────────
403
453
 
404
- /** @param {*} payload */
454
+ /** @param {SeekPayload} payload */
405
455
  export function renderSeek(payload) {
406
456
  return renderSeekSimple(payload) ?? renderSeekState(payload);
407
457
  }
408
458
 
409
459
  // ── Doctor renderer ──────────────────────────────────────────────────────────
410
460
 
411
- /** @param {'ok'|'warn'|'fail'} status */
461
+ /** @param {string} status */
412
462
  function findingIcon(status) {
413
463
  if (status === 'ok') {
414
464
  return `${ANSI_GREEN}\u2713${ANSI_RESET}`;
@@ -419,7 +469,7 @@ function findingIcon(status) {
419
469
  return `${ANSI_RED}\u2717${ANSI_RESET}`;
420
470
  }
421
471
 
422
- /** @param {'ok'|'degraded'|'failed'} health */
472
+ /** @param {string} health */
423
473
  function colorHealth(health) {
424
474
  if (health === 'ok') {
425
475
  return `${ANSI_GREEN}${health}${ANSI_RESET}`;
@@ -430,7 +480,7 @@ function colorHealth(health) {
430
480
  return `${ANSI_RED}${health}${ANSI_RESET}`;
431
481
  }
432
482
 
433
- /** @param {*} payload */
483
+ /** @param {DoctorPayload} payload */
434
484
  export function renderDoctor(payload) {
435
485
  const lines = [
436
486
  `Graph: ${payload.graph}`,
@@ -468,7 +518,7 @@ function colorStatus(status) {
468
518
  return `${ANSI_RED}${status}${ANSI_RESET}`;
469
519
  }
470
520
 
471
- /** @param {*} payload */
521
+ /** @param {VerifyAuditPayload} payload */
472
522
  export function renderVerifyAudit(payload) {
473
523
  const lines = [
474
524
  `Graph: ${payload.graph}`,
@@ -513,7 +563,7 @@ function colorVerdict(verdict) {
513
563
  return `${ANSI_RED}${verdict}${ANSI_RESET}`;
514
564
  }
515
565
 
516
- /** @param {*} payload */
566
+ /** @param {TrustPayload} payload */
517
567
  export function renderTrust(payload) {
518
568
  const lines = [
519
569
  `Graph: ${payload.graph}`,
@@ -541,3 +591,75 @@ export function renderTrust(payload) {
541
591
 
542
592
  return `${lines.join('\n')}\n`;
543
593
  }
594
+
595
+ // ── Patch renderers ──────────────────────────────────────────────────────────
596
+
597
+ /**
598
+ * Formats a single operation line for patch show output.
599
+ * @param {PatchOp} op
600
+ * @returns {string|null}
601
+ */
602
+ function formatPatchOp(op) {
603
+ if (op.type === 'NodeAdd') {
604
+ return ` + node ${op.node}`;
605
+ }
606
+ if (op.type === 'NodeTombstone') {
607
+ return ` - node ${op.node}`;
608
+ }
609
+ if (op.type === 'EdgeAdd') {
610
+ return ` + edge ${op.from} -[${op.label}]-> ${op.to}`;
611
+ }
612
+ if (op.type === 'EdgeTombstone') {
613
+ return ` - edge ${op.from} -[${op.label}]-> ${op.to}`;
614
+ }
615
+ if (op.type === 'PropSet') {
616
+ return ` ~ ${op.node}.${op.key} = ${JSON.stringify(op.value)}`;
617
+ }
618
+ if (op.type === 'BlobValue') {
619
+ return ` + blob ${op.node}`;
620
+ }
621
+ return null;
622
+ }
623
+
624
+ /** @param {PatchShowPayload} payload */
625
+ export function renderPatchShow(payload) {
626
+ const lines = [
627
+ `Graph: ${payload.graph}`,
628
+ `SHA: ${payload.sha}`,
629
+ `Writer: ${payload.writer}`,
630
+ `Lamport: ${payload.lamport}`,
631
+ `Schema: ${payload.schema}`,
632
+ `Operations: ${payload.ops.length}`,
633
+ '',
634
+ ];
635
+
636
+ for (const op of payload.ops) {
637
+ const line = formatPatchOp(op);
638
+ if (line) {
639
+ lines.push(line);
640
+ }
641
+ }
642
+
643
+ return `${lines.join('\n')}\n`;
644
+ }
645
+
646
+ /** @param {PatchListPayload} payload */
647
+ export function renderPatchList(payload) {
648
+ const lines = [
649
+ `Graph: ${payload.graph}`,
650
+ `Patches: ${payload.showing}/${payload.total}`,
651
+ ];
652
+
653
+ if (payload.writerFilter) {
654
+ lines.push(`Writer: ${payload.writerFilter}`);
655
+ }
656
+
657
+ lines.push('');
658
+
659
+ for (const entry of payload.entries) {
660
+ const nodes = entry.nodeIds.length > 0 ? ` [${entry.nodeIds.join(', ')}]` : '';
661
+ lines.push(` ${entry.sha} L${String(entry.lamport).padStart(3)} ${entry.writer.padEnd(20)} ${entry.opCount} ops${nodes}`);
662
+ }
663
+
664
+ return `${lines.join('\n')}\n`;
665
+ }