@git-stunts/git-warp 10.8.0 → 11.3.3

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 (136) hide show
  1. package/README.md +53 -32
  2. package/SECURITY.md +64 -0
  3. package/bin/cli/commands/check.js +168 -0
  4. package/bin/cli/commands/doctor/checks.js +422 -0
  5. package/bin/cli/commands/doctor/codes.js +46 -0
  6. package/bin/cli/commands/doctor/index.js +239 -0
  7. package/bin/cli/commands/doctor/types.js +89 -0
  8. package/bin/cli/commands/history.js +80 -0
  9. package/bin/cli/commands/info.js +139 -0
  10. package/bin/cli/commands/install-hooks.js +128 -0
  11. package/bin/cli/commands/materialize.js +99 -0
  12. package/bin/cli/commands/patch.js +142 -0
  13. package/bin/cli/commands/path.js +88 -0
  14. package/bin/cli/commands/query.js +235 -0
  15. package/bin/cli/commands/registry.js +32 -0
  16. package/bin/cli/commands/seek.js +598 -0
  17. package/bin/cli/commands/tree.js +230 -0
  18. package/bin/cli/commands/trust.js +154 -0
  19. package/bin/cli/commands/verify-audit.js +114 -0
  20. package/bin/cli/commands/view.js +46 -0
  21. package/bin/cli/infrastructure.js +350 -0
  22. package/bin/cli/schemas.js +177 -0
  23. package/bin/cli/shared.js +244 -0
  24. package/bin/cli/types.js +96 -0
  25. package/bin/presenters/index.js +41 -9
  26. package/bin/presenters/json.js +14 -12
  27. package/bin/presenters/text.js +286 -28
  28. package/bin/warp-graph.js +5 -2346
  29. package/index.d.ts +111 -21
  30. package/index.js +2 -0
  31. package/package.json +10 -8
  32. package/src/domain/WarpGraph.js +109 -3252
  33. package/src/domain/crdt/ORSet.js +8 -8
  34. package/src/domain/errors/EmptyMessageError.js +2 -2
  35. package/src/domain/errors/ForkError.js +1 -1
  36. package/src/domain/errors/IndexError.js +1 -1
  37. package/src/domain/errors/OperationAbortedError.js +1 -1
  38. package/src/domain/errors/QueryError.js +3 -3
  39. package/src/domain/errors/SchemaUnsupportedError.js +1 -1
  40. package/src/domain/errors/ShardCorruptionError.js +2 -2
  41. package/src/domain/errors/ShardLoadError.js +2 -2
  42. package/src/domain/errors/ShardValidationError.js +4 -4
  43. package/src/domain/errors/StorageError.js +2 -2
  44. package/src/domain/errors/SyncError.js +1 -1
  45. package/src/domain/errors/TraversalError.js +1 -1
  46. package/src/domain/errors/TrustError.js +29 -0
  47. package/src/domain/errors/WarpError.js +2 -2
  48. package/src/domain/errors/WormholeError.js +1 -1
  49. package/src/domain/errors/index.js +1 -0
  50. package/src/domain/services/AuditMessageCodec.js +137 -0
  51. package/src/domain/services/AuditReceiptService.js +471 -0
  52. package/src/domain/services/AuditVerifierService.js +707 -0
  53. package/src/domain/services/BitmapIndexBuilder.js +3 -3
  54. package/src/domain/services/BitmapIndexReader.js +28 -19
  55. package/src/domain/services/BoundaryTransitionRecord.js +18 -17
  56. package/src/domain/services/CheckpointSerializerV5.js +17 -16
  57. package/src/domain/services/CheckpointService.js +2 -2
  58. package/src/domain/services/CommitDagTraversalService.js +13 -13
  59. package/src/domain/services/DagPathFinding.js +7 -7
  60. package/src/domain/services/DagTopology.js +1 -1
  61. package/src/domain/services/DagTraversal.js +1 -1
  62. package/src/domain/services/HealthCheckService.js +1 -1
  63. package/src/domain/services/HookInstaller.js +1 -1
  64. package/src/domain/services/HttpSyncServer.js +120 -55
  65. package/src/domain/services/IndexRebuildService.js +7 -7
  66. package/src/domain/services/IndexStalenessChecker.js +4 -3
  67. package/src/domain/services/JoinReducer.js +11 -11
  68. package/src/domain/services/LogicalTraversal.js +1 -1
  69. package/src/domain/services/MessageCodecInternal.js +4 -1
  70. package/src/domain/services/MessageSchemaDetector.js +2 -2
  71. package/src/domain/services/MigrationService.js +1 -1
  72. package/src/domain/services/ObserverView.js +8 -8
  73. package/src/domain/services/PatchBuilderV2.js +42 -26
  74. package/src/domain/services/ProvenanceIndex.js +1 -1
  75. package/src/domain/services/ProvenancePayload.js +1 -1
  76. package/src/domain/services/QueryBuilder.js +3 -3
  77. package/src/domain/services/StateDiff.js +14 -11
  78. package/src/domain/services/StateSerializerV5.js +2 -2
  79. package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
  80. package/src/domain/services/SyncAuthService.js +71 -4
  81. package/src/domain/services/SyncProtocol.js +25 -11
  82. package/src/domain/services/TemporalQuery.js +9 -6
  83. package/src/domain/services/TranslationCost.js +7 -5
  84. package/src/domain/services/WarpMessageCodec.js +4 -1
  85. package/src/domain/services/WormholeService.js +16 -7
  86. package/src/domain/trust/TrustCanonical.js +42 -0
  87. package/src/domain/trust/TrustCrypto.js +111 -0
  88. package/src/domain/trust/TrustEvaluator.js +195 -0
  89. package/src/domain/trust/TrustRecordService.js +281 -0
  90. package/src/domain/trust/TrustStateBuilder.js +222 -0
  91. package/src/domain/trust/canonical.js +68 -0
  92. package/src/domain/trust/reasonCodes.js +64 -0
  93. package/src/domain/trust/schemas.js +160 -0
  94. package/src/domain/trust/verdict.js +42 -0
  95. package/src/domain/types/TickReceipt.js +1 -1
  96. package/src/domain/types/WarpErrors.js +45 -0
  97. package/src/domain/types/WarpOptions.js +29 -0
  98. package/src/domain/types/WarpPersistence.js +41 -0
  99. package/src/domain/types/WarpTypes.js +2 -2
  100. package/src/domain/types/WarpTypesV2.js +2 -2
  101. package/src/domain/types/git-cas.d.ts +20 -0
  102. package/src/domain/utils/MinHeap.js +6 -5
  103. package/src/domain/utils/RefLayout.js +59 -0
  104. package/src/domain/utils/canonicalStringify.js +5 -4
  105. package/src/domain/utils/roaring.js +31 -5
  106. package/src/domain/warp/PatchSession.js +26 -17
  107. package/src/domain/warp/Writer.js +18 -3
  108. package/src/domain/warp/_internal.js +26 -0
  109. package/src/domain/warp/_wire.js +58 -0
  110. package/src/domain/warp/_wiredMethods.d.ts +254 -0
  111. package/src/domain/warp/checkpoint.methods.js +401 -0
  112. package/src/domain/warp/fork.methods.js +323 -0
  113. package/src/domain/warp/materialize.methods.js +238 -0
  114. package/src/domain/warp/materializeAdvanced.methods.js +350 -0
  115. package/src/domain/warp/patch.methods.js +554 -0
  116. package/src/domain/warp/provenance.methods.js +286 -0
  117. package/src/domain/warp/query.methods.js +280 -0
  118. package/src/domain/warp/subscribe.methods.js +272 -0
  119. package/src/domain/warp/sync.methods.js +554 -0
  120. package/src/globals.d.ts +64 -0
  121. package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
  122. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
  123. package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
  124. package/src/infrastructure/adapters/GitGraphAdapter.js +79 -11
  125. package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
  126. package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
  127. package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
  128. package/src/ports/CommitPort.js +10 -0
  129. package/src/ports/RefPort.js +17 -0
  130. package/src/visualization/layouts/converters.js +2 -2
  131. package/src/visualization/layouts/elkAdapter.js +1 -1
  132. package/src/visualization/layouts/elkLayout.js +10 -7
  133. package/src/visualization/layouts/index.js +1 -1
  134. package/src/visualization/renderers/ascii/seek.js +16 -6
  135. package/src/visualization/renderers/svg/index.js +1 -1
  136. package/src/hooks/post-merge.sh +0 -60
