@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/README.md
CHANGED
|
@@ -328,6 +328,29 @@ const sha = await (await graph.createPatch())
|
|
|
328
328
|
|
|
329
329
|
Each `commit()` creates one Git commit containing all the operations, advances the writer's Lamport clock, and updates the writer's ref via compare-and-swap.
|
|
330
330
|
|
|
331
|
+
### Content Attachment
|
|
332
|
+
|
|
333
|
+
Attach content-addressed blobs to nodes and edges as first-class payloads (Paper I `Atom(p)`). Blobs are stored in the Git object store, referenced by SHA, and inherit CRDT merge, time-travel, and observer scoping automatically.
|
|
334
|
+
|
|
335
|
+
```javascript
|
|
336
|
+
const patch = await graph.createPatch();
|
|
337
|
+
patch.addNode('adr:0007'); // sync — queues a NodeAdd op
|
|
338
|
+
await patch.attachContent('adr:0007', '# ADR 0007\n\nDecision text...'); // async — writes blob
|
|
339
|
+
await patch.commit();
|
|
340
|
+
|
|
341
|
+
// Read content back
|
|
342
|
+
const buffer = await graph.getContent('adr:0007'); // Buffer | null
|
|
343
|
+
const oid = await graph.getContentOid('adr:0007'); // hex SHA or null
|
|
344
|
+
|
|
345
|
+
// Edge content works the same way (assumes nodes and edge already exist)
|
|
346
|
+
const patch2 = await graph.createPatch();
|
|
347
|
+
await patch2.attachEdgeContent('a', 'b', 'rel', 'edge payload');
|
|
348
|
+
await patch2.commit();
|
|
349
|
+
const edgeBuf = await graph.getEdgeContent('a', 'b', 'rel');
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Content blobs survive `git gc` — their OIDs are embedded in the patch commit tree and checkpoint tree, keeping them reachable.
|
|
353
|
+
|
|
331
354
|
### Writer API
|
|
332
355
|
|
|
333
356
|
For repeated writes, the Writer API is more convenient:
|
|
@@ -508,7 +531,7 @@ npm run test:matrix # All runtimes in parallel
|
|
|
508
531
|
## When NOT to Use It
|
|
509
532
|
|
|
510
533
|
- **High-throughput transactional workloads.** If you need thousands of writes per second with immediate consistency, use Postgres or Redis.
|
|
511
|
-
- **Large binary or blob storage.**
|
|
534
|
+
- **Large binary or blob storage.** Properties live in Git commit messages; content blobs live in the Git object store. Neither is optimized for large media files. Use object storage for images or videos.
|
|
512
535
|
- **Sub-millisecond read latency.** Materialization has overhead. Use an in-memory database for real-time gaming physics or HFT.
|
|
513
536
|
- **Simple key-value storage.** If you don't have relationships or need traversals, a graph database is overkill.
|
|
514
537
|
- **Non-Git environments.** The value proposition depends on Git infrastructure (push/pull, content-addressing).
|
|
@@ -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 {
|
|
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:
|
|
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 {
|
|
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
|
|
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 {
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
370
|
+
} catch (err) {
|
|
371
371
|
return internalError('hooks-installed', err);
|
|
372
372
|
}
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
/**
|
|
376
|
-
* @param {
|
|
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 (
|
|
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
|
|
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
|
|
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 {
|
|
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:
|
|
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 {
|
|
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 {
|
|
57
|
-
.map((/** @type {
|
|
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 {
|
|
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 {
|
|
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 (
|
|
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:
|
|
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:
|
|
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) =>
|
|
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
|
+
}
|
package/bin/cli/commands/path.js
CHANGED
|
@@ -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:
|
|
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.
|
|
80
|
+
exitCode: result.found ? EXIT_CODES.OK : EXIT_CODES.NO_MATCH,
|
|
81
81
|
};
|
|
82
|
-
} catch (
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
132
|
-
* @
|
|
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
|
|
179
|
+
nodes,
|
|
139
180
|
};
|
|
140
181
|
}
|
|
141
182
|
|
|
142
|
-
/** @param {
|
|
183
|
+
/** @param {unknown} error */
|
|
143
184
|
function mapQueryError(error) {
|
|
144
|
-
if (error &&
|
|
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:
|
|
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
|
|
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
|
]));
|