@git-stunts/git-warp 10.7.0 → 11.2.1

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 (71) hide show
  1. package/README.md +53 -32
  2. package/SECURITY.md +64 -0
  3. package/bin/cli/commands/check.js +168 -0
  4. package/bin/cli/commands/doctor/checks.js +422 -0
  5. package/bin/cli/commands/doctor/codes.js +46 -0
  6. package/bin/cli/commands/doctor/index.js +239 -0
  7. package/bin/cli/commands/doctor/types.js +89 -0
  8. package/bin/cli/commands/history.js +73 -0
  9. package/bin/cli/commands/info.js +139 -0
  10. package/bin/cli/commands/install-hooks.js +128 -0
  11. package/bin/cli/commands/materialize.js +99 -0
  12. package/bin/cli/commands/path.js +88 -0
  13. package/bin/cli/commands/query.js +194 -0
  14. package/bin/cli/commands/registry.js +28 -0
  15. package/bin/cli/commands/seek.js +592 -0
  16. package/bin/cli/commands/trust.js +154 -0
  17. package/bin/cli/commands/verify-audit.js +113 -0
  18. package/bin/cli/commands/view.js +45 -0
  19. package/bin/cli/infrastructure.js +336 -0
  20. package/bin/cli/schemas.js +177 -0
  21. package/bin/cli/shared.js +244 -0
  22. package/bin/cli/types.js +85 -0
  23. package/bin/presenters/index.js +214 -0
  24. package/bin/presenters/json.js +66 -0
  25. package/bin/presenters/text.js +543 -0
  26. package/bin/warp-graph.js +19 -2824
  27. package/index.d.ts +32 -2
  28. package/index.js +2 -0
  29. package/package.json +9 -7
  30. package/src/domain/WarpGraph.js +106 -3252
  31. package/src/domain/errors/QueryError.js +2 -2
  32. package/src/domain/errors/TrustError.js +29 -0
  33. package/src/domain/errors/index.js +1 -0
  34. package/src/domain/services/AuditMessageCodec.js +137 -0
  35. package/src/domain/services/AuditReceiptService.js +471 -0
  36. package/src/domain/services/AuditVerifierService.js +693 -0
  37. package/src/domain/services/HttpSyncServer.js +36 -22
  38. package/src/domain/services/MessageCodecInternal.js +3 -0
  39. package/src/domain/services/MessageSchemaDetector.js +2 -2
  40. package/src/domain/services/SyncAuthService.js +69 -3
  41. package/src/domain/services/WarpMessageCodec.js +4 -1
  42. package/src/domain/trust/TrustCanonical.js +42 -0
  43. package/src/domain/trust/TrustCrypto.js +111 -0
  44. package/src/domain/trust/TrustEvaluator.js +180 -0
  45. package/src/domain/trust/TrustRecordService.js +274 -0
  46. package/src/domain/trust/TrustStateBuilder.js +209 -0
  47. package/src/domain/trust/canonical.js +68 -0
  48. package/src/domain/trust/reasonCodes.js +64 -0
  49. package/src/domain/trust/schemas.js +160 -0
  50. package/src/domain/trust/verdict.js +42 -0
  51. package/src/domain/types/git-cas.d.ts +20 -0
  52. package/src/domain/utils/RefLayout.js +59 -0
  53. package/src/domain/warp/PatchSession.js +18 -0
  54. package/src/domain/warp/Writer.js +18 -3
  55. package/src/domain/warp/_internal.js +26 -0
  56. package/src/domain/warp/_wire.js +58 -0
  57. package/src/domain/warp/_wiredMethods.d.ts +100 -0
  58. package/src/domain/warp/checkpoint.methods.js +397 -0
  59. package/src/domain/warp/fork.methods.js +323 -0
  60. package/src/domain/warp/materialize.methods.js +188 -0
  61. package/src/domain/warp/materializeAdvanced.methods.js +339 -0
  62. package/src/domain/warp/patch.methods.js +529 -0
  63. package/src/domain/warp/provenance.methods.js +284 -0
  64. package/src/domain/warp/query.methods.js +279 -0
  65. package/src/domain/warp/subscribe.methods.js +272 -0
  66. package/src/domain/warp/sync.methods.js +549 -0
  67. package/src/infrastructure/adapters/GitGraphAdapter.js +67 -1
  68. package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
  69. package/src/ports/CommitPort.js +10 -0
  70. package/src/ports/RefPort.js +17 -0
  71. package/src/hooks/post-merge.sh +0 -60
