@git-stunts/git-warp 11.2.1 → 11.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -1
- package/bin/cli/commands/check.js +2 -2
- package/bin/cli/commands/doctor/checks.js +12 -12
- package/bin/cli/commands/doctor/index.js +2 -2
- package/bin/cli/commands/doctor/types.js +1 -1
- package/bin/cli/commands/history.js +12 -5
- package/bin/cli/commands/install-hooks.js +5 -5
- package/bin/cli/commands/materialize.js +2 -2
- package/bin/cli/commands/patch.js +142 -0
- package/bin/cli/commands/path.js +4 -4
- package/bin/cli/commands/query.js +54 -13
- package/bin/cli/commands/registry.js +4 -0
- package/bin/cli/commands/seek.js +17 -11
- package/bin/cli/commands/tree.js +230 -0
- package/bin/cli/commands/trust.js +3 -3
- package/bin/cli/commands/verify-audit.js +8 -7
- package/bin/cli/commands/view.js +6 -5
- package/bin/cli/infrastructure.js +26 -12
- package/bin/cli/shared.js +2 -2
- package/bin/cli/types.js +19 -8
- package/bin/presenters/index.js +35 -9
- package/bin/presenters/json.js +14 -12
- package/bin/presenters/text.js +155 -33
- package/index.d.ts +118 -22
- package/index.js +2 -0
- package/package.json +5 -3
- package/src/domain/WarpGraph.js +4 -1
- package/src/domain/crdt/ORSet.js +8 -8
- package/src/domain/errors/EmptyMessageError.js +2 -2
- package/src/domain/errors/ForkError.js +1 -1
- package/src/domain/errors/IndexError.js +1 -1
- package/src/domain/errors/OperationAbortedError.js +1 -1
- package/src/domain/errors/QueryError.js +1 -1
- package/src/domain/errors/SchemaUnsupportedError.js +1 -1
- package/src/domain/errors/ShardCorruptionError.js +2 -2
- package/src/domain/errors/ShardLoadError.js +2 -2
- package/src/domain/errors/ShardValidationError.js +4 -4
- package/src/domain/errors/StorageError.js +2 -2
- package/src/domain/errors/SyncError.js +1 -1
- package/src/domain/errors/TraversalError.js +1 -1
- package/src/domain/errors/TrustError.js +1 -1
- package/src/domain/errors/WarpError.js +2 -2
- package/src/domain/errors/WormholeError.js +1 -1
- package/src/domain/services/AuditReceiptService.js +6 -6
- package/src/domain/services/AuditVerifierService.js +52 -38
- package/src/domain/services/BitmapIndexBuilder.js +3 -3
- package/src/domain/services/BitmapIndexReader.js +28 -19
- package/src/domain/services/BoundaryTransitionRecord.js +18 -17
- package/src/domain/services/CheckpointSerializerV5.js +17 -16
- package/src/domain/services/CheckpointService.js +22 -3
- package/src/domain/services/CommitDagTraversalService.js +13 -13
- package/src/domain/services/DagPathFinding.js +7 -7
- package/src/domain/services/DagTopology.js +1 -1
- package/src/domain/services/DagTraversal.js +1 -1
- package/src/domain/services/HealthCheckService.js +1 -1
- package/src/domain/services/HookInstaller.js +1 -1
- package/src/domain/services/HttpSyncServer.js +92 -41
- package/src/domain/services/IndexRebuildService.js +7 -7
- package/src/domain/services/IndexStalenessChecker.js +4 -3
- package/src/domain/services/JoinReducer.js +26 -11
- package/src/domain/services/KeyCodec.js +7 -0
- package/src/domain/services/LogicalTraversal.js +1 -1
- package/src/domain/services/MessageCodecInternal.js +1 -1
- package/src/domain/services/MigrationService.js +1 -1
- package/src/domain/services/ObserverView.js +8 -8
- package/src/domain/services/PatchBuilderV2.js +96 -30
- package/src/domain/services/ProvenanceIndex.js +1 -1
- package/src/domain/services/ProvenancePayload.js +1 -1
- package/src/domain/services/QueryBuilder.js +3 -3
- package/src/domain/services/StateDiff.js +14 -11
- package/src/domain/services/StateSerializerV5.js +2 -2
- package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
- package/src/domain/services/SyncAuthService.js +3 -2
- package/src/domain/services/SyncProtocol.js +25 -11
- package/src/domain/services/TemporalQuery.js +9 -6
- package/src/domain/services/TranslationCost.js +7 -5
- package/src/domain/services/WormholeService.js +16 -7
- package/src/domain/trust/TrustCanonical.js +3 -3
- package/src/domain/trust/TrustEvaluator.js +18 -3
- package/src/domain/trust/TrustRecordService.js +30 -23
- package/src/domain/trust/TrustStateBuilder.js +21 -8
- package/src/domain/trust/canonical.js +6 -6
- package/src/domain/types/TickReceipt.js +1 -1
- package/src/domain/types/WarpErrors.js +45 -0
- package/src/domain/types/WarpOptions.js +29 -0
- package/src/domain/types/WarpPersistence.js +41 -0
- package/src/domain/types/WarpTypes.js +2 -2
- package/src/domain/types/WarpTypesV2.js +2 -2
- package/src/domain/utils/MinHeap.js +6 -5
- package/src/domain/utils/canonicalStringify.js +5 -4
- package/src/domain/utils/roaring.js +31 -5
- package/src/domain/warp/PatchSession.js +40 -18
- package/src/domain/warp/_wiredMethods.d.ts +199 -45
- package/src/domain/warp/checkpoint.methods.js +5 -1
- package/src/domain/warp/fork.methods.js +2 -2
- package/src/domain/warp/materialize.methods.js +55 -5
- package/src/domain/warp/materializeAdvanced.methods.js +15 -4
- package/src/domain/warp/patch.methods.js +54 -29
- package/src/domain/warp/provenance.methods.js +5 -3
- package/src/domain/warp/query.methods.js +89 -6
- package/src/domain/warp/sync.methods.js +16 -11
- package/src/globals.d.ts +64 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
- package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
- package/src/infrastructure/adapters/GitGraphAdapter.js +18 -13
- package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
- package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
- package/src/visualization/layouts/converters.js +2 -2
- package/src/visualization/layouts/elkAdapter.js +1 -1
- package/src/visualization/layouts/elkLayout.js +10 -7
- package/src/visualization/layouts/index.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +16 -6
- package/src/visualization/renderers/svg/index.js +1 -1
package/bin/cli/commands/seek.js
CHANGED
|
@@ -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:
|
|
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
|
|
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:
|
|
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 =
|
|
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 {
|
|
319
|
+
* @param {StateDiffResult} diff
|
|
314
320
|
* @param {string} diffBaseline
|
|
315
321
|
* @param {number|null} baselineTick
|
|
316
322
|
* @param {number} diffLimit
|
|
317
|
-
* @returns {{structuralDiff:
|
|
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 {
|
|
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:
|
|
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:
|
|
420
|
+
* @returns {Promise<{payload: unknown, exitCode: number}>}
|
|
415
421
|
*/
|
|
416
422
|
export default async function handleSeek({ options, args }) {
|
|
417
423
|
const seekSpec = parseSeekArgs(args);
|
|
@@ -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 {
|
|
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:
|
|
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 {
|
|
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:
|
|
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 {
|
|
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 {
|
|
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 (
|
|
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
|
|
99
|
+
error: err instanceof Error ? err.message : 'Trust evaluation failed',
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
const
|
|
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
|
-
|
|
107
|
+
trustAssessment?.trustVerdict === 'fail';
|
|
107
108
|
return {
|
|
108
109
|
payload,
|
|
109
110
|
exitCode: trustFailed ? EXIT_CODES.TRUST_FAIL
|
package/bin/cli/commands/view.js
CHANGED
|
@@ -11,7 +11,7 @@ const VIEW_OPTIONS = {
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* @param {{options: CliOptions, args: string[]}} params
|
|
14
|
-
* @returns {Promise<{payload:
|
|
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 (
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
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 {
|
|
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 {
|
|
294
|
+
options: /** @type {import('node:util').ParseArgsConfig['options']} */ (BASE_OPTIONS),
|
|
281
295
|
strict: true,
|
|
282
296
|
allowPositionals: false,
|
|
283
297
|
});
|
|
284
|
-
} catch (
|
|
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 {
|
|
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 {
|
|
334
|
+
options: /** @type {import('node:util').ParseArgsConfig['options']} */ (config),
|
|
321
335
|
strict: true,
|
|
322
336
|
allowPositionals,
|
|
323
337
|
});
|
|
324
|
-
} catch (
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {() =>
|
|
22
|
+
* @property {() => QueryBuilderLike} query
|
|
23
23
|
* @property {{ shortestPath: Function }} traverse
|
|
24
|
-
* @property {(writerId: string) => Promise<Array<{patch:
|
|
25
|
-
* @property {() => Promise<{frontier: Record<string,
|
|
26
|
-
* @property {() => Promise<
|
|
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?:
|
|
32
|
-
* @property {(cache:
|
|
33
|
-
* @property {
|
|
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 {};
|