@git-stunts/git-warp 11.2.1 → 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 (111) hide show
  1. package/bin/cli/commands/check.js +2 -2
  2. package/bin/cli/commands/doctor/checks.js +12 -12
  3. package/bin/cli/commands/doctor/index.js +2 -2
  4. package/bin/cli/commands/doctor/types.js +1 -1
  5. package/bin/cli/commands/history.js +12 -5
  6. package/bin/cli/commands/install-hooks.js +5 -5
  7. package/bin/cli/commands/materialize.js +2 -2
  8. package/bin/cli/commands/patch.js +142 -0
  9. package/bin/cli/commands/path.js +4 -4
  10. package/bin/cli/commands/query.js +54 -13
  11. package/bin/cli/commands/registry.js +4 -0
  12. package/bin/cli/commands/seek.js +17 -11
  13. package/bin/cli/commands/tree.js +230 -0
  14. package/bin/cli/commands/trust.js +3 -3
  15. package/bin/cli/commands/verify-audit.js +8 -7
  16. package/bin/cli/commands/view.js +6 -5
  17. package/bin/cli/infrastructure.js +26 -12
  18. package/bin/cli/shared.js +2 -2
  19. package/bin/cli/types.js +19 -8
  20. package/bin/presenters/index.js +35 -9
  21. package/bin/presenters/json.js +14 -12
  22. package/bin/presenters/text.js +155 -33
  23. package/index.d.ts +82 -22
  24. package/package.json +3 -2
  25. package/src/domain/WarpGraph.js +4 -1
  26. package/src/domain/crdt/ORSet.js +8 -8
  27. package/src/domain/errors/EmptyMessageError.js +2 -2
  28. package/src/domain/errors/ForkError.js +1 -1
  29. package/src/domain/errors/IndexError.js +1 -1
  30. package/src/domain/errors/OperationAbortedError.js +1 -1
  31. package/src/domain/errors/QueryError.js +1 -1
  32. package/src/domain/errors/SchemaUnsupportedError.js +1 -1
  33. package/src/domain/errors/ShardCorruptionError.js +2 -2
  34. package/src/domain/errors/ShardLoadError.js +2 -2
  35. package/src/domain/errors/ShardValidationError.js +4 -4
  36. package/src/domain/errors/StorageError.js +2 -2
  37. package/src/domain/errors/SyncError.js +1 -1
  38. package/src/domain/errors/TraversalError.js +1 -1
  39. package/src/domain/errors/TrustError.js +1 -1
  40. package/src/domain/errors/WarpError.js +2 -2
  41. package/src/domain/errors/WormholeError.js +1 -1
  42. package/src/domain/services/AuditReceiptService.js +6 -6
  43. package/src/domain/services/AuditVerifierService.js +52 -38
  44. package/src/domain/services/BitmapIndexBuilder.js +3 -3
  45. package/src/domain/services/BitmapIndexReader.js +28 -19
  46. package/src/domain/services/BoundaryTransitionRecord.js +18 -17
  47. package/src/domain/services/CheckpointSerializerV5.js +17 -16
  48. package/src/domain/services/CheckpointService.js +2 -2
  49. package/src/domain/services/CommitDagTraversalService.js +13 -13
  50. package/src/domain/services/DagPathFinding.js +7 -7
  51. package/src/domain/services/DagTopology.js +1 -1
  52. package/src/domain/services/DagTraversal.js +1 -1
  53. package/src/domain/services/HealthCheckService.js +1 -1
  54. package/src/domain/services/HookInstaller.js +1 -1
  55. package/src/domain/services/HttpSyncServer.js +92 -41
  56. package/src/domain/services/IndexRebuildService.js +7 -7
  57. package/src/domain/services/IndexStalenessChecker.js +4 -3
  58. package/src/domain/services/JoinReducer.js +11 -11
  59. package/src/domain/services/LogicalTraversal.js +1 -1
  60. package/src/domain/services/MessageCodecInternal.js +1 -1
  61. package/src/domain/services/MigrationService.js +1 -1
  62. package/src/domain/services/ObserverView.js +8 -8
  63. package/src/domain/services/PatchBuilderV2.js +42 -26
  64. package/src/domain/services/ProvenanceIndex.js +1 -1
  65. package/src/domain/services/ProvenancePayload.js +1 -1
  66. package/src/domain/services/QueryBuilder.js +3 -3
  67. package/src/domain/services/StateDiff.js +14 -11
  68. package/src/domain/services/StateSerializerV5.js +2 -2
  69. package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
  70. package/src/domain/services/SyncAuthService.js +3 -2
  71. package/src/domain/services/SyncProtocol.js +25 -11
  72. package/src/domain/services/TemporalQuery.js +9 -6
  73. package/src/domain/services/TranslationCost.js +7 -5
  74. package/src/domain/services/WormholeService.js +16 -7
  75. package/src/domain/trust/TrustCanonical.js +3 -3
  76. package/src/domain/trust/TrustEvaluator.js +18 -3
  77. package/src/domain/trust/TrustRecordService.js +30 -23
  78. package/src/domain/trust/TrustStateBuilder.js +21 -8
  79. package/src/domain/trust/canonical.js +6 -6
  80. package/src/domain/types/TickReceipt.js +1 -1
  81. package/src/domain/types/WarpErrors.js +45 -0
  82. package/src/domain/types/WarpOptions.js +29 -0
  83. package/src/domain/types/WarpPersistence.js +41 -0
  84. package/src/domain/types/WarpTypes.js +2 -2
  85. package/src/domain/types/WarpTypesV2.js +2 -2
  86. package/src/domain/utils/MinHeap.js +6 -5
  87. package/src/domain/utils/canonicalStringify.js +5 -4
  88. package/src/domain/utils/roaring.js +31 -5
  89. package/src/domain/warp/PatchSession.js +9 -18
  90. package/src/domain/warp/_wiredMethods.d.ts +199 -45
  91. package/src/domain/warp/checkpoint.methods.js +5 -1
  92. package/src/domain/warp/fork.methods.js +2 -2
  93. package/src/domain/warp/materialize.methods.js +55 -5
  94. package/src/domain/warp/materializeAdvanced.methods.js +15 -4
  95. package/src/domain/warp/patch.methods.js +54 -29
  96. package/src/domain/warp/provenance.methods.js +5 -3
  97. package/src/domain/warp/query.methods.js +6 -5
  98. package/src/domain/warp/sync.methods.js +16 -11
  99. package/src/globals.d.ts +64 -0
  100. package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
  101. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
  102. package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
  103. package/src/infrastructure/adapters/GitGraphAdapter.js +14 -12
  104. package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
  105. package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
  106. package/src/visualization/layouts/converters.js +2 -2
  107. package/src/visualization/layouts/elkAdapter.js +1 -1
  108. package/src/visualization/layouts/elkLayout.js +10 -7
  109. package/src/visualization/layouts/index.js +1 -1
  110. package/src/visualization/renderers/ascii/seek.js +16 -6
  111. package/src/visualization/renderers/svg/index.js +1 -1