@@ -0,0 +1,397 @@
1
+ /**
2
+ * Checkpoint, GC, and coverage methods for WarpGraph.
3
+ *
4
+ * Every function uses `this` bound to a WarpGraph instance at runtime
5
+ * via wireWarpMethods().
6
+ *
7
+ * @module domain/warp/checkpoint.methods
8
+ */
9
+
10
+ import { QueryError, E_NO_STATE_MSG } from './_internal.js';
11
+ import { buildWriterRef, buildCheckpointRef, buildCoverageRef } from '../utils/RefLayout.js';
12
+ import { createFrontier, updateFrontier } from '../services/Frontier.js';
13
+ import { loadCheckpoint, create as createCheckpointCommit } from '../services/CheckpointService.js';
14
+ import { decodePatchMessage, detectMessageKind, encodeAnchorMessage } from '../services/WarpMessageCodec.js';
15
+ import { shouldRunGC, executeGC } from '../services/GCPolicy.js';
16
+ import { collectGCMetrics } from '../services/GCMetrics.js';
17
+ import { computeAppliedVV } from '../services/CheckpointSerializerV5.js';
18
+
19
+ /**
20
+ * Creates a checkpoint of the current graph state.
21
+ *
22
+ * Discovers all writers, builds a frontier of writer tips, materializes
23
+ * the current state, and creates a checkpoint commit with provenance.
24
+ *
25
+ * @this {import('../WarpGraph.js').default}
26
+ * @returns {Promise<string>} The checkpoint commit SHA
27
+ * @throws {Error} If materialization or commit creation fails
28
+ */
29
+ export async function createCheckpoint() {
30
+ const t0 = this._clock.now();
31
+ try {
32
+ // 1. Discover all writers
33
+ const writers = await this.discoverWriters();
34
+
35
+ // 2. Build frontier (map of writerId → tip SHA)
36
+ const frontier = createFrontier();
37
+ const parents = [];
38
+
39
+ for (const writerId of writers) {
40
+ const writerRef = buildWriterRef(this._graphName, writerId);
41
+ const sha = await this._persistence.readRef(writerRef);
42
+ if (sha) {
43
+ updateFrontier(frontier, writerId, sha);
44
+ parents.push(sha);
45
+ }
46
+ }
47
+
48
+ // 3. Materialize current state (reuse cached if fresh, guard against recursion)
49
+ const prevCheckpointing = this._checkpointing;
50
+ this._checkpointing = true;
51
+ /** @type {import('../services/JoinReducer.js').WarpStateV5} */
52
+ let state;
53
+ try {
54
+ state = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ ((this._cachedState && !this._stateDirty)
55
+ ? this._cachedState
56
+ : await this.materialize());
57
+ } finally {
58
+ this._checkpointing = prevCheckpointing;
59
+ }
60
+
61
+ // 4. Call CheckpointService.create() with provenance index if available
62
+ const checkpointSha = await createCheckpointCommit({
63
+ persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
64
+ graphName: this._graphName,
65
+ state,
66
+ frontier,
67
+ parents,
68
+ provenanceIndex: this._provenanceIndex || undefined,
69
+ crypto: this._crypto,
70
+ codec: this._codec,
71
+ });
72
+
73
+ // 5. Update checkpoint ref
74
+ const checkpointRef = buildCheckpointRef(this._graphName);
75
+ await this._persistence.updateRef(checkpointRef, checkpointSha);
76
+
77
+ this._logTiming('createCheckpoint', t0);
78
+
79
+ // 6. Return checkpoint SHA
80
+ return checkpointSha;
81
+ } catch (err) {
82
+ this._logTiming('createCheckpoint', t0, { error: /** @type {Error} */ (err) });
83
+ throw err;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Syncs coverage information across writers.
89
+ *
90
+ * Creates an octopus anchor commit with all writer tips as parents,
91
+ * then updates the coverage ref to point to this anchor. The "octopus anchor"
92
+ * is a merge commit that records which writer tips have been observed,
93
+ * enabling efficient replication and consistency checks.
94
+ *
95
+ * @this {import('../WarpGraph.js').default}
96
+ * @returns {Promise<void>}
97
+ * @throws {Error} If ref access or commit creation fails
98
+ */
99
+ export async function syncCoverage() {
100
+ // 1. Discover all writers
101
+ const writers = await this.discoverWriters();
102
+
103
+ // If no writers exist, do nothing
104
+ if (writers.length === 0) {
105
+ return;
106
+ }
107
+
108
+ // 2. Get tip SHA for each writer's ref
109
+ const parents = [];
110
+ for (const writerId of writers) {
111
+ const writerRef = buildWriterRef(this._graphName, writerId);
112
+ const sha = await this._persistence.readRef(writerRef);
113
+ if (sha) {
114
+ parents.push(sha);
115
+ }
116
+ }
117
+
118
+ // If no refs have SHAs, do nothing
119
+ if (parents.length === 0) {
120
+ return;
121
+ }
122
+
123
+ // 3. Create octopus anchor commit with all tips as parents
124
+ const message = encodeAnchorMessage({ graph: this._graphName });
125
+ const anchorSha = await this._persistence.commitNode({ message, parents });
126
+
127
+ // 4. Update coverage ref
128
+ const coverageRef = buildCoverageRef(this._graphName);
129
+ await this._persistence.updateRef(coverageRef, anchorSha);
130
+ }
131
+
132
+ /**
133
+ * Loads the latest checkpoint for this graph.
134
+ *
135
+ * @this {import('../WarpGraph.js').default}
136
+ * @returns {Promise<{state: import('../services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, stateHash: string, schema: number, provenanceIndex?: import('../services/ProvenanceIndex.js').ProvenanceIndex}|null>} The checkpoint or null
137
+ * @private
138
+ */
139
+ export async function _loadLatestCheckpoint() {
140
+ const checkpointRef = buildCheckpointRef(this._graphName);
141
+ const checkpointSha = await this._persistence.readRef(checkpointRef);
142
+
143
+ if (!checkpointSha) {
144
+ return null;
145
+ }
146
+
147
+ try {
148
+ return await loadCheckpoint(this._persistence, checkpointSha, { codec: this._codec });
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Loads patches since a checkpoint for incremental materialization.
156
+ *
157
+ * @this {import('../WarpGraph.js').default}
158
+ * @param {{state: import('../services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, stateHash: string, schema: number}} checkpoint - The checkpoint to start from
159
+ * @returns {Promise<Array<{patch: import('../types/WarpTypesV2.js').PatchV2, sha: string}>>} Patches since checkpoint
160
+ * @private
161
+ */
162
+ export async function _loadPatchesSince(checkpoint) {
163
+ const writerIds = await this.discoverWriters();
164
+ const allPatches = [];
165
+
166
+ for (const writerId of writerIds) {
167
+ const checkpointSha = checkpoint.frontier?.get(writerId) || null;
168
+ const patches = await this._loadWriterPatches(writerId, checkpointSha);
169
+
170
+ // Validate each patch against checkpoint frontier
171
+ for (const { sha } of patches) {
172
+ await this._validatePatchAgainstCheckpoint(writerId, sha, checkpoint);
173
+ }
174
+
175
+ for (const p of patches) {
176
+ allPatches.push(p);
177
+ }
178
+ }
179
+
180
+ return allPatches;
181
+ }
182
+
183
+ /**
184
+ * Validates migration boundary for graphs.
185
+ *
186
+ * Graphs cannot be opened if there is schema:1 history without
187
+ * a migration checkpoint. This ensures data consistency during migration.
188
+ *
189
+ * @this {import('../WarpGraph.js').default}
190
+ * @returns {Promise<void>}
191
+ * @throws {Error} If v1 history exists without migration checkpoint
192
+ * @private
193
+ */
194
+ export async function _validateMigrationBoundary() {
195
+ const checkpoint = await this._loadLatestCheckpoint();
196
+ if (checkpoint?.schema === 2 || checkpoint?.schema === 3) {
197
+ return; // Already migrated
198
+ }
199
+
200
+ const hasSchema1History = await this._hasSchema1Patches();
201
+ if (hasSchema1History) {
202
+ throw new Error(
203
+ 'Cannot open graph with v1 history. ' +
204
+ 'Run MigrationService.migrate() first to create migration checkpoint.'
205
+ );
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Checks if there are any schema:1 patches in the graph.
211
+ *
212
+ * @this {import('../WarpGraph.js').default}
213
+ * @returns {Promise<boolean>} True if schema:1 patches exist
214
+ * @private
215
+ */
216
+ export async function _hasSchema1Patches() {
217
+ const writerIds = await this.discoverWriters();
218
+
219
+ for (const writerId of writerIds) {
220
+ const writerRef = buildWriterRef(this._graphName, writerId);
221
+ const tipSha = await this._persistence.readRef(writerRef);
222
+
223
+ if (!tipSha) {
224
+ continue;
225
+ }
226
+
227
+ // Check the first (most recent) patch from this writer
228
+ const nodeInfo = await this._persistence.getNodeInfo(tipSha);
229
+ const kind = detectMessageKind(nodeInfo.message);
230
+
231
+ if (kind === 'patch') {
232
+ const patchMeta = decodePatchMessage(nodeInfo.message);
233
+ const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
234
+ const patch = /** @type {{schema?: number}} */ (this._codec.decode(patchBuffer));
235
+
236
+ // If any patch has schema:1, we have v1 history
237
+ if (patch.schema === 1 || patch.schema === undefined) {
238
+ return true;
239
+ }
240
+ }
241
+ }
242
+
243
+ return false;
244
+ }
245
+
246
+ /**
247
+ * Post-materialize GC check. Warn by default; execute only when enabled.
248
+ * GC failure never breaks materialize.
249
+ *
250
+ * @this {import('../WarpGraph.js').default}
251
+ * @param {import('../services/JoinReducer.js').WarpStateV5} state
252
+ * @private
253
+ */
254
+ export function _maybeRunGC(state) {
255
+ try {
256
+ const metrics = collectGCMetrics(state);
257
+ /** @type {import('../services/GCPolicy.js').GCInputMetrics} */
258
+ const inputMetrics = {
259
+ ...metrics,
260
+ patchesSinceCompaction: this._patchesSinceGC,
261
+ timeSinceCompaction: this._lastGCTime > 0 ? this._clock.now() - this._lastGCTime : 0,
262
+ };
263
+ const { shouldRun, reasons } = shouldRunGC(inputMetrics, /** @type {import('../services/GCPolicy.js').GCPolicy} */ (this._gcPolicy));
264
+
265
+ if (!shouldRun) {
266
+ return;
267
+ }
268
+
269
+ if (/** @type {import('../services/GCPolicy.js').GCPolicy} */ (this._gcPolicy).enabled) {
270
+ const appliedVV = computeAppliedVV(state);
271
+ const result = executeGC(state, appliedVV);
272
+ this._lastGCTime = this._clock.now();
273
+ this._patchesSinceGC = 0;
274
+ if (this._logger) {
275
+ this._logger.info('Auto-GC completed', { ...result, reasons });
276
+ }
277
+ } else if (this._logger) {
278
+ this._logger.warn(
279
+ 'GC thresholds exceeded but auto-GC is disabled. Set gcPolicy: { enabled: true } to auto-compact.',
280
+ { reasons },
281
+ );
282
+ }
283
+ } catch {
284
+ // GC failure never breaks materialize
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Checks if GC should run based on current metrics and policy.
290
+ * If thresholds are exceeded, runs GC on the cached state.
291
+ *
292
+ * **Requires a cached state.**
293
+ *
294
+ * @this {import('../WarpGraph.js').default}
295
+ * @returns {{ran: boolean, result: Object|null, reasons: string[]}} GC result
296
+ *
297
+ * @example
298
+ * await graph.materialize();
299
+ * const { ran, result, reasons } = graph.maybeRunGC();
300
+ * if (ran) {
301
+ * console.log(`GC ran: ${result.tombstonesRemoved} tombstones removed`);
302
+ * }
303
+ */
304
+ export function maybeRunGC() {
305
+ if (!this._cachedState) {
306
+ return { ran: false, result: null, reasons: [] };
307
+ }
308
+
309
+ const rawMetrics = collectGCMetrics(this._cachedState);
310
+ /** @type {import('../services/GCPolicy.js').GCInputMetrics} */
311
+ const metrics = {
312
+ ...rawMetrics,
313
+ patchesSinceCompaction: this._patchesSinceGC,
314
+ timeSinceCompaction: this._lastGCTime > 0 ? this._clock.now() - this._lastGCTime : 0,
315
+ };
316
+
317
+ const { shouldRun, reasons } = shouldRunGC(metrics, /** @type {import('../services/GCPolicy.js').GCPolicy} */ (this._gcPolicy));
318
+
319
+ if (!shouldRun) {
320
+ return { ran: false, result: null, reasons: [] };
321
+ }
322
+
323
+ const result = this.runGC();
324
+ return { ran: true, result, reasons };
325
+ }
326
+
327
+ /**
328
+ * Explicitly runs GC on the cached state.
329
+ * Compacts tombstoned dots that are covered by the appliedVV.
330
+ *
331
+ * **Requires a cached state.**
332
+ *
333
+ * @this {import('../WarpGraph.js').default}
334
+ * @returns {{nodesCompacted: number, edgesCompacted: number, tombstonesRemoved: number, durationMs: number}}
335
+ * @throws {QueryError} If no cached state exists (code: `E_NO_STATE`)
336
+ *
337
+ * @example
338
+ * await graph.materialize();
339
+ * const result = graph.runGC();
340
+ * console.log(`Removed ${result.tombstonesRemoved} tombstones in ${result.durationMs}ms`);
341
+ */
342
+ export function runGC() {
343
+ const t0 = this._clock.now();
344
+ try {
345
+ if (!this._cachedState) {
346
+ throw new QueryError(E_NO_STATE_MSG, {
347
+ code: 'E_NO_STATE',
348
+ });
349
+ }
350
+
351
+ // Compute appliedVV from current state
352
+ const appliedVV = computeAppliedVV(this._cachedState);
353
+
354
+ // Execute GC (mutates cached state)
355
+ const result = executeGC(this._cachedState, appliedVV);
356
+
357
+ // Update GC tracking
358
+ this._lastGCTime = this._clock.now();
359
+ this._patchesSinceGC = 0;
360
+
361
+ this._logTiming('runGC', t0, { metrics: `${result.tombstonesRemoved} tombstones removed` });
362
+
363
+ return result;
364
+ } catch (err) {
365
+ this._logTiming('runGC', t0, { error: /** @type {Error} */ (err) });
366
+ throw err;
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Gets current GC metrics for the cached state.
372
+ *
373
+ * @this {import('../WarpGraph.js').default}
374
+ * @returns {{
375
+ * nodeCount: number,
376
+ * edgeCount: number,
377
+ * tombstoneCount: number,
378
+ * tombstoneRatio: number,
379
+ * patchesSinceCompaction: number,
380
+ * lastCompactionTime: number
381
+ * }|null} GC metrics or null if no cached state
382
+ */
383
+ export function getGCMetrics() {
384
+ if (!this._cachedState) {
385
+ return null;
386
+ }
387
+
388
+ const rawMetrics = collectGCMetrics(this._cachedState);
389
+ return {
390
+ nodeCount: rawMetrics.nodeLiveDots,
391
+ edgeCount: rawMetrics.edgeLiveDots,
392
+ tombstoneCount: rawMetrics.totalTombstones,
393
+ tombstoneRatio: rawMetrics.tombstoneRatio,
394
+ patchesSinceCompaction: this._patchesSinceGC,
395
+ lastCompactionTime: this._lastGCTime,
396
+ };
397
+ }