@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,339 @@
1
+ /**
2
+ * Advanced materialization methods for WarpGraph — ceiling-aware replay,
3
+ * checkpoint-based materializeAt, adjacency building, and state caching.
4
+ *
5
+ * Every function uses `this` bound to a WarpGraph instance at runtime
6
+ * via wireWarpMethods().
7
+ *
8
+ * @module domain/warp/materializeAdvanced.methods
9
+ */
10
+
11
+ import { reduceV5, createEmptyStateV5 } from '../services/JoinReducer.js';
12
+ import { orsetContains, orsetElements } from '../crdt/ORSet.js';
13
+ import { decodeEdgeKey } from '../services/KeyCodec.js';
14
+ import { vvClone } from '../crdt/VersionVector.js';
15
+ import { computeStateHashV5 } from '../services/StateSerializerV5.js';
16
+ import { ProvenanceIndex } from '../services/ProvenanceIndex.js';
17
+ import { serializeFullStateV5, deserializeFullStateV5 } from '../services/CheckpointSerializerV5.js';
18
+ import { buildSeekCacheKey } from '../utils/seekCacheKey.js';
19
+ import { materializeIncremental } from '../services/CheckpointService.js';
20
+ import { createFrontier, updateFrontier } from '../services/Frontier.js';
21
+ import { buildWriterRef } from '../utils/RefLayout.js';
22
+ import { decodePatchMessage, detectMessageKind } from '../services/WarpMessageCodec.js';
23
+
24
+ /**
25
+ * Resolves the effective ceiling from options and instance state.
26
+ *
27
+ * Precedence: explicit `ceiling` in options overrides the instance-level
28
+ * `_seekCeiling`. Uses the `'ceiling' in options` check, so passing
29
+ * `{ ceiling: null }` explicitly clears the seek ceiling for that call
30
+ * (returns `null`), while omitting the key falls through to `_seekCeiling`.
31
+ *
32
+ * @this {import('../WarpGraph.js').default}
33
+ * @param {{ceiling?: number|null}} [options] - Options object; when the
34
+ * `ceiling` key is present (even if `null`), its value takes precedence
35
+ * @returns {number|null} Lamport ceiling to apply, or `null` for latest
36
+ * @private
37
+ */
38
+ export function _resolveCeiling(options) {
39
+ if (options && 'ceiling' in options) {
40
+ return options.ceiling ?? null;
41
+ }
42
+ return this._seekCeiling;
43
+ }
44
+
45
+ /**
46
+ * Builds a deterministic adjacency map for the logical graph.
47
+ *
48
+ * @this {import('../WarpGraph.js').default}
49
+ * @param {import('../services/JoinReducer.js').WarpStateV5} state
50
+ * @returns {{outgoing: Map<string, Array<{neighborId: string, label: string}>>, incoming: Map<string, Array<{neighborId: string, label: string}>>}}
51
+ * @private
52
+ */
53
+ export function _buildAdjacency(state) {
54
+ const outgoing = new Map();
55
+ const incoming = new Map();
56
+
57
+ for (const edgeKey of orsetElements(state.edgeAlive)) {
58
+ const { from, to, label } = decodeEdgeKey(edgeKey);
59
+
60
+ if (!orsetContains(state.nodeAlive, from) || !orsetContains(state.nodeAlive, to)) {
61
+ continue;
62
+ }
63
+
64
+ if (!outgoing.has(from)) {
65
+ outgoing.set(from, []);
66
+ }
67
+ if (!incoming.has(to)) {
68
+ incoming.set(to, []);
69
+ }
70
+
71
+ outgoing.get(from).push({ neighborId: to, label });
72
+ incoming.get(to).push({ neighborId: from, label });
73
+ }
74
+
75
+ const sortNeighbors = (/** @type {Array<{neighborId: string, label: string}>} */ list) => {
76
+ list.sort((/** @type {{neighborId: string, label: string}} */ a, /** @type {{neighborId: string, label: string}} */ b) => {
77
+ if (a.neighborId !== b.neighborId) {
78
+ return a.neighborId < b.neighborId ? -1 : 1;
79
+ }
80
+ return a.label < b.label ? -1 : a.label > b.label ? 1 : 0;
81
+ });
82
+ };
83
+
84
+ for (const list of outgoing.values()) {
85
+ sortNeighbors(list);
86
+ }
87
+
88
+ for (const list of incoming.values()) {
89
+ sortNeighbors(list);
90
+ }
91
+
92
+ return { outgoing, incoming };
93
+ }
94
+
95
+ /**
96
+ * Sets the cached state and materialized graph details.
97
+ *
98
+ * @this {import('../WarpGraph.js').default}
99
+ * @param {import('../services/JoinReducer.js').WarpStateV5} state
100
+ * @returns {Promise<{state: any, stateHash: string, adjacency: any}>}
101
+ * @private
102
+ */
103
+ export async function _setMaterializedState(state) {
104
+ this._cachedState = state;
105
+ this._stateDirty = false;
106
+ this._versionVector = vvClone(state.observedFrontier);
107
+
108
+ const stateHash = await computeStateHashV5(state, { crypto: this._crypto, codec: this._codec });
109
+ let adjacency;
110
+
111
+ if (this._adjacencyCache) {
112
+ adjacency = this._adjacencyCache.get(stateHash);
113
+ if (!adjacency) {
114
+ adjacency = this._buildAdjacency(state);
115
+ this._adjacencyCache.set(stateHash, adjacency);
116
+ }
117
+ } else {
118
+ adjacency = this._buildAdjacency(state);
119
+ }
120
+
121
+ this._materializedGraph = { state, stateHash, adjacency };
122
+ return this._materializedGraph;
123
+ }
124
+
125
+ /**
126
+ * Materializes the graph with a Lamport ceiling (time-travel).
127
+ *
128
+ * Bypasses checkpoints entirely — replays all patches from all writers,
129
+ * filtering to only those with `lamport <= ceiling`. Skips auto-checkpoint
130
+ * and GC since this is an exploratory read.
131
+ *
132
+ * Uses a dedicated cache keyed on `ceiling` + frontier snapshot. Cache
133
+ * is bypassed when the writer frontier has advanced (new writers or
134
+ * updated tips) or when `collectReceipts` is `true` because the cached
135
+ * path does not retain receipt data.
136
+ *
137
+ * @this {import('../WarpGraph.js').default}
138
+ * @param {number} ceiling - Maximum Lamport tick to include (patches with
139
+ * `lamport <= ceiling` are replayed; `ceiling <= 0` yields empty state)
140
+ * @param {boolean} collectReceipts - When `true`, return receipts alongside
141
+ * state and skip the ceiling cache
142
+ * @param {number} t0 - Start timestamp for performance logging
143
+ * @returns {Promise<import('../services/JoinReducer.js').WarpStateV5 |
144
+ * {state: import('../services/JoinReducer.js').WarpStateV5,
145
+ * receipts: import('../types/TickReceipt.js').TickReceipt[]}>}
146
+ * Plain state when `collectReceipts` is falsy; `{ state, receipts }`
147
+ * when truthy
148
+ * @private
149
+ */
150
+ export async function _materializeWithCeiling(ceiling, collectReceipts, t0) {
151
+ const frontier = await this.getFrontier();
152
+
153
+ // Cache hit: same ceiling, clean state, AND frontier unchanged.
154
+ // Bypass cache when collectReceipts is true — cached path has no receipts.
155
+ const cf = this._cachedFrontier;
156
+ if (
157
+ this._cachedState && !this._stateDirty &&
158
+ ceiling === this._cachedCeiling && !collectReceipts &&
159
+ cf !== null &&
160
+ cf.size === frontier.size &&
161
+ [...frontier].every(([w, sha]) => cf.get(w) === sha)
162
+ ) {
163
+ return this._cachedState;
164
+ }
165
+
166
+ const writerIds = [...frontier.keys()];
167
+
168
+ if (writerIds.length === 0 || ceiling <= 0) {
169
+ const state = createEmptyStateV5();
170
+ this._provenanceIndex = new ProvenanceIndex();
171
+ this._provenanceDegraded = false;
172
+ await this._setMaterializedState(state);
173
+ this._cachedCeiling = ceiling;
174
+ this._cachedFrontier = frontier;
175
+ this._logTiming('materialize', t0, { metrics: '0 patches (ceiling)' });
176
+ if (collectReceipts) {
177
+ return { state, receipts: [] };
178
+ }
179
+ return state;
180
+ }
181
+
182
+ // Persistent cache check — skip when collectReceipts is requested
183
+ let cacheKey;
184
+ if (this._seekCache && !collectReceipts) {
185
+ cacheKey = buildSeekCacheKey(ceiling, frontier);
186
+ try {
187
+ const cached = await this._seekCache.get(cacheKey);
188
+ if (cached) {
189
+ try {
190
+ const state = deserializeFullStateV5(cached, { codec: this._codec });
191
+ this._provenanceIndex = new ProvenanceIndex();
192
+ this._provenanceDegraded = true;
193
+ await this._setMaterializedState(state);
194
+ this._cachedCeiling = ceiling;
195
+ this._cachedFrontier = frontier;
196
+ this._logTiming('materialize', t0, { metrics: `cache hit (ceiling=${ceiling})` });
197
+ return state;
198
+ } catch {
199
+ // Corrupted payload — self-heal by removing the bad entry
200
+ try { await this._seekCache.delete(cacheKey); } catch { /* best-effort */ }
201
+ }
202
+ }
203
+ } catch {
204
+ // Cache read failed — fall through to full materialization
205
+ }
206
+ }
207
+
208
+ const allPatches = [];
209
+ for (const writerId of writerIds) {
210
+ const writerPatches = await this._loadWriterPatches(writerId);
211
+ for (const entry of writerPatches) {
212
+ if (entry.patch.lamport <= ceiling) {
213
+ allPatches.push(entry);
214
+ }
215
+ }
216
+ }
217
+
218
+ /** @type {import('../services/JoinReducer.js').WarpStateV5|undefined} */
219
+ let state;
220
+ /** @type {import('../types/TickReceipt.js').TickReceipt[]|undefined} */
221
+ let receipts;
222
+
223
+ if (allPatches.length === 0) {
224
+ state = createEmptyStateV5();
225
+ if (collectReceipts) {
226
+ receipts = [];
227
+ }
228
+ } else if (collectReceipts) {
229
+ const result = /** @type {{state: import('../services/JoinReducer.js').WarpStateV5, receipts: import('../types/TickReceipt.js').TickReceipt[]}} */ (reduceV5(/** @type {any} */ (allPatches), undefined, { receipts: true })); // TODO(ts-cleanup): type patch array
230
+ state = result.state;
231
+ receipts = result.receipts;
232
+ } else {
233
+ state = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {any} */ (allPatches))); // TODO(ts-cleanup): type patch array
234
+ }
235
+
236
+ this._provenanceIndex = new ProvenanceIndex();
237
+ for (const { patch, sha } of allPatches) {
238
+ this._provenanceIndex.addPatch(sha, /** @type {string[]|undefined} */ (patch.reads), /** @type {string[]|undefined} */ (patch.writes));
239
+ }
240
+ this._provenanceDegraded = false;
241
+
242
+ await this._setMaterializedState(state);
243
+ this._cachedCeiling = ceiling;
244
+ this._cachedFrontier = frontier;
245
+
246
+ // Store to persistent cache (fire-and-forget — failure is non-fatal)
247
+ if (this._seekCache && !collectReceipts && allPatches.length > 0) {
248
+ if (!cacheKey) {
249
+ cacheKey = buildSeekCacheKey(ceiling, frontier);
250
+ }
251
+ const buf = serializeFullStateV5(state, { codec: this._codec });
252
+ this._seekCache.set(cacheKey, /** @type {Buffer} */ (buf)).catch(() => {});
253
+ }
254
+
255
+ // Skip auto-checkpoint and GC — this is an exploratory read
256
+ this._logTiming('materialize', t0, { metrics: `${allPatches.length} patches (ceiling=${ceiling})` });
257
+
258
+ if (collectReceipts) {
259
+ return { state, receipts: /** @type {import('../types/TickReceipt.js').TickReceipt[]} */ (receipts) };
260
+ }
261
+ return state;
262
+ }
263
+
264
+ /**
265
+ * Materializes the graph state at a specific checkpoint.
266
+ *
267
+ * Loads the checkpoint state and frontier, discovers current writers,
268
+ * builds the target frontier from current writer tips, and applies
269
+ * incremental patches since the checkpoint.
270
+ *
271
+ * @this {import('../WarpGraph.js').default}
272
+ * @param {string} checkpointSha - The checkpoint commit SHA
273
+ * @returns {Promise<import('../services/JoinReducer.js').WarpStateV5>} The materialized graph state at the checkpoint
274
+ * @throws {Error} If checkpoint SHA is invalid or not found
275
+ * @throws {Error} If checkpoint loading or patch decoding fails
276
+ *
277
+ * @example
278
+ * // Time-travel to a previous checkpoint
279
+ * const oldState = await graph.materializeAt('abc123');
280
+ * console.log('Nodes at checkpoint:', orsetElements(oldState.nodeAlive));
281
+ */
282
+ export async function materializeAt(checkpointSha) {
283
+ // 1. Discover current writers to build target frontier
284
+ const writerIds = await this.discoverWriters();
285
+
286
+ // 2. Build target frontier (current tips for all writers)
287
+ const targetFrontier = createFrontier();
288
+ for (const writerId of writerIds) {
289
+ const writerRef = buildWriterRef(this._graphName, writerId);
290
+ const tipSha = await this._persistence.readRef(writerRef);
291
+ if (tipSha) {
292
+ updateFrontier(targetFrontier, writerId, tipSha);
293
+ }
294
+ }
295
+
296
+ // 3. Create a patch loader function for incremental materialization
297
+ const patchLoader = async (/** @type {string} */ writerId, /** @type {string|null} */ fromSha, /** @type {string} */ toSha) => {
298
+ // Load patches from fromSha (exclusive) to toSha (inclusive)
299
+ // Walk from toSha back to fromSha
300
+ const patches = [];
301
+ let currentSha = toSha;
302
+
303
+ while (currentSha && currentSha !== fromSha) {
304
+ const nodeInfo = await this._persistence.getNodeInfo(currentSha);
305
+ const {message} = nodeInfo;
306
+
307
+ const kind = detectMessageKind(message);
308
+ if (kind !== 'patch') {
309
+ break;
310
+ }
311
+
312
+ const patchMeta = decodePatchMessage(message);
313
+ const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
314
+ const patch = this._codec.decode(patchBuffer);
315
+
316
+ patches.push({ patch, sha: currentSha });
317
+
318
+ if (nodeInfo.parents && nodeInfo.parents.length > 0) {
319
+ currentSha = nodeInfo.parents[0];
320
+ } else {
321
+ break;
322
+ }
323
+ }
324
+
325
+ return patches.reverse();
326
+ };
327
+
328
+ // 4. Call materializeIncremental with the checkpoint and target frontier
329
+ const state = await materializeIncremental({
330
+ persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
331
+ graphName: this._graphName,
332
+ checkpointSha,
333
+ targetFrontier,
334
+ patchLoader,
335
+ codec: this._codec,
336
+ });
337
+ await this._setMaterializedState(state);
338
+ return state;
339
+ }