@@ -11,7 +11,7 @@ import { openGraph, applyCursorCeiling, emitCursorWarning, readCheckpointDate, c
11
11
  /** @param {Persistence} persistence */
12
12
  async function getHealth(persistence) {
13
13
  const clock = ClockAdapter.global();
14
- const healthService = new HealthCheckService({ persistence: /** @type {*} */ (persistence), clock }); // TODO(ts-cleanup): narrow port type
14
+ const healthService = new HealthCheckService({ persistence: /** @type {import('../../../src/domain/types/WarpPersistence.js').CorePersistence} */ (/** @type {unknown} */ (persistence)), clock });
15
15
  return await healthService.getHealth();
16
16
  }
17
17
 
@@ -137,7 +137,7 @@ function getHookStatusForCheck(repoPath) {
137
137
  /**
138
138
  * Handles the `check` command: reports graph health, GC, and hook status.
139
139
  * @param {{options: CliOptions}} params
140
- * @returns {Promise<{payload: *, exitCode: number}>}
140
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
141
141
  */
142
142
  export default async function handleCheck({ options }) {
143
143
  const { graph, graphName, persistence } = await openGraph(options);
@@ -24,7 +24,7 @@ import { CODES } from './codes.js';
24
24
 
25
25
  /**
26
26
  * @param {string} id
27
- * @param {*} err TODO(ts-cleanup): narrow error type
27
+ * @param {unknown} err
28
28
  * @returns {DoctorFinding}
29
29
  */
30
30
  function internalError(id, err) {
@@ -33,7 +33,7 @@ function internalError(id, err) {
33
33
  status: 'fail',
34
34
  code: CODES.CHECK_INTERNAL_ERROR,
35
35
  impact: 'data_integrity',
36
- message: `Internal error: ${err?.message || String(err)}`,
36
+ message: `Internal error: ${err instanceof Error ? err.message : String(err)}`,
37
37
  };
38
38
  }
39
39
 
@@ -43,7 +43,7 @@ function internalError(id, err) {
43
43
  export async function checkRepoAccessible(ctx) {
44
44
  try {
45
45
  const clock = ClockAdapter.global();
46
- const svc = new HealthCheckService({ persistence: /** @type {*} TODO(ts-cleanup): narrow port type */ (ctx.persistence), clock });
46
+ const svc = new HealthCheckService({ persistence: /** @type {import('../../../../src/domain/types/WarpPersistence.js').CorePersistence} */ (/** @type {unknown} */ (ctx.persistence)), clock });
47
47
  const health = await svc.getHealth();
48
48
  if (health.components.repository.status === 'unhealthy') {
49
49
  return {
@@ -56,7 +56,7 @@ export async function checkRepoAccessible(ctx) {
56
56
  id: 'repo-accessible', status: 'ok', code: CODES.REPO_OK,
57
57
  impact: 'operability', message: 'Repository is accessible',
58
58
  };
59
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): narrow error type
59
+ } catch (err) {
60
60
  return internalError('repo-accessible', err);
61
61
  }
62
62
  }
@@ -104,7 +104,7 @@ export async function checkRefsConsistent(ctx) {
104
104
  });
105
105
  }
106
106
  return findings;
107
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): narrow error type
107
+ } catch (err) {
108
108
  return [internalError('refs-consistent', err)];
109
109
  }
110
110
  }
