@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
@@ -0,0 +1,230 @@
1
+ import { EXIT_CODES, usageError, parseCommandArgs } from '../infrastructure.js';
2
+ import { openGraph, applyCursorCeiling, emitCursorWarning } from '../shared.js';
3
+ import { z } from 'zod';
4
+
5
+ /** @typedef {import('../types.js').CliOptions} CliOptions */
6
+
7
+ const TREE_OPTIONS = {
8
+ edge: { type: 'string' },
9
+ prop: { type: 'string', multiple: true },
10
+ 'max-depth': { type: 'string' },
11
+ };
12
+
13
+ const treeSchema = z.object({
14
+ edge: z.string().optional(),
15
+ prop: z.union([z.string(), z.array(z.string())]).optional(),
16
+ 'max-depth': z.coerce.number().int().nonnegative().optional(),
17
+ }).strict().transform((val) => ({
18
+ edgeLabel: val.edge ?? null,
19
+ props: Array.isArray(val.prop) ? val.prop : val.prop ? [val.prop] : [],
20
+ maxDepth: val['max-depth'],
21
+ }));
22
+
23
+ /**
24
+ * Builds a parent-to-children adjacency map from edges.
25
+ * @param {Array<{from: string, to: string, label?: string}>} edges
26
+ * @param {string|null} labelFilter
27
+ * @returns {Map<string, Array<{id: string, label: string}>>}
28
+ */
29
+ function buildChildMap(edges, labelFilter) {
30
+ /** @type {Map<string, Array<{id: string, label: string}>>} */
31
+ const children = new Map();
32
+ /** @type {Set<string>} */
33
+ const hasParent = new Set();
34
+
35
+ for (const edge of edges) {
36
+ if (labelFilter && edge.label !== labelFilter) {
37
+ continue;
38
+ }
39
+ if (!children.has(edge.from)) {
40
+ children.set(edge.from, []);
41
+ }
42
+ const fromChildren = children.get(edge.from);
43
+ if (fromChildren) {
44
+ fromChildren.push({ id: edge.to, label: edge.label || '' });
45
+ }
46
+ hasParent.add(edge.to);
47
+ }
48
+
49
+ return children;
50
+ }
51
+
52
+ /**
53
+ * Finds root nodes (nodes with outgoing edges but no incoming edges in the filtered set).
54
+ * @param {string[]} nodeIds
55
+ * @param {Array<{from: string, to: string, label?: string}>} edges
56
+ * @param {string|null} labelFilter
57
+ * @returns {string[]}
58
+ */
59
+ function findRoots(nodeIds, edges, labelFilter) {
60
+ const hasParent = new Set();
61
+ const hasChild = new Set();
62
+
63
+ for (const edge of edges) {
64
+ if (labelFilter && edge.label !== labelFilter) {
65
+ continue;
66
+ }
67
+ hasParent.add(edge.to);
68
+ hasChild.add(edge.from);
69
+ }
70
+
71
+ // Roots: nodes that have children but no parents in the filtered edge set
72
+ const roots = nodeIds.filter((id) => !hasParent.has(id) && hasChild.has(id));
73
+ if (roots.length > 0) {
74
+ return roots.sort();
75
+ }
76
+
77
+ // Fallback: nodes with no incoming edges at all
78
+ return nodeIds.filter((id) => !hasParent.has(id)).sort();
79
+ }
80
+
81
+ /**
82
+ * Formats annotation string for a node based on requested props.
83
+ * @param {Record<string, unknown>} nodeProps
84
+ * @param {string[]} propKeys
85
+ * @returns {string}
86
+ */
87
+ function formatAnnotation(nodeProps, propKeys) {
88
+ if (propKeys.length === 0 || !nodeProps) {
89
+ return '';
90
+ }
91
+ const parts = [];
92
+ for (const key of propKeys) {
93
+ if (Object.prototype.hasOwnProperty.call(nodeProps, key)) {
94
+ parts.push(`${key}: ${nodeProps[key]}`);
95
+ }
96
+ }
97
+ return parts.length > 0 ? ` [${parts.join(', ')}]` : '';
98
+ }
99
+
100
+ /**
101
+ * Renders a tree structure as lines with box-drawing characters.
102
+ * @param {object} params
103
+ * @param {string} params.nodeId
104
+ * @param {Map<string, Array<{id: string, label: string}>>} params.childMap
105
+ * @param {Map<string, Record<string, unknown>>} params.propsMap
106
+ * @param {string[]} params.propKeys
107
+ * @param {string} params.prefix
108
+ * @param {boolean} params.isLast
109
+ * @param {Set<string>} params.visited
110
+ * @param {number} params.depth
111
+ * @param {number|undefined} params.maxDepth
112
+ * @param {string[]} params.lines
113
+ */
114
+ function renderTreeNode({ nodeId, childMap, propsMap, propKeys, prefix, isLast, visited, depth, maxDepth, lines }) {
115
+ const connector = depth === 0 ? '' : (isLast ? '\u2514\u2500\u2500 ' : '\u251C\u2500\u2500 ');
116
+ const annotation = formatAnnotation(propsMap.get(nodeId) || {}, propKeys);
117
+ lines.push(`${prefix}${connector}${nodeId}${annotation}`);
118
+
119
+ if (visited.has(nodeId)) {
120
+ lines.push(`${prefix}${isLast ? ' ' : '\u2502 '} (cycle)`);
121
+ return;
122
+ }
123
+ visited.add(nodeId);
124
+
125
+ if (maxDepth !== undefined && depth >= maxDepth) {
126
+ const kids = childMap.get(nodeId);
127
+ if (kids && kids.length > 0) {
128
+ lines.push(`${prefix}${isLast ? ' ' : '\u2502 '} ... (${kids.length} children)`);
129
+ }
130
+ return;
131
+ }
132
+
133
+ const kids = childMap.get(nodeId) || [];
134
+ const childPrefix = depth === 0 ? '' : `${prefix}${isLast ? ' ' : '\u2502 '}`;
135
+ for (let i = 0; i < kids.length; i++) {
136
+ renderTreeNode({
137
+ nodeId: kids[i].id,
138
+ childMap,
139
+ propsMap,
140
+ propKeys,
141
+ prefix: childPrefix,
142
+ isLast: i === kids.length - 1,
143
+ visited,
144
+ depth: depth + 1,
145
+ maxDepth,
146
+ lines,
147
+ });
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Handles the `tree` command: ASCII tree output from graph edges.
153
+ * @param {{options: CliOptions, args: string[]}} params
154
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
155
+ */
156
+ export default async function handleTree({ options, args }) {
157
+ const { values, positionals } = parseCommandArgs(
158
+ args, TREE_OPTIONS, treeSchema, { allowPositionals: true },
159
+ );
160
+ const { graph, graphName, persistence } = await openGraph(options);
161
+ const cursorInfo = await applyCursorCeiling(graph, persistence, graphName);
162
+ emitCursorWarning(cursorInfo, null);
163
+
164
+ const queryResult = await graph.query().run();
165
+ const edges = await graph.getEdges();
166
+ const rootArg = positionals[0] || null;
167
+
168
+ const nodeIds = queryResult.nodes.map((/** @type {{id: string}} */ n) => n.id);
169
+ const propsMap = new Map(queryResult.nodes.map((/** @type {{id: string, props?: Record<string, unknown>}} */ n) => [n.id, n.props || {}]));
170
+ const childMap = buildChildMap(edges, values.edgeLabel);
171
+
172
+ const roots = rootArg ? [rootArg] : findRoots(nodeIds, edges, values.edgeLabel);
173
+
174
+ if (rootArg && !nodeIds.includes(rootArg)) {
175
+ throw usageError(`Node not found: ${rootArg}`);
176
+ }
177
+
178
+ /** @type {string[]} */
179
+ const lines = [];
180
+ for (const root of roots) {
181
+ renderTreeNode({
182
+ nodeId: root,
183
+ childMap,
184
+ propsMap,
185
+ propKeys: values.props,
186
+ prefix: '',
187
+ isLast: true,
188
+ visited: new Set(),
189
+ depth: 0,
190
+ maxDepth: values.maxDepth,
191
+ lines,
192
+ });
193
+ }
194
+
195
+ // Collect orphans (nodes not reachable from any root)
196
+ const reachable = new Set();
197
+ collectReachable(roots, childMap, reachable);
198
+ const orphans = nodeIds.filter((/** @type {string} */ id) => !reachable.has(id));
199
+
200
+ const payload = {
201
+ graph: graphName,
202
+ roots,
203
+ tree: lines.join('\n'),
204
+ orphanCount: orphans.length,
205
+ orphans: orphans.length > 0 ? orphans : undefined,
206
+ };
207
+
208
+ return { payload, exitCode: EXIT_CODES.OK };
209
+ }
210
+
211
+ /**
212
+ * Collects all reachable node IDs via DFS.
213
+ * @param {string[]} roots
214
+ * @param {Map<string, Array<{id: string, label: string}>>} childMap
215
+ * @param {Set<string>} reachable
216
+ */
217
+ function collectReachable(roots, childMap, reachable) {
218
+ const stack = [...roots];
219
+ while (stack.length > 0) {
220
+ const id = /** @type {string} */ (stack.pop());
221
+ if (reachable.has(id)) {
222
+ continue;
223
+ }
224
+ reachable.add(id);
225
+ const kids = childMap.get(id) || [];
226
+ for (const kid of kids) {
227
+ stack.push(kid.id);
228
+ }
229
+ }
230
+ }
@@ -49,7 +49,7 @@ function resolveTrustPin(cliPin) {
49
49
 
50
50
  /**
51
51
  * Discovers all writer IDs from the writers prefix refs.
52
- * @param {*} persistence
52
+ * @param {import('../types.js').Persistence} persistence
53
53
  * @param {string} graphName
54
54
  * @returns {Promise<string[]>}
55
55
  */
@@ -96,7 +96,7 @@ function buildNotConfiguredResult(graphName) {
96
96
 
97
97
  /**
98
98
  * @param {{options: CliOptions, args: string[]}} params
99
- * @returns {Promise<{payload: *, exitCode: number}>}
99
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
100
100
  */
101
101
  export default async function handleTrust({ options, args }) {
102
102
  const { mode, trustPin } = parseTrustArgs(args);
@@ -104,7 +104,7 @@ export default async function handleTrust({ options, args }) {
104
104
  const graphName = await resolveGraphName(persistence, options.graph);
105
105
 
106
106
  const recordService = new TrustRecordService({
107
- persistence: /** @type {*} TODO(ts-cleanup) */ (persistence),
107
+ persistence: /** @type {import('../../../src/domain/types/WarpPersistence.js').CorePersistence} */ (/** @type {unknown} */ (persistence)),
108
108
  codec: defaultCodec,
109
109
  });
110
110
 
@@ -46,20 +46,20 @@ export function parseVerifyAuditArgs(args) {
46
46
 
47
47
  /**
48
48
  * @param {{options: CliOptions, args: string[]}} params
49
- * @returns {Promise<{payload: *, exitCode: number}>}
49
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
50
50
  */
51
51
  export default async function handleVerifyAudit({ options, args }) {
52
52
  const { since, writerFilter, trustMode, trustPin } = parseVerifyAuditArgs(args);
53
53
  const { persistence } = await createPersistence(options.repo);
54
54
  const graphName = await resolveGraphName(persistence, options.graph);
55
55
  const verifier = new AuditVerifierService({
56
- persistence: /** @type {*} */ (persistence), // TODO(ts-cleanup): narrow port type
56
+ persistence: /** @type {import('../../../src/domain/types/WarpPersistence.js').CorePersistence} */ (/** @type {unknown} */ (persistence)),
57
57
  codec: defaultCodec,
58
58
  });
59
59
 
60
60
  const trustWarning = detectTrustWarning();
61
61
 
62
- /** @type {*} */ // TODO(ts-cleanup): type verify-audit payload
62
+ /** @type {Record<string, unknown>} */
63
63
  let payload;
64
64
  if (writerFilter !== undefined) {
65
65
  const chain = await verifier.verifyChain(graphName, writerFilter, { since });
@@ -88,7 +88,7 @@ export default async function handleVerifyAudit({ options, args }) {
88
88
  mode: trustMode,
89
89
  });
90
90
  payload.trustAssessment = trustAssessment;
91
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type catch
91
+ } catch (err) {
92
92
  if (trustMode === 'enforce') {
93
93
  throw err;
94
94
  }
@@ -96,14 +96,15 @@ export default async function handleVerifyAudit({ options, args }) {
96
96
  trustSchemaVersion: 1,
97
97
  mode: 'signed_evidence_v1',
98
98
  trustVerdict: 'error',
99
- error: err?.message ?? 'Trust evaluation failed',
99
+ error: err instanceof Error ? err.message : 'Trust evaluation failed',
100
100
  };
101
101
  }
102
102
  }
103
103
 
104
- const hasInvalid = payload.summary.invalid > 0;
104
+ const { summary, trustAssessment } = /** @type {{summary: {invalid: number}, trustAssessment?: {trustVerdict?: string}}} */ (payload);
105
+ const hasInvalid = summary.invalid > 0;
105
106
  const trustFailed = trustMode === 'enforce' &&
106
- payload.trustAssessment?.trustVerdict === 'fail';
107
+ trustAssessment?.trustVerdict === 'fail';
107
108
  return {
108
109
  payload,
109
110
  exitCode: trustFailed ? EXIT_CODES.TRUST_FAIL
@@ -11,7 +11,7 @@ const VIEW_OPTIONS = {
11
11
 
12
12
  /**
13
13
  * @param {{options: CliOptions, args: string[]}} params
14
- * @returns {Promise<{payload: *, exitCode: number}>}
14
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
15
15
  */
16
16
  export default async function handleView({ options, args }) {
17
17
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
@@ -29,10 +29,11 @@ export default async function handleView({ options, args }) {
29
29
  graph: options.graph || 'default',
30
30
  mode: viewMode,
31
31
  });
32
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
33
- const isMissing = err.code === 'ERR_MODULE_NOT_FOUND' || (err.message && err.message.includes('Cannot find module'));
34
- const isTui = err.specifier?.includes('git-warp-tui') ||
35
- /cannot find (?:package|module) ['"]@git-stunts\/git-warp-tui/i.test(err.message);
32
+ } catch (err) {
33
+ const errObj = /** @type {{code?: string, message?: string, specifier?: string}} */ (typeof err === 'object' && err !== null ? err : {});
34
+ const isMissing = errObj.code === 'ERR_MODULE_NOT_FOUND' || (errObj.message && errObj.message.includes('Cannot find module'));
35
+ const isTui = errObj.specifier?.includes('git-warp-tui') ||
36
+ /cannot find (?:package|module) ['"]@git-stunts\/git-warp-tui/i.test(errObj.message || '');
36
37
  if (isMissing && isTui) {
37
38
  throw usageError(
38
39
  'Interactive TUI requires @git-stunts/git-warp-tui.\n' +
@@ -11,6 +11,8 @@ export const EXIT_CODES = {
11
11
  INTERNAL: 3,
12
12
  /** Trust policy denial (enforce mode). */
13
13
  TRUST_FAIL: 4,
14
+ /** Valid result but negative (e.g. no path found). Follows grep convention. */
15
+ NO_MATCH: 1,
14
16
  };
15
17
 
16
18
  /**
@@ -22,9 +24,7 @@ export function getEnvVar(name) {
22
24
  if (typeof process !== 'undefined' && process.env) {
23
25
  return process.env[name];
24
26
  }
25
- // @ts-expect-error — Deno global is only present in Deno runtime
26
27
  if (typeof Deno !== 'undefined') {
27
- // @ts-expect-error — Deno global is only present in Deno runtime
28
28
  // eslint-disable-next-line no-undef
29
29
  try { return Deno.env.get(name); } catch { return undefined; }
30
30
  }
@@ -45,6 +45,8 @@ Commands:
45
45
  trust Evaluate writer trust from signed evidence
46
46
  materialize Materialize and checkpoint all graphs
47
47
  seek Time-travel: step through graph history by Lamport tick
48
+ patch Decode and inspect raw patches
49
+ tree ASCII tree traversal from root nodes
48
50
  view Interactive TUI graph browser (requires @git-stunts/git-warp-tui)
49
51
  install-hooks Install post-merge git hook
50
52
 
@@ -99,6 +101,18 @@ Seek options:
99
101
  --drop <name> Delete a saved cursor
100
102
  --diff Show structural diff (added/removed nodes, edges, props)
101
103
  --diff-limit <N> Max diff entries (default 2000)
104
+
105
+ Patch options:
106
+ show <sha> Decode and display a single patch as JSON
107
+ list List all patches sorted by Lamport clock
108
+ --writer <id> Filter by writer (list only)
109
+ --limit <n> Max entries to show (list only)
110
+
111
+ Tree options:
112
+ [rootNode] Root node id (auto-detected if omitted)
113
+ --edge <label> Follow only this edge label
114
+ --prop <key> Annotate nodes with this property (repeatable)
115
+ --max-depth <n> Maximum traversal depth
102
116
  `;
103
117
 
104
118
  /**
@@ -130,7 +144,7 @@ export function notFoundError(message) {
130
144
  return new CliError(message, { code: 'E_NOT_FOUND', exitCode: EXIT_CODES.NOT_FOUND });
131
145
  }
132
146
 
133
- export const KNOWN_COMMANDS = ['info', 'query', 'path', 'history', 'check', 'doctor', 'materialize', 'seek', 'verify-audit', 'trust', 'install-hooks', 'view'];
147
+ export const KNOWN_COMMANDS = ['info', 'query', 'path', 'history', 'check', 'doctor', 'materialize', 'seek', 'verify-audit', 'trust', 'patch', 'tree', 'install-hooks', 'view'];
134
148
 
135
149
  const BASE_OPTIONS = {
136
150
  repo: { type: 'string', short: 'r' },
@@ -272,17 +286,17 @@ export function parseArgs(argv) {
272
286
  const { baseArgs, command, commandArgs } = extractBaseArgs(argv);
273
287
  const processed = preprocessView(baseArgs);
274
288
 
275
- /** @type {*} */ // TODO(ts-cleanup): type parseArgs return
289
+ /** @type {{ values: Record<string, string|boolean|string[]|boolean[]|undefined>, positionals: string[] }} */
276
290
  let parsed;
277
291
  try {
278
292
  parsed = nodeParseArgs({
279
293
  args: processed,
280
- options: /** @type {*} */ (BASE_OPTIONS), // TODO(ts-cleanup): type parseArgs config
294
+ options: /** @type {import('node:util').ParseArgsConfig['options']} */ (BASE_OPTIONS),
281
295
  strict: true,
282
296
  allowPositionals: false,
283
297
  });
284
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type parseArgs error
285
- throw usageError(err.message);
298
+ } catch (err) {
299
+ throw usageError(err instanceof Error ? err.message : String(err));
286
300
  }
287
301
 
288
302
  const { values } = parsed;
@@ -312,22 +326,22 @@ export function parseArgs(argv) {
312
326
  * @returns {{values: *, positionals: string[]}}
313
327
  */
314
328
  export function parseCommandArgs(args, config, schema, { allowPositionals = false } = {}) {
315
- /** @type {*} */ // TODO(ts-cleanup): type parseArgs return
329
+ /** @type {{ values: Record<string, string|boolean|string[]|boolean[]|undefined>, positionals: string[] }} */
316
330
  let parsed;
317
331
  try {
318
332
  parsed = nodeParseArgs({
319
333
  args,
320
- options: /** @type {*} */ (config), // TODO(ts-cleanup): type parseArgs config
334
+ options: /** @type {import('node:util').ParseArgsConfig['options']} */ (config),
321
335
  strict: true,
322
336
  allowPositionals,
323
337
  });
324
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type parseArgs error
325
- throw usageError(err.message);
338
+ } catch (err) {
339
+ throw usageError(err instanceof Error ? err.message : String(err));
326
340
  }
327
341
 
328
342
  const result = schema.safeParse(parsed.values);
329
343
  if (!result.success) {
330
- const msg = result.error.issues.map((/** @type {*} */ issue) => issue.message).join('; '); // TODO(ts-cleanup): type Zod issue
344
+ const msg = result.error.issues.map((/** @type {{message: string}} */ issue) => issue.message).join('; ');
331
345
  throw usageError(msg);
332
346
  }
333
347
 
package/bin/cli/shared.js CHANGED
@@ -92,7 +92,7 @@ export async function openGraph(options) {
92
92
  throw notFoundError(`Graph not found: ${options.graph}`);
93
93
  }
94
94
  }
95
- const graph = /** @type {WarpGraphInstance} */ (/** @type {*} */ (await WarpGraph.open({ // TODO(ts-cleanup): narrow port type
95
+ const graph = /** @type {WarpGraphInstance} */ (/** @type {unknown} */ (await WarpGraph.open({
96
96
  persistence,
97
97
  graphName,
98
98
  writerId: options.writer,
@@ -183,7 +183,7 @@ export function createHookInstaller() {
183
183
  const templateDir = path.resolve(__dirname, '..', '..', 'scripts', 'hooks');
184
184
  const { version } = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', '..', 'package.json'), 'utf8'));
185
185
  return new HookInstaller({
186
- fs: /** @type {*} */ (fs), // TODO(ts-cleanup): narrow port type
186
+ fs: /** @type {import('../../src/domain/services/HookInstaller.js').FsAdapter} */ (/** @type {unknown} */ (fs)),
187
187
  execGitConfig: execGitConfigValue,
188
188
  version,
189
189
  templateDir,
package/bin/cli/types.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * @property {(sha: string) => Promise<boolean>} nodeExists
11
11
  * @property {(sha: string, coverageSha: string) => Promise<boolean>} isAncestor
12
12
  * @property {() => Promise<{ok: boolean}>} ping
13
- * @property {*} plumbing
13
+ * @property {unknown} plumbing
14
14
  */
15
15
 
16
16
  /**
@@ -19,18 +19,19 @@
19
19
  * @property {() => Promise<Array<{id: string}>>} getNodes
20
20
  * @property {() => Promise<Array<{from: string, to: string, label?: string}>>} getEdges
21
21
  * @property {() => Promise<string|null>} createCheckpoint
22
- * @property {() => *} query
22
+ * @property {() => QueryBuilderLike} query
23
23
  * @property {{ shortestPath: Function }} traverse
24
- * @property {(writerId: string) => Promise<Array<{patch: any, sha: string}>>} getWriterPatches
25
- * @property {() => Promise<{frontier: Record<string, any>}>} status
26
- * @property {() => Promise<Map<string, any>>} getFrontier
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
27
28
  * @property {() => {totalTombstones: number, tombstoneRatio: number}} getGCMetrics
28
29
  * @property {() => Promise<number>} getPropertyCount
29
30
  * @property {() => Promise<import('../../src/domain/services/JoinReducer.js').WarpStateV5 | null>} getStateSnapshot
30
31
  * @property {() => Promise<{ticks: number[], maxTick: number, perWriter: Map<string, WriterTickInfo>}>} discoverTicks
31
- * @property {(sha: string) => Promise<{ops?: any[]}>} loadPatchBySha
32
- * @property {(cache: any) => void} setSeekCache
33
- * @property {*} seekCache
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
34
35
  * @property {number} [_seekCeiling]
35
36
  * @property {boolean} [_provenanceDegraded]
36
37
  */
@@ -82,4 +83,14 @@
82
83
  * @property {number} diffLimit
83
84
  */
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
+
85
96
  export {};
@@ -30,6 +30,8 @@ import {
30
30
  renderSeek,
31
31
  renderVerifyAudit,
32
32
  renderTrust,
33
+ renderPatchShow,
34
+ renderPatchList,
33
35
  } from './text.js';
34
36
 
35
37
  // ── Color control ────────────────────────────────────────────────────────────
@@ -61,8 +63,29 @@ export function shouldStripColor() {
61
63
 
62
64
  // ── Text renderer map ────────────────────────────────────────────────────────
63
65
 
64
- /** @type {Map<string, function(*): string>} */
65
- const TEXT_RENDERERS = new Map(/** @type {[string, function(*): string][]} */ ([
66
+ /** @param {import('./text.js').PatchShowPayload & Partial<import('./text.js').PatchListPayload>} payload */
67
+ function renderPatch(payload) {
68
+ if (payload.ops) {
69
+ return renderPatchShow(payload);
70
+ }
71
+ return renderPatchList(/** @type {import('./text.js').PatchListPayload} */ (payload));
72
+ }
73
+
74
+ /** @param {{ graph: string, tree?: string, orphanCount?: number, orphans?: string[] }} payload */
75
+ function renderTree(payload) {
76
+ const lines = [`Graph: ${payload.graph}`];
77
+ if (payload.tree) {
78
+ lines.push(payload.tree);
79
+ }
80
+ if (payload.orphanCount && payload.orphanCount > 0 && payload.orphans) {
81
+ lines.push('');
82
+ lines.push(`Orphans (${payload.orphanCount}): ${payload.orphans.join(', ')}`);
83
+ }
84
+ return `${lines.join('\n')}\n`;
85
+ }
86
+
87
+ /** @type {Map<string, function(unknown): string>} */
88
+ const TEXT_RENDERERS = new Map(/** @type {[string, function(unknown): string][]} */ ([
66
89
  ['info', renderInfo],
67
90
  ['query', renderQuery],
68
91
  ['path', renderPath],
@@ -73,11 +96,13 @@ const TEXT_RENDERERS = new Map(/** @type {[string, function(*): string][]} */ ([
73
96
  ['seek', renderSeek],
74
97
  ['verify-audit', renderVerifyAudit],
75
98
  ['trust', renderTrust],
99
+ ['patch', renderPatch],
100
+ ['tree', renderTree],
76
101
  ['install-hooks', renderInstallHooks],
77
102
  ]));
78
103
 
79
- /** @type {Map<string, function(*): string>} */
80
- const VIEW_RENDERERS = new Map(/** @type {[string, function(*): string][]} */ ([
104
+ /** @type {Map<string, function(unknown): string>} */
105
+ const VIEW_RENDERERS = new Map(/** @type {[string, function(unknown): string][]} */ ([
81
106
  ['info', renderInfoView],
82
107
  ['check', renderCheckView],
83
108
  ['history', renderHistoryView],
@@ -102,7 +127,7 @@ function writeHtmlExport(filePath, svgContent) {
102
127
 
103
128
  /**
104
129
  * Handles svg:PATH and html:PATH view modes for commands that carry _renderedSvg.
105
- * @param {*} payload
130
+ * @param {{ _renderedSvg?: string }} payload
106
131
  * @param {string} view
107
132
  * @returns {boolean} true if handled
108
133
  */
@@ -146,13 +171,13 @@ function writeText(text, strip) {
146
171
  /**
147
172
  * Writes a command result to stdout/stderr in the requested format.
148
173
  *
149
- * @param {*} payload - Command result payload
174
+ * @param {Record<string, unknown>} payload - Command result payload
150
175
  * @param {{format: string, command: string, view: string|null|boolean}} options
151
176
  */
152
177
  export function present(payload, { format, command, view }) {
153
178
  // Error payloads always go to stderr as plain text
154
179
  if (payload?.error) {
155
- process.stderr.write(renderError(payload));
180
+ process.stderr.write(renderError(/** @type {import('./text.js').ErrorPayload} */ (payload)));
156
181
  return;
157
182
  }
158
183
 
@@ -186,7 +211,7 @@ export function present(payload, { format, command, view }) {
186
211
 
187
212
  /**
188
213
  * Handles --view output dispatch (ASCII view, SVG file, HTML file).
189
- * @param {*} payload
214
+ * @param {Record<string, unknown>} payload
190
215
  * @param {string} command
191
216
  * @param {string|boolean} view
192
217
  */
@@ -200,7 +225,8 @@ function presentView(payload, command, view) {
200
225
 
201
226
  // query is special: uses pre-rendered _renderedAscii
202
227
  if (command === 'query') {
203
- writeText(`${payload._renderedAscii ?? ''}\n`, strip);
228
+ const ascii = typeof payload._renderedAscii === 'string' ? payload._renderedAscii : '';
229
+ writeText(`${ascii}\n`, strip);
204
230
  return;
205
231
  }
206
232
 
@@ -8,18 +8,19 @@
8
8
 
9
9
  /**
10
10
  * Recursively sorts object keys for deterministic JSON output.
11
- * @param {*} input
12
- * @returns {*}
11
+ * @param {unknown} input
12
+ * @returns {unknown}
13
13
  */
14
14
  function normalize(input) {
15
15
  if (Array.isArray(input)) {
16
16
  return input.map(normalize);
17
17
  }
18
18
  if (input && typeof input === 'object') {
19
- /** @type {Record<string, *>} */
19
+ const rec = /** @type {Record<string, unknown>} */ (input);
20
+ /** @type {Record<string, unknown>} */
20
21
  const sorted = {};
21
- for (const key of Object.keys(input).sort()) {
22
- sorted[key] = normalize(input[key]);
22
+ for (const key of Object.keys(rec).sort()) {
23
+ sorted[key] = normalize(rec[key]);
23
24
  }
24
25
  return sorted;
25
26
  }
@@ -28,7 +29,7 @@ function normalize(input) {
28
29
 
29
30
  /**
30
31
  * Pretty-printed JSON with sorted keys (2-space indent).
31
- * @param {*} value
32
+ * @param {unknown} value
32
33
  * @returns {string}
33
34
  */
34
35
  export function stableStringify(value) {
@@ -37,7 +38,7 @@ export function stableStringify(value) {
37
38
 
38
39
  /**
39
40
  * Single-line JSON with sorted keys (no indent).
40
- * @param {*} value
41
+ * @param {unknown} value
41
42
  * @returns {string}
42
43
  */
43
44
  export function compactStringify(value) {
@@ -48,18 +49,19 @@ export function compactStringify(value) {
48
49
  * Shallow-clones a payload, removing all top-level underscore-prefixed keys.
49
50
  * These are internal rendering artifacts (e.g. _renderedSvg, _renderedAscii)
50
51
  * that should not leak into JSON/NDJSON output.
51
- * @param {*} payload
52
- * @returns {*}
52
+ * @param {Record<string, unknown> | unknown} payload
53
+ * @returns {Record<string, unknown> | unknown}
53
54
  */
54
55
  export function sanitizePayload(payload) {
55
56
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
56
57
  return payload;
57
58
  }
58
- /** @type {Record<string, *>} */
59
+ const rec = /** @type {Record<string, unknown>} */ (payload);
60
+ /** @type {Record<string, unknown>} */
59
61
  const clean = {};
60
- for (const key of Object.keys(payload)) {
62
+ for (const key of Object.keys(rec)) {
61
63
  if (!key.startsWith('_')) {
62
- clean[key] = payload[key];
64
+ clean[key] = rec[key];
63
65
  }
64
66
  }
65
67
  return clean;