@@ -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,12 +446,220 @@ 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
  }
458
+
459
+ // ── Doctor renderer ──────────────────────────────────────────────────────────
460
+
461
+ /** @param {string} status */
462
+ function findingIcon(status) {
463
+ if (status === 'ok') {
464
+ return `${ANSI_GREEN}\u2713${ANSI_RESET}`;
465
+ }
466
+ if (status === 'warn') {
467
+ return `${ANSI_YELLOW}\u26A0${ANSI_RESET}`;
468
+ }
469
+ return `${ANSI_RED}\u2717${ANSI_RESET}`;
470
+ }
471
+
472
+ /** @param {string} health */
473
+ function colorHealth(health) {
474
+ if (health === 'ok') {
475
+ return `${ANSI_GREEN}${health}${ANSI_RESET}`;
476
+ }
477
+ if (health === 'degraded') {
478
+ return `${ANSI_YELLOW}${health}${ANSI_RESET}`;
479
+ }
480
+ return `${ANSI_RED}${health}${ANSI_RESET}`;
481
+ }
482
+
483
+ /** @param {DoctorPayload} payload */
484
+ export function renderDoctor(payload) {
485
+ const lines = [
486
+ `Graph: ${payload.graph}`,
487
+ `Health: ${colorHealth(payload.health)}`,
488
+ `Checked: ${payload.checkedAt}`,
489
+ `Summary: ${payload.summary.checksRun} checks, ${payload.summary.findingsTotal} findings (${payload.summary.ok} ok, ${payload.summary.warn} warn, ${payload.summary.fail} fail)`,
490
+ '',
491
+ ];
492
+
493
+ for (const f of payload.findings) {
494
+ lines.push(`${findingIcon(f.status)} ${f.id}: ${f.message}`);
495
+ if (f.fix) {
496
+ lines.push(` fix: ${f.fix}`);
497
+ }
498
+ }
499
+
500
+ if (payload.summary.priorityActions.length > 0) {
501
+ lines.push('');
502
+ lines.push('Priority actions:');
503
+ for (const action of payload.summary.priorityActions) {
504
+ lines.push(` - ${action}`);
505
+ }
506
+ }
507
+
508
+ return `${lines.join('\n')}\n`;
509
+ }
510
+
511
+ // ── Verify-audit renderer ────────────────────────────────────────────────────
512
+
513
+ /** @param {string} status */
514
+ function colorStatus(status) {
515
+ if (status === 'VALID' || status === 'PARTIAL') {
516
+ return `${ANSI_GREEN}${status}${ANSI_RESET}`;
517
+ }
518
+ return `${ANSI_RED}${status}${ANSI_RESET}`;
519
+ }
520
+
521
+ /** @param {VerifyAuditPayload} payload */
522
+ export function renderVerifyAudit(payload) {
523
+ const lines = [
524
+ `Graph: ${payload.graph}`,
525
+ `Verified: ${payload.verifiedAt}`,
526
+ `Chains: ${payload.summary.total} (${payload.summary.valid} valid, ${payload.summary.partial} partial, ${payload.summary.invalid} invalid)`,
527
+ ];
528
+
529
+ for (const chain of payload.chains) {
530
+ lines.push('');
531
+ lines.push(` Writer: ${chain.writerId}`);
532
+ lines.push(` Status: ${colorStatus(chain.status)}`);
533
+ lines.push(` Receipts: ${chain.receiptsVerified} verified`);
534
+ if (chain.since) {
535
+ lines.push(` Since: ${chain.since}`);
536
+ }
537
+ for (const err of chain.errors) {
538
+ lines.push(` ${ANSI_RED}Error [${err.code}]: ${err.message}${ANSI_RESET}`);
539
+ }
540
+ for (const warn of chain.warnings) {
541
+ lines.push(` ${ANSI_YELLOW}Warning [${warn.code}]: ${warn.message}${ANSI_RESET}`);
542
+ }
543
+ }
544
+
545
+ if (payload.trustWarning) {
546
+ lines.push('');
547
+ lines.push(`${ANSI_YELLOW}Trust: ${payload.trustWarning.message}${ANSI_RESET}`);
548
+ }
549
+
550
+ return `${lines.join('\n')}\n`;
551
+ }
552
+
553
+ // ── Trust renderer ────────────────────────────────────────────────────────
554
+
555
+ /** @param {string} verdict */
556
+ function colorVerdict(verdict) {
557
+ if (verdict === 'pass') {
558
+ return `${ANSI_GREEN}${verdict}${ANSI_RESET}`;
559
+ }
560
+ if (verdict === 'not_configured') {
561
+ return `${ANSI_YELLOW}${verdict}${ANSI_RESET}`;
562
+ }
563
+ return `${ANSI_RED}${verdict}${ANSI_RESET}`;
564
+ }
565
+
566
+ /** @param {TrustPayload} payload */
567
+ export function renderTrust(payload) {
568
+ const lines = [
569
+ `Graph: ${payload.graph}`,
570
+ `Verdict: ${colorVerdict(payload.trustVerdict)}`,
571
+ `Mode: ${payload.mode}`,
572
+ `Source: ${payload.trust.source}`,
573
+ ];
574
+
575
+ const { evidenceSummary } = payload.trust;
576
+ lines.push(`Evidence: ${evidenceSummary.activeKeys} active keys, ${evidenceSummary.revokedKeys} revoked keys, ${evidenceSummary.activeBindings} active bindings`);
577
+
578
+ if (payload.trust.explanations.length > 0) {
579
+ lines.push('');
580
+ for (const expl of payload.trust.explanations) {
581
+ const icon = expl.trusted ? `${ANSI_GREEN}\u2713${ANSI_RESET}` : `${ANSI_RED}\u2717${ANSI_RESET}`;
582
+ lines.push(` ${icon} ${expl.writerId}: ${expl.reasonCode}`);
583
+ lines.push(` ${ANSI_DIM}${expl.reason}${ANSI_RESET}`);
584
+ }
585
+ }
586
+
587
+ if (payload.trust.untrustedWriters.length > 0) {
588
+ lines.push('');
589
+ lines.push(`${ANSI_RED}Untrusted: ${payload.trust.untrustedWriters.join(', ')}${ANSI_RESET}`);
590
+ }
591
+
592
+ return `${lines.join('\n')}\n`;
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
+ }