@@ -151,7 +151,7 @@ export async function checkCoverageComplete(ctx) {
151
151
  id: 'coverage-complete', status: 'ok', code: CODES.COVERAGE_OK,
152
152
  impact: 'operability', message: 'Coverage anchor includes all writers',
153
153
  };
154
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): narrow error type
154
+ } catch (err) {
155
155
  return internalError('coverage-complete', err);
156
156
  }
157
157
  }
@@ -192,7 +192,7 @@ export async function checkCheckpointFresh(ctx) {
192
192
 
193
193
  const { date, ageHours } = await getCheckpointAge(ctx.persistence, sha);
194
194
  return buildCheckpointFinding({ sha, date, ageHours, maxAge: ctx.policy.checkpointMaxAgeHours });
195
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): narrow error type
195
+ } catch (err) {
196
196
  return internalError('checkpoint-fresh', err);
197
197
  }
198
198
  }
@@ -290,7 +290,7 @@ export async function checkAuditConsistent(ctx) {
290
290
  });
291
291
  }
292
292
  return findings;
293
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): narrow error type
293
+ } catch (err) {
294
294
  return [internalError('audit-consistent', err)];
295
295
  }
296
296
  }
@@ -350,7 +350,7 @@ export async function checkClockSkew(ctx) {
350
350
  message: `Clock skew is within threshold (${Math.round(spreadMs / 1000)}s)`,
351
351
  evidence: { spreadMs },
352
352
  };
353
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): narrow error type
353
+ } catch (err) {
354
354
  return internalError('clock-skew', err);
355
355
  }
356
356
  }
@@ -367,13 +367,13 @@ export async function checkHooksInstalled(ctx) {
367
367
  const installer = createHookInstaller();
368
368
  const s = installer.getHookStatus(ctx.repoPath);
369
369
  return buildHookFinding(s);
370
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): narrow error type
370
+ } catch (err) {
371
371
  return internalError('hooks-installed', err);
372
372
  }
373
373
  }
374
374
 
375
375
  /**
376
- * @param {*} s TODO(ts-cleanup): narrow hook status type
376
+ * @param {{ installed: boolean, version?: string, current?: boolean, foreign?: boolean, hookPath: string }} s
377
377
  * @returns {DoctorFinding}
378
378
  */
379
379
  function buildHookFinding(s) {
@@ -396,7 +396,7 @@ function buildHookFinding(s) {
396
396
  id: 'hooks-installed', status: 'warn', code: CODES.HOOKS_OUTDATED,
397
397
  impact: 'hygiene', message: `Hook is outdated (v${s.version})`,
398
398
  fix: 'Run `git warp install-hooks` to upgrade',
399
- evidence: { version: s.version },
399
+ evidence: { version: s.version ?? null },
400
400
  };
401
401
  }
