@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
@@ -0,0 +1,244 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ import readline from 'node:readline';
5
+ import { execFileSync } from 'node:child_process';
6
+ // @ts-expect-error — no type declarations for @git-stunts/plumbing
7
+ import GitPlumbing, { ShellRunnerFactory } from '@git-stunts/plumbing';
8
+ import WarpGraph from '../../src/domain/WarpGraph.js';
9
+ import GitGraphAdapter from '../../src/infrastructure/adapters/GitGraphAdapter.js';
10
+ import WebCryptoAdapter from '../../src/infrastructure/adapters/WebCryptoAdapter.js';
11
+ import {
12
+ REF_PREFIX,
13
+ buildCursorActiveRef,
14
+ } from '../../src/domain/utils/RefLayout.js';
15
+ import CasSeekCacheAdapter from '../../src/infrastructure/adapters/CasSeekCacheAdapter.js';
16
+ import { HookInstaller } from '../../src/domain/services/HookInstaller.js';
17
+ import { parseCursorBlob } from '../../src/domain/utils/parseCursorBlob.js';
18
+ import { usageError, notFoundError } from './infrastructure.js';
19
+
20
+ /** @typedef {import('./types.js').Persistence} Persistence */
21
+ /** @typedef {import('./types.js').WarpGraphInstance} WarpGraphInstance */
22
+ /** @typedef {import('./types.js').CursorBlob} CursorBlob */
23
+ /** @typedef {import('./types.js').CliOptions} CliOptions */
24
+ /** @typedef {import('./types.js').SeekSpec} SeekSpec */
25
+
26
+ /** @param {string} repoPath @returns {Promise<{persistence: Persistence}>} */
27
+ export async function createPersistence(repoPath) {
28
+ const runner = ShellRunnerFactory.create();
29
+ const plumbing = new GitPlumbing({ cwd: repoPath, runner });
30
+ const persistence = new GitGraphAdapter({ plumbing });
31
+ const ping = await persistence.ping();
32
+ if (!ping.ok) {
33
+ throw usageError(`Repository not accessible: ${repoPath}`);
34
+ }
35
+ return { persistence };
36
+ }
37
+
38
+ /** @param {Persistence} persistence @returns {Promise<string[]>} */
39
+ export async function listGraphNames(persistence) {
40
+ if (typeof persistence.listRefs !== 'function') {
41
+ return [];
42
+ }
43
+ const refs = await persistence.listRefs(REF_PREFIX);
44
+ const prefix = `${REF_PREFIX}/`;
45
+ const names = new Set();
46
+
47
+ for (const ref of refs) {
48
+ if (!ref.startsWith(prefix)) {
49
+ continue;
50
+ }
51
+ const rest = ref.slice(prefix.length);
52
+ const [graphName] = rest.split('/');
53
+ if (graphName) {
54
+ names.add(graphName);
55
+ }
56
+ }
57
+
58
+ return [...names].sort();
59
+ }
60
+
61
+ /**
62
+ * @param {Persistence} persistence
63
+ * @param {string|null} explicitGraph
64
+ * @returns {Promise<string>}
65
+ */
66
+ export async function resolveGraphName(persistence, explicitGraph) {
67
+ if (explicitGraph) {
68
+ return explicitGraph;
69
+ }
70
+ const graphNames = await listGraphNames(persistence);
71
+ if (graphNames.length === 1) {
72
+ return graphNames[0];
73
+ }
74
+ if (graphNames.length === 0) {
75
+ throw notFoundError('No graphs found in repo; specify --graph');
76
+ }
77
+ throw usageError('Multiple graphs found; specify --graph');
78
+ }
79
+
80
+ /**
81
+ * Opens a WarpGraph for the given CLI options.
82
+ * @param {CliOptions} options - Parsed CLI options
83
+ * @returns {Promise<{graph: WarpGraphInstance, graphName: string, persistence: Persistence}>}
84
+ * @throws {import('./infrastructure.js').CliError} If the specified graph is not found
85
+ */
86
+ export async function openGraph(options) {
87
+ const { persistence } = await createPersistence(options.repo);
88
+ const graphName = await resolveGraphName(persistence, options.graph);
89
+ if (options.graph) {
90
+ const graphNames = await listGraphNames(persistence);
91
+ if (!graphNames.includes(options.graph)) {
92
+ throw notFoundError(`Graph not found: ${options.graph}`);
93
+ }
94
+ }
95
+ const graph = /** @type {WarpGraphInstance} */ (/** @type {unknown} */ (await WarpGraph.open({
96
+ persistence,
97
+ graphName,
98
+ writerId: options.writer,
99
+ crypto: new WebCryptoAdapter(),
100
+ })));
101
+ return { graph, graphName, persistence };
102
+ }
103
+
104
+ /**
105
+ * Reads the active cursor and sets `_seekCeiling` on the graph instance
106
+ * so that subsequent materialize calls respect the time-travel boundary.
107
+ *
108
+ * @param {WarpGraphInstance} graph - WarpGraph instance
109
+ * @param {Persistence} persistence - GraphPersistencePort adapter
110
+ * @param {string} graphName - Name of the WARP graph
111
+ * @returns {Promise<{active: boolean, tick: number|null, maxTick: number|null}>}
112
+ */
113
+ export async function applyCursorCeiling(graph, persistence, graphName) {
114
+ const cursor = await readActiveCursor(persistence, graphName);
115
+ if (cursor) {
116
+ graph._seekCeiling = cursor.tick;
117
+ return { active: true, tick: cursor.tick, maxTick: null };
118
+ }
119
+ return { active: false, tick: null, maxTick: null };
120
+ }
121
+
122
+ /**
123
+ * Prints a seek cursor warning banner to stderr when a cursor is active.
124
+ *
125
+ * @param {{active: boolean, tick: number|null, maxTick: number|null}} cursorInfo
126
+ * @param {number|null} maxTick
127
+ * @returns {void}
128
+ */
129
+ export function emitCursorWarning(cursorInfo, maxTick) {
130
+ if (cursorInfo.active) {
131
+ const maxLabel = maxTick !== null && maxTick !== undefined ? ` of ${maxTick}` : '';
132
+ process.stderr.write(`\u26A0 seek active (tick ${cursorInfo.tick}${maxLabel}) \u2014 run "git warp seek --latest" to return to present\n`);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Reads the active seek cursor for a graph from Git ref storage.
138
+ *
139
+ * @param {Persistence} persistence - GraphPersistencePort adapter
140
+ * @param {string} graphName - Name of the WARP graph
141
+ * @returns {Promise<CursorBlob|null>}
142
+ */
143
+ export async function readActiveCursor(persistence, graphName) {
144
+ const ref = buildCursorActiveRef(graphName);
145
+ const oid = await persistence.readRef(ref);
146
+ if (!oid) {
147
+ return null;
148
+ }
149
+ const buf = await persistence.readBlob(oid);
150
+ return parseCursorBlob(buf, 'active cursor');
151
+ }
152
+
153
+ /**
154
+ * Writes (creates or overwrites) the active seek cursor for a graph.
155
+ *
156
+ * @param {Persistence} persistence - GraphPersistencePort adapter
157
+ * @param {string} graphName - Name of the WARP graph
158
+ * @param {CursorBlob} cursor - Cursor state to persist
159
+ * @returns {Promise<void>}
160
+ */
161
+ export async function writeActiveCursor(persistence, graphName, cursor) {
162
+ const ref = buildCursorActiveRef(graphName);
163
+ const json = JSON.stringify(cursor);
164
+ const oid = await persistence.writeBlob(Buffer.from(json, 'utf8'));
165
+ await persistence.updateRef(ref, oid);
166
+ }
167
+
168
+ /**
169
+ * @param {Persistence} persistence
170
+ * @param {string|null} checkpointSha
171
+ */
172
+ export async function readCheckpointDate(persistence, checkpointSha) {
173
+ if (!checkpointSha) {
174
+ return null;
175
+ }
176
+ const info = await persistence.getNodeInfo(checkpointSha);
177
+ return info.date || null;
178
+ }
179
+
180
+ export function createHookInstaller() {
181
+ const __filename = new URL(import.meta.url).pathname;
182
+ const __dirname = path.dirname(__filename);
183
+ const templateDir = path.resolve(__dirname, '..', '..', 'scripts', 'hooks');
184
+ const { version } = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', '..', 'package.json'), 'utf8'));
185
+ return new HookInstaller({
186
+ fs: /** @type {import('../../src/domain/services/HookInstaller.js').FsAdapter} */ (/** @type {unknown} */ (fs)),
187
+ execGitConfig: execGitConfigValue,
188
+ version,
189
+ templateDir,
190
+ path,
191
+ });
192
+ }
193
+
194
+ /**
195
+ * @param {string} repoPath
196
+ * @param {string} key
197
+ * @returns {string|null}
198
+ */
199
+ export function execGitConfigValue(repoPath, key) {
200
+ try {
201
+ if (key === '--git-dir') {
202
+ return execFileSync('git', ['-C', repoPath, 'rev-parse', '--git-dir'], {
203
+ encoding: 'utf8',
204
+ }).trim();
205
+ }
206
+ return execFileSync('git', ['-C', repoPath, 'config', key], {
207
+ encoding: 'utf8',
208
+ }).trim();
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+
214
+ export function isInteractive() {
215
+ return Boolean(process.stderr.isTTY);
216
+ }
217
+
218
+ /** @param {string} question @returns {Promise<string>} */
219
+ export function promptUser(question) {
220
+ const rl = readline.createInterface({
221
+ input: process.stdin,
222
+ output: process.stderr,
223
+ });
224
+ return new Promise((resolve) => {
225
+ rl.question(question, (answer) => {
226
+ rl.close();
227
+ resolve(answer.trim());
228
+ });
229
+ });
230
+ }
231
+
232
+ /**
233
+ * @param {{graph: WarpGraphInstance, persistence: Persistence, graphName: string, seekSpec: SeekSpec}} params
234
+ */
235
+ export function wireSeekCache({ graph, persistence, graphName, seekSpec }) {
236
+ if (seekSpec.noPersistentCache) {
237
+ return;
238
+ }
239
+ graph.setSeekCache(new CasSeekCacheAdapter({
240
+ persistence,
241
+ plumbing: persistence.plumbing,
242
+ graphName,
243
+ }));
244
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @typedef {Object} Persistence
3
+ * @property {(prefix: string) => Promise<string[]>} listRefs
4
+ * @property {(ref: string) => Promise<string|null>} readRef
5
+ * @property {(ref: string, oid: string) => Promise<void>} updateRef
6
+ * @property {(ref: string) => Promise<void>} deleteRef
7
+ * @property {(oid: string) => Promise<Buffer>} readBlob
8
+ * @property {(buf: Buffer) => Promise<string>} writeBlob
9
+ * @property {(sha: string) => Promise<{date?: string|null}>} getNodeInfo
10
+ * @property {(sha: string) => Promise<boolean>} nodeExists
11
+ * @property {(sha: string, coverageSha: string) => Promise<boolean>} isAncestor
12
+ * @property {() => Promise<{ok: boolean}>} ping
13
+ * @property {unknown} plumbing
14
+ */
15
+
16
+ /**
17
+ * @typedef {Object} WarpGraphInstance
18
+ * @property {(opts?: {ceiling?: number}) => Promise<void>} materialize
19
+ * @property {() => Promise<Array<{id: string}>>} getNodes
20
+ * @property {() => Promise<Array<{from: string, to: string, label?: string}>>} getEdges
21
+ * @property {() => Promise<string|null>} createCheckpoint
22
+ * @property {() => QueryBuilderLike} query
23
+ * @property {{ shortestPath: Function }} traverse
24
+ * @property {(writerId: string) => Promise<Array<{patch: {schema?: number, lamport: number, ops?: Array<{type: string, node?: string, from?: string, to?: string}>}, sha: string}>>} getWriterPatches
25
+ * @property {() => Promise<{frontier: Record<string, string>}>} status
26
+ * @property {() => Promise<string[]>} discoverWriters
27
+ * @property {() => Promise<Map<string, string>>} getFrontier
28
+ * @property {() => {totalTombstones: number, tombstoneRatio: number}} getGCMetrics
29
+ * @property {() => Promise<number>} getPropertyCount
30
+ * @property {() => Promise<import('../../src/domain/services/JoinReducer.js').WarpStateV5 | null>} getStateSnapshot
31
+ * @property {() => Promise<{ticks: number[], maxTick: number, perWriter: Map<string, WriterTickInfo>}>} discoverTicks
32
+ * @property {(sha: string) => Promise<{ops?: Array<{type: string, node?: string, from?: string, to?: string}>}>} loadPatchBySha
33
+ * @property {(cache: import('../../src/ports/SeekCachePort.js').default) => void} setSeekCache
34
+ * @property {{clear: () => Promise<void>} | null} seekCache
35
+ * @property {number} [_seekCeiling]
36
+ * @property {boolean} [_provenanceDegraded]
37
+ */
38
+
39
+ /**
40
+ * @typedef {Object} WriterTickInfo
41
+ * @property {number[]} ticks
42
+ * @property {string|null} tipSha
43
+ * @property {Record<number, string>} [tickShas]
44
+ */
45
+
46
+ /**
47
+ * @typedef {Object} CursorBlob
48
+ * @property {number} tick
49
+ * @property {string} [mode]
50
+ * @property {number} [nodes]
51
+ * @property {number} [edges]
52
+ * @property {string} [frontierHash]
53
+ */
54
+
55
+ /**
56
+ * @typedef {Object} CliOptions
57
+ * @property {string} repo
58
+ * @property {boolean} json
59
+ * @property {boolean} ndjson
60
+ * @property {string|null} view
61
+ * @property {string|null} graph
62
+ * @property {string} writer
63
+ * @property {boolean} help
64
+ */
65
+
66
+ /**
67
+ * @typedef {Object} GraphInfoResult
68
+ * @property {string} name
69
+ * @property {{count: number, ids?: string[]}} writers
70
+ * @property {{ref: string, sha: string|null, date?: string|null}} [checkpoint]
71
+ * @property {{ref: string, sha: string|null}} [coverage]
72
+ * @property {Record<string, number>} [writerPatches]
73
+ * @property {{active: boolean, tick?: number, mode?: string}} [cursor]
74
+ */
75
+
76
+ /**
77
+ * @typedef {Object} SeekSpec
78
+ * @property {string} action
79
+ * @property {string|null} tickValue
80
+ * @property {string|null} name
81
+ * @property {boolean} noPersistentCache
82
+ * @property {boolean} diff
83
+ * @property {number} diffLimit
84
+ */
85
+
86
+ /**
87
+ * @typedef {Object} QueryBuilderLike
88
+ * @property {(label?: string) => QueryBuilderLike} outgoing
89
+ * @property {(label?: string) => QueryBuilderLike} incoming
90
+ * @property {(fn: Function) => QueryBuilderLike} where
91
+ * @property {(pattern: string) => QueryBuilderLike} match
92
+ * @property {(fields: string[]) => QueryBuilderLike} select
93
+ * @property {() => Promise<{nodes: Array<{id: string, props?: Record<string, unknown>}>, stateHash?: string}>} run
94
+ */
95
+
96
+ export {};
@@ -22,11 +22,16 @@ import {
22
22
  renderQuery,
23
23
  renderPath,
24
24
  renderCheck,
25
+ renderDoctor,
25
26
  renderHistory,
26
27
  renderError,
27
28
  renderMaterialize,
28
29
  renderInstallHooks,
29
30
  renderSeek,
31
+ renderVerifyAudit,
32
+ renderTrust,
33
+ renderPatchShow,
34
+ renderPatchList,
30
35
  } from './text.js';
31
36
 
32
37
  // ── Color control ────────────────────────────────────────────────────────────
@@ -58,20 +63,46 @@ export function shouldStripColor() {
58
63
 
59
64
  // ── Text renderer map ────────────────────────────────────────────────────────
60
65
 
61
- /** @type {Map<string, function(*): string>} */
62
- 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][]} */ ([
63
89
  ['info', renderInfo],
64
90
  ['query', renderQuery],
65
91
  ['path', renderPath],
66
92
  ['check', renderCheck],
93
+ ['doctor', renderDoctor],
67
94
  ['history', renderHistory],
68
95
  ['materialize', renderMaterialize],
69
96
  ['seek', renderSeek],
97
+ ['verify-audit', renderVerifyAudit],
98
+ ['trust', renderTrust],
99
+ ['patch', renderPatch],
100
+ ['tree', renderTree],
70
101
  ['install-hooks', renderInstallHooks],
71
102
  ]));
72
103
 
73
- /** @type {Map<string, function(*): string>} */
74
- 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][]} */ ([
75
106
  ['info', renderInfoView],
76
107
  ['check', renderCheckView],
77
108
  ['history', renderHistoryView],
@@ -96,7 +127,7 @@ function writeHtmlExport(filePath, svgContent) {
96
127
 
97
128
  /**
98
129
  * Handles svg:PATH and html:PATH view modes for commands that carry _renderedSvg.
99
- * @param {*} payload
130
+ * @param {{ _renderedSvg?: string }} payload
100
131
  * @param {string} view
101
132
  * @returns {boolean} true if handled
102
133
  */
@@ -140,13 +171,13 @@ function writeText(text, strip) {
140
171
  /**
141
172
  * Writes a command result to stdout/stderr in the requested format.
142
173
  *
143
- * @param {*} payload - Command result payload
174
+ * @param {Record<string, unknown>} payload - Command result payload
144
175
  * @param {{format: string, command: string, view: string|null|boolean}} options
145
176
  */
146
177
  export function present(payload, { format, command, view }) {
147
178
  // Error payloads always go to stderr as plain text
148
179
  if (payload?.error) {
149
- process.stderr.write(renderError(payload));
180
+ process.stderr.write(renderError(/** @type {import('./text.js').ErrorPayload} */ (payload)));
150
181
  return;
151
182
  }
152
183
 
@@ -180,7 +211,7 @@ export function present(payload, { format, command, view }) {
180
211
 
181
212
  /**
182
213
  * Handles --view output dispatch (ASCII view, SVG file, HTML file).
183
- * @param {*} payload
214
+ * @param {Record<string, unknown>} payload
184
215
  * @param {string} command
185
216
  * @param {string|boolean} view
186
217
  */
@@ -194,7 +225,8 @@ function presentView(payload, command, view) {
194
225
 
195
226
  // query is special: uses pre-rendered _renderedAscii
196
227
  if (command === 'query') {
197
- writeText(`${payload._renderedAscii ?? ''}\n`, strip);
228
+ const ascii = typeof payload._renderedAscii === 'string' ? payload._renderedAscii : '';
229
+ writeText(`${ascii}\n`, strip);
198
230
  return;
199
231
  }
200
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;