402
402
  return {
@@ -160,7 +160,7 @@ async function runChecks(ctx, startMs) {
160
160
  f.durationMs = checkDuration;
161
161
  findings.push(f);
162
162
  }
163
- } catch (/** @type {*} TODO(ts-cleanup): narrow error type */ err) {
163
+ } catch (err) {
164
164
  checkDuration = checkDuration ?? 0;
165
165
  checksRun++;
166
166
  findings.push({
@@ -168,7 +168,7 @@ async function runChecks(ctx, startMs) {
168
168
  status: 'fail',
169
169
  code: CODES.CHECK_INTERNAL_ERROR,
170
170
  impact: 'data_integrity',
171
- message: `Internal error in ${check.id}: ${err?.message || String(err)}`,
171
+ message: `Internal error in ${check.id}: ${err instanceof Error ? err.message : String(err)}`,
172
172
  durationMs: checkDuration,
173
173
  });
174
174
  }
@@ -6,7 +6,7 @@
6
6
 
7
7
  // ── JSON-safe recursive value type ──────────────────────────────────────────
8
8
 
9
- /** @typedef {null | boolean | number | string | Array<*> | {[k:string]: *}} JsonValue */ // TODO(ts-cleanup): recursive type
9
+ /** @typedef {null | boolean | number | string | Array<unknown> | {[k:string]: unknown}} JsonValue */
10
10
 
11
11
  /** @typedef {{[k:string]: JsonValue}} FindingEvidence */
12
12
 
@@ -4,6 +4,7 @@ import { historySchema } from '../schemas.js';
4
4
  import { openGraph, applyCursorCeiling, emitCursorWarning } from '../shared.js';
5
5
 
6
6
  /** @typedef {import('../types.js').CliOptions} CliOptions */
7
+ /** @typedef {{patch: {schema?: number, lamport: number, ops?: Array<{type: string, node?: string, from?: string, to?: string}>}, sha: string}} PatchEntry */
7
8
 
8
9
  const HISTORY_OPTIONS = {
9
10
  node: { type: 'string' },
@@ -16,7 +17,7 @@ function parseHistoryArgs(args) {
16
17
  }
17
18
 
18
19
  /**
19
- * @param {*} patch
20
+ * @param {{ops?: Array<{node?: string, from?: string, to?: string}>}} patch
20
21
  * @param {string} nodeId
21
22
  */
22
23
  function patchTouchesNode(patch, nodeId) {
@@ -35,7 +36,7 @@ function patchTouchesNode(patch, nodeId) {
35
36
  /**
36
37
  * Handles the `history` command: shows patch history for a writer.
37
38
  * @param {{options: CliOptions, args: string[]}} params
38
- * @returns {Promise<{payload: *, exitCode: number}>}
39
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
39
40
  */
40
41
  export default async function handleHistory({ options, args }) {
41
42
  const historyOptions = parseHistoryArgs(args);
@@ -46,15 +47,21 @@ export default async function handleHistory({ options, args }) {
46
47
  const writerId = options.writer;
47
48
  let patches = await graph.getWriterPatches(writerId);
48
49
  if (cursorInfo.active) {
49
- patches = patches.filter((/** @type {*} */ { patch }) => patch.lamport <= /** @type {number} */ (cursorInfo.tick)); // TODO(ts-cleanup): type CLI payload
50
+ patches = patches.filter((/** @type {PatchEntry} */ { patch }) => patch.lamport <= /** @type {number} */ (cursorInfo.tick));
50
51
  }
51
52
  if (patches.length === 0) {
53
+ const knownWriters = await graph.discoverWriters();
54
+ if (knownWriters.length > 0) {
55
+ throw notFoundError(
56
+ `No patches found for writer: ${writerId}\nKnown writers: ${knownWriters.join(', ')}\nUse: warp-graph history --writer <id>`
57
+ );
58
+ }
52
59
  throw notFoundError(`No patches found for writer: ${writerId}`);
53
60
  }
54
61
 
55
62
  const entries = patches
56
- .filter((/** @type {*} */ { patch }) => !historyOptions.node || patchTouchesNode(patch, historyOptions.node)) // TODO(ts-cleanup): type CLI payload
57
- .map((/** @type {*} */ { patch, sha }) => ({ // TODO(ts-cleanup): type CLI payload
63
+ .filter((/** @type {PatchEntry} */ { patch }) => !historyOptions.node || patchTouchesNode(patch, historyOptions.node))
64
+ .map((/** @type {PatchEntry} */ { patch, sha }) => ({
58
65
  sha,
59
66
  schema: patch.schema,
60
67
  lamport: patch.lamport,
@@ -18,7 +18,7 @@ function parseInstallHooksArgs(args) {
18
18
  }
19
19
 
20
20
  /**
21
- * @param {*} classification
21
+ * @param {{kind: string, version?: string, appended?: boolean}} classification
22
22
  * @param {{force: boolean}} hookOptions
23
23
  */
24
24
  async function resolveStrategy(classification, hookOptions) {
@@ -37,7 +37,7 @@ async function resolveStrategy(classification, hookOptions) {
37
37
  return await promptForForeignStrategy();
38
38
  }
39
39
 
40
- /** @param {*} classification */
40
+ /** @param {{kind: string, version?: string, appended?: boolean}} classification */
41
41
  async function promptForOursStrategy(classification) {
42
42
  const installer = createHookInstaller();
43
43
  if (classification.version === installer._version) {
@@ -81,8 +81,8 @@ async function promptForForeignStrategy() {
81
81
  function readHookContent(hookPath) {
82
82
  try {
83
83
  return fs.readFileSync(hookPath, 'utf8');
84
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type fs error
85
- if (err.code === 'ENOENT') {
84
+ } catch (err) {
85
+ if (err instanceof Error && /** @type {{code?: string}} */ (err).code === 'ENOENT') {
86
86
  return null;
87
87
  }
88
88
  throw err;
@@ -92,7 +92,7 @@ function readHookContent(hookPath) {
92
92
  /**
93
93
  * Handles the `install-hooks` command.
94
94
  * @param {{options: CliOptions, args: string[]}} params
95
- * @returns {Promise<{payload: *, exitCode: number}>}
95
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
96
96
  */
97
97
  export default async function handleInstallHooks({ options, args }) {
98
98
  const hookOptions = parseInstallHooksArgs(args);
@@ -45,7 +45,7 @@ async function materializeOneGraph({ persistence, graphName, writerId, ceiling }
45
45
  /**
46
46
  * Handles the `materialize` command: materializes and checkpoints all graphs.
47
47
  * @param {{options: CliOptions}} params
48
- * @returns {Promise<{payload: *, exitCode: number}>}
48
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
49
49
  */
50
50
  export default async function handleMaterialize({ options }) {
51
51
  const { persistence } = await createPersistence(options.repo);
@@ -91,7 +91,7 @@ export default async function handleMaterialize({ options }) {
91
91
  }
92
92
  }
93
93
 
94
- const allFailed = results.every((r) => /** @type {*} */ (r).error); // TODO(ts-cleanup): type CLI payload
94
+ const allFailed = results.every((r) => 'error' in r);
95
95
  return {
96
96
  payload: { graphs: results },
97
97
  exitCode: allFailed ? EXIT_CODES.INTERNAL : EXIT_CODES.OK,
@@ -0,0 +1,142 @@
1
+ import { EXIT_CODES, usageError, notFoundError, parseCommandArgs } from '../infrastructure.js';
2
+ import { openGraph } from '../shared.js';
3
+ import { z } from 'zod';
4
+
5
+ /** @typedef {import('../types.js').CliOptions} CliOptions */
6
+
7
+ const PATCH_OPTIONS = {
8
+ writer: { type: 'string' },
9
+ limit: { type: 'string' },
10
+ };
11
+
12
+ const patchSchema = z.object({
13
+ writer: z.string().optional(),
14
+ limit: z.coerce.number().int().positive().optional(),
15
+ }).strict();
16
+
17
+ /**
18
+ * Collects all patches across all writers (or a single writer).
19
+ * @param {import('../types.js').WarpGraphInstance} graph
20
+ * @param {string|null} writerFilter
21
+ * @returns {Promise<Array<{sha: string, writer: string, patch: {schema?: number, lamport: number, ops?: Array<Record<string, unknown>>, context?: Record<string, unknown>}}>>}
22
+ */
23
+ async function collectPatches(graph, writerFilter) {
24
+ const writers = writerFilter ? [writerFilter] : await graph.discoverWriters();
25
+ const all = [];
26
+ for (const writerId of writers) {
27
+ const patches = await graph.getWriterPatches(writerId);
28
+ for (const { patch, sha } of patches) {
29
+ all.push({ sha, writer: writerId, patch });
30
+ }
31
+ }
32
+ // Sort by lamport ascending
33
+ all.sort((a, b) => (a.patch.lamport ?? 0) - (b.patch.lamport ?? 0));
34
+ return all;
35
+ }
36
+
37
+ /**
38
+ * Handles the `patch` command: show or list decoded patches.
39
+ * @param {{options: CliOptions, args: string[]}} params
40
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
41
+ */
42
+ export default async function handlePatch({ options, args }) {
43
+ // First positional is the subaction: show or list
44
+ const subaction = args[0];
45
+ const rest = args.slice(1);
46
+
47
+ if (subaction === 'show') {
48
+ return await handlePatchShow({ options, args: rest });
49
+ }
50
+ if (subaction === 'list') {
51
+ return await handlePatchList({ options, args: rest });
52
+ }
53
+ if (!subaction) {
54
+ throw usageError('Usage: warp-graph patch <show|list> [options]\n show <sha> Decode and display a single patch\n list List all patches');
55
+ }
56
+ throw usageError(`Unknown patch subaction: ${subaction}. Use: show, list`);
57
+ }
58
+
59
+ /**
60
+ * @param {{options: CliOptions, args: string[]}} params
61
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
62
+ */
63
+ async function handlePatchShow({ options, args }) {
64
+ if (!args[0]) {
65
+ throw usageError('Usage: warp-graph patch show <sha>');
66
+ }
67
+ const targetSha = args[0];
68
+ const { graph, graphName } = await openGraph(options);
69
+ const allPatches = await collectPatches(graph, null);
70
+
71
+ const match = allPatches.find((p) => p.sha === targetSha || p.sha.startsWith(targetSha));
72
+ if (!match) {
73
+ throw notFoundError(`Patch not found: ${targetSha}`);
74
+ }
75
+
76
+ const payload = {
77
+ graph: graphName,
78
+ sha: match.sha,
79
+ writer: match.writer,
80
+ lamport: match.patch.lamport,
81
+ schema: match.patch.schema,
82
+ ops: match.patch.ops,
83
+ context: match.patch.context,
84
+ };
85
+
86
+ return { payload, exitCode: EXIT_CODES.OK };
87
+ }
88
+
89
+ /**
90
+ * @param {{options: CliOptions, args: string[]}} params
91
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
92
+ */
93
+ async function handlePatchList({ options, args }) {
94
+ const { values } = parseCommandArgs(args, PATCH_OPTIONS, patchSchema);
95
+ const { graph, graphName } = await openGraph(options);
96
+ const writerFilter = values.writer || null;
97
+ const allPatches = await collectPatches(graph, writerFilter);
98
+
99
+ const limit = values.limit ?? allPatches.length;
100
+ const entries = allPatches.slice(0, limit).map((p) => ({
101
+ sha: p.sha.slice(0, 7),
102
+ fullSha: p.sha,
103
+ writer: p.writer,
104
+ lamport: p.patch.lamport,
105
+ opCount: Array.isArray(p.patch.ops) ? p.patch.ops.length : 0,
106
+ nodeIds: extractNodeIds(p.patch.ops || []),
107
+ }));
108
+
109
+ const payload = {
110
+ graph: graphName,
111
+ total: allPatches.length,
112
+ showing: entries.length,
113
+ writerFilter,
114
+ entries,
115
+ };
116
+
117
+ return { payload, exitCode: EXIT_CODES.OK };
118
+ }
119
+
120
+ /**
121
+ * Extracts unique node IDs touched by a patch's operations.
122
+ * @param {Array<Record<string, unknown>>} ops
123
+ * @returns {string[]}
124
+ */
125
+ function extractNodeIds(ops) {
126
+ if (!Array.isArray(ops)) {
127
+ return [];
128
+ }
129
+ const ids = new Set();
130
+ for (const op of ops) {
131
+ if (op.node) {
132
+ ids.add(op.node);
133
+ }
134
+ if (op.from) {
135
+ ids.add(op.from);
136
+ }
137
+ if (op.to) {
138
+ ids.add(op.to);
139
+ }
140
+ }
141
+ return [...ids].sort();
142
+ }
@@ -43,7 +43,7 @@ function parsePathArgs(args) {
43
43
  /**
44
44
  * Handles the `path` command: finds a shortest path between two nodes.
45
45
  * @param {{options: CliOptions, args: string[]}} params
46
- * @returns {Promise<{payload: *, exitCode: number}>}
46
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
47
47
  */
48
48
  export default async function handlePath({ options, args }) {
49
49
  const pathOptions = parsePathArgs(args);
@@ -77,10 +77,10 @@ export default async function handlePath({ options, args }) {
77
77
 
78
78
  return {
79
79
  payload,
80
- exitCode: result.found ? EXIT_CODES.OK : EXIT_CODES.NOT_FOUND,
80
+ exitCode: result.found ? EXIT_CODES.OK : EXIT_CODES.NO_MATCH,
81
81
  };
82
- } catch (/** @type {*} */ error) { // TODO(ts-cleanup): type error
83
- if (error && error.code === 'NODE_NOT_FOUND') {
82
+ } catch (error) {
83
+ if (error instanceof Error && /** @type {{code?: string}} */ (error).code === 'NODE_NOT_FOUND') {
84
84
  throw notFoundError(error.message);
85
85
  }
86
86
  throw error;
@@ -6,6 +6,7 @@ import { openGraph, applyCursorCeiling, emitCursorWarning } from '../shared.js';
6
6
  import { querySchema } from '../schemas.js';
7
7
 
8
8
  /** @typedef {import('../types.js').CliOptions} CliOptions */
9
+ /** @typedef {import('../types.js').QueryBuilderLike} QueryBuilderLike */
9
10
 
10
11
  const QUERY_OPTIONS = {
11
12
  match: { type: 'string' },
@@ -85,7 +86,7 @@ function parseQueryArgs(args) {
85
86
  }
86
87
 
87
88
  /**
88
- * @param {*} builder
89
+ * @param {QueryBuilderLike} builder
89
90
  * @param {Array<{type: string, label?: string, key?: string, value?: string}>} steps
90
91
  */
91
92
  function applyQuerySteps(builder, steps) {
@@ -97,7 +98,7 @@ function applyQuerySteps(builder, steps) {
97
98
  }
98
99
 
99
100
  /**
100
- * @param {*} builder
101
+ * @param {QueryBuilderLike} builder
101
102
  * @param {{type: string, label?: string, key?: string, value?: string}} step
102
103
  */
103
104
  function applyQueryStep(builder, step) {
@@ -108,13 +109,13 @@ function applyQueryStep(builder, step) {
108
109
  return builder.incoming(step.label);
109
110
  }
110
111
  if (step.type === 'where-prop') {
111
- return builder.where((/** @type {*} */ node) => matchesPropFilter(node, /** @type {string} */ (step.key), /** @type {string} */ (step.value))); // TODO(ts-cleanup): type CLI payload
112
+ return builder.where((/** @type {{props?: Record<string, unknown>}} */ node) => matchesPropFilter(node, /** @type {string} */ (step.key), /** @type {string} */ (step.value)));
112
113
  }
113
114
  return builder;
114
115
  }
115
116
 
116
117
  /**
117
- * @param {*} node
118
+ * @param {{props?: Record<string, unknown>}} node
118
119
  * @param {string} key
119
120
  * @param {string} value
120
121
  */
@@ -126,22 +127,62 @@ function matchesPropFilter(node, key, value) {
126
127
  return String(props[key]) === value;
127
128
  }
128
129
 
130
+ /**
131
+ * Builds a map of nodeId -> {outgoing: [], incoming: []} from edges.
132
+ * @param {Array<{from: string, to: string, label?: string}>} edges
133
+ * @returns {Map<string, {outgoing: Array<{label: string, to: string}>, incoming: Array<{label: string, from: string}>}>}
134
+ */
135
+ function buildEdgeMap(edges) {
136
+ /** @type {Map<string, {outgoing: Array<{label: string, to: string}>, incoming: Array<{label: string, from: string}>}>} */
137
+ const edgeMap = new Map();
138
+ for (const edge of edges) {
139
+ if (!edgeMap.has(edge.from)) {
140
+ edgeMap.set(edge.from, { outgoing: [], incoming: [] });
141
+ }
142
+ if (!edgeMap.has(edge.to)) {
143
+ edgeMap.set(edge.to, { outgoing: [], incoming: [] });
144
+ }
145
+ const fromEntry = edgeMap.get(edge.from);
146
+ const toEntry = edgeMap.get(edge.to);
147
+ if (fromEntry) {
148
+ fromEntry.outgoing.push({ label: edge.label || '', to: edge.to });
149
+ }
150
+ if (toEntry) {
151
+ toEntry.incoming.push({ label: edge.label || '', from: edge.from });
152
+ }
153
+ }
154
+ return edgeMap;
155
+ }
156
+
129
157
  /**
130
158
  * @param {string} graphName
131
- * @param {*} result
132
- * @returns {{graph: string, stateHash: *, nodes: *, _renderedSvg?: string, _renderedAscii?: string}}
159
+ * @param {{nodes: Array<{id: string, props?: Record<string, unknown>}>, stateHash?: string}} result
160
+ * @param {Array<{from: string, to: string, label?: string}>} edges
161
+ * @returns {{graph: string, stateHash: string|undefined, nodes: Array<{id: string, props?: Record<string, unknown>} & Record<string, unknown>>, [k: string]: unknown}}
133
162
  */
134
- function buildQueryPayload(graphName, result) {
163
+ function buildQueryPayload(graphName, result, edges) {
164
+ const edgeMap = buildEdgeMap(edges);
165
+
166
+ const nodes = result.nodes.map((/** @type {{id: string, props?: Record<string, unknown>}} */ node) => {
167
+ /** @type {{id: string, props?: Record<string, unknown>} & Record<string, unknown>} */
168
+ const entry = { ...node };
169
+ const nodeEdges = edgeMap.get(node.id);
170
+ if (nodeEdges) {
171
+ entry.edges = nodeEdges;
172
+ }
173
+ return entry;
174
+ });
175
+
135
176
  return {
136
177
  graph: graphName,
137
178
  stateHash: result.stateHash,
138
- nodes: result.nodes,
179
+ nodes,
139
180
  };
140
181
  }
141
182
 
142
- /** @param {*} error */
183
+ /** @param {unknown} error */
143
184
  function mapQueryError(error) {
144
- if (error && error.code && String(error.code).startsWith('E_QUERY')) {
185
+ if (error instanceof Error && /** @type {{code?: string}} */ (error).code?.startsWith('E_QUERY')) {
145
186
  throw usageError(error.message);
146
187
  }
147
188
  throw error;
@@ -150,7 +191,7 @@ function mapQueryError(error) {
150
191
  /**
151
192
  * Handles the `query` command: runs a logical graph query.
152
193
  * @param {{options: CliOptions, args: string[]}} params
153
- * @returns {Promise<{payload: *, exitCode: number}>}
194
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
154
195
  */
155
196
  export default async function handleQuery({ options, args }) {
156
197
  const querySpec = parseQueryArgs(args);
@@ -171,10 +212,10 @@ export default async function handleQuery({ options, args }) {
171
212
 
172
213
  try {
173
214
  const result = await builder.run();
174
- const payload = buildQueryPayload(graphName, result);
215
+ const edges = await graph.getEdges();
216
+ const payload = buildQueryPayload(graphName, result, edges);
175
217
 
176
218
  if (options.view) {
177
- const edges = await graph.getEdges();
178
219
  const graphData = queryResultToGraphData(payload, edges);
179
220
  const positioned = await layoutGraph(graphData, { type: 'query' });
180
221
  if (typeof options.view === 'string' && (options.view.startsWith('svg:') || options.view.startsWith('html:'))) {
@@ -10,6 +10,8 @@ import handleVerifyAudit from './verify-audit.js';
10
10
  import handleView from './view.js';
11
11
  import handleInstallHooks from './install-hooks.js';
12
12
  import handleTrust from './trust.js';
13
+ import handlePatch from './patch.js';
14
+ import handleTree from './tree.js';
13
15
 
14
16
  /** @type {Map<string, Function>} */
15
17
  export const COMMANDS = new Map(/** @type {[string, Function][]} */ ([
@@ -23,6 +25,8 @@ export const COMMANDS = new Map(/** @type {[string, Function][]} */ ([
23
25
  ['seek', handleSeek],
24
26
  ['verify-audit', handleVerifyAudit],
25
27
  ['trust', handleTrust],
28
+ ['patch', handlePatch],
29
+ ['tree', handleTree],
26
30
  ['view', handleView],
27
31
  ['install-hooks', handleInstallHooks],
28
32
  ]));
@@ -17,6 +17,7 @@ import { openGraph, readActiveCursor, writeActiveCursor, wireSeekCache } from '.
17
17
  /** @typedef {import('../types.js').WriterTickInfo} WriterTickInfo */
18
18
  /** @typedef {import('../types.js').CursorBlob} CursorBlob */
19
19
  /** @typedef {import('../types.js').SeekSpec} SeekSpec */
20
+ /** @typedef {import('../../../src/domain/services/StateDiff.js').StateDiffResult} StateDiffResult */
20
21
 
21
22
  // ============================================================================
22
23
  // Cursor I/O Helpers (seek-only)
@@ -257,18 +258,19 @@ function computeSeekStateDiff(prevCursor, next, frontierHash) {
257
258
 
258
259
  /**
259
260
  * @param {{tick: number, perWriter: Map<string, WriterTickInfo>, graph: WarpGraphInstance}} params
260
- * @returns {Promise<Record<string, {sha: string, opSummary: *}>|null>}
261
+ * @returns {Promise<Record<string, {sha: string, opSummary: unknown}>|null>}
261
262
  */
262
263
  async function buildTickReceipt({ tick, perWriter, graph }) {
263
264
  if (!Number.isInteger(tick) || tick <= 0) {
264
265
  return null;
265
266
  }
266
267
 
267
- /** @type {Record<string, {sha: string, opSummary: *}>} */
268
+ /** @type {Record<string, {sha: string, opSummary: unknown}>} */
268
269
  const receipt = {};
269
270
 
270
271
  for (const [writerId, info] of perWriter) {
271
- const sha = /** @type {*} */ (info?.tickShas)?.[tick]; // TODO(ts-cleanup): type CLI payload
272
+ const tickShas = /** @type {Record<number, string> | undefined} */ (info?.tickShas);
273
+ const sha = tickShas?.[tick];
272
274
  if (!sha) {
273
275
  continue;
274
276
  }
@@ -283,7 +285,7 @@ async function buildTickReceipt({ tick, perWriter, graph }) {
283
285
 
284
286
  /**
285
287
  * @param {{graph: WarpGraphInstance, prevTick: number|null, currentTick: number, diffLimit: number}} params
286
- * @returns {Promise<{structuralDiff: *, diffBaseline: string, baselineTick: number|null, truncated: boolean, totalChanges: number, shownChanges: number}>}
288
+ * @returns {Promise<{structuralDiff: unknown, diffBaseline: string, baselineTick: number|null, truncated: boolean, totalChanges: number, shownChanges: number}>}
287
289
  */
288
290
  async function computeStructuralDiff({ graph, prevTick, currentTick, diffLimit }) {
289
291
  let beforeState = null;
@@ -303,18 +305,22 @@ async function computeStructuralDiff({ graph, prevTick, currentTick, diffLimit }
303
305
  }
304
306
 
305
307
  await graph.materialize({ ceiling: currentTick });
306
- const afterState = /** @type {*} */ (await graph.getStateSnapshot()); // TODO(ts-cleanup): narrow WarpStateV5
308
+ const afterState = await graph.getStateSnapshot();
309
+ if (!afterState) {
310
+ const empty = { nodes: { added: [], removed: [] }, edges: { added: [], removed: [] }, props: { set: [], removed: [] } };
311
+ return applyDiffLimit(empty, diffBaseline, baselineTick, diffLimit);
312
+ }
307
313
  const diff = diffStates(beforeState, afterState);
308
314
 
309
315
  return applyDiffLimit(diff, diffBaseline, baselineTick, diffLimit);
310
316
  }
311
317
 
312
318
  /**
313
- * @param {*} diff
319
+ * @param {StateDiffResult} diff
314
320
  * @param {string} diffBaseline
315
321
  * @param {number|null} baselineTick
316
322
  * @param {number} diffLimit
317
- * @returns {{structuralDiff: *, diffBaseline: string, baselineTick: number|null, truncated: boolean, totalChanges: number, shownChanges: number}}
323
+ * @returns {{structuralDiff: StateDiffResult, diffBaseline: string, baselineTick: number|null, truncated: boolean, totalChanges: number, shownChanges: number}}
318
324
  */
319
325
  function applyDiffLimit(diff, diffBaseline, baselineTick, diffLimit) {
320
326
  const totalChanges =
@@ -327,7 +333,7 @@ function applyDiffLimit(diff, diffBaseline, baselineTick, diffLimit) {
327
333
  }
328
334
 
329
335
  let remaining = diffLimit;
330
- const cap = (/** @type {any[]} */ arr) => {
336
+ const cap = (/** @type {unknown[]} */ arr) => {
331
337
  const take = Math.min(arr.length, remaining);
332
338
  remaining -= take;
333
339
  return arr.slice(0, take);
@@ -340,7 +346,7 @@ function applyDiffLimit(diff, diffBaseline, baselineTick, diffLimit) {
340
346
  };
341
347
 
342
348
  const shownChanges = diffLimit - remaining;
343
- return { structuralDiff: capped, diffBaseline, baselineTick, truncated: true, totalChanges, shownChanges };
349
+ return { structuralDiff: /** @type {StateDiffResult} */ (capped), diffBaseline, baselineTick, truncated: true, totalChanges, shownChanges };
344
350
  }
345
351
 
346
352
  // ============================================================================
@@ -349,7 +355,7 @@ function applyDiffLimit(diff, diffBaseline, baselineTick, diffLimit) {
349
355
 
350
356
  /**
351
357
  * @param {{graph: WarpGraphInstance, graphName: string, persistence: Persistence, activeCursor: CursorBlob|null, ticks: number[], maxTick: number, perWriter: Map<string, WriterTickInfo>, frontierHash: string}} params
352
- * @returns {Promise<{payload: *, exitCode: number}>}
358
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
353
359
  */
354
360
  async function handleSeekStatus({ graph, graphName, persistence, activeCursor, ticks, maxTick, perWriter, frontierHash }) {
355
361
  if (activeCursor) {
@@ -411,7 +417,7 @@ async function handleSeekStatus({ graph, graphName, persistence, activeCursor, t
411
417
  /**
412
418
  * Handles the `git warp seek` command across all sub-actions.
413
419
  * @param {{options: CliOptions, args: string[]}} params
414
- * @returns {Promise<{payload: *, exitCode: number}>}
420
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
415
421
  */
416
422
  export default async function handleSeek({ options, args }) {
417
423
  const seekSpec = parseSeekArgs(args);