@git-stunts/git-warp 10.1.1 → 10.3.2

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/index.js CHANGED
@@ -1,5 +1,24 @@
1
+ /* @ts-self-types="./index.d.ts" */
2
+
1
3
  /**
2
- * @fileoverview Empty Graph - A graph database substrate using Git commits pointing to the empty tree.
4
+ * @module
5
+ *
6
+ * Deterministic WARP graph over Git: graph-native storage, traversal,
7
+ * and tooling. All graph state lives as Git commits pointing to the
8
+ * well-known empty tree — invisible to normal Git workflows, but
9
+ * inheriting content-addressing, cryptographic integrity, and
10
+ * distributed replication.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import WarpGraph from "@git-stunts/git-warp";
15
+ *
16
+ * const graph = await WarpGraph.open({ repo: ".", graphName: "myGraph" });
17
+ * const patch = await graph.createPatch("writer-1");
18
+ * patch.addNode("user:alice").setProperty("user:alice", "name", "Alice");
19
+ * await patch.commit();
20
+ * const state = await graph.materialize();
21
+ * ```
3
22
  */
4
23
 
5
24
  import GitGraphAdapter from './src/infrastructure/adapters/GitGraphAdapter.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@git-stunts/git-warp",
3
- "version": "10.1.1",
3
+ "version": "10.3.2",
4
4
  "description": "Deterministic WARP graph over Git: graph-native storage, traversal, and tooling.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -79,7 +79,12 @@
79
79
  "prepare": "patch-package && node scripts/setup-hooks.js",
80
80
  "prepack": "npm run lint && npm run test:local",
81
81
  "install:git-warp": "bash scripts/install-git-warp.sh",
82
- "uninstall:git-warp": "bash scripts/uninstall-git-warp.sh"
82
+ "uninstall:git-warp": "bash scripts/uninstall-git-warp.sh",
83
+ "test:node20": "docker compose -f docker-compose.test.yml --profile node20 run --build --rm test-node20",
84
+ "test:node22": "docker compose -f docker-compose.test.yml --profile node22 run --build --rm test-node22",
85
+ "test:bun": "docker compose -f docker-compose.test.yml --profile bun run --build --rm test-bun",
86
+ "test:deno": "docker compose -f docker-compose.test.yml --profile deno run --build --rm test-deno",
87
+ "test:matrix": "docker compose -f docker-compose.test.yml --profile full up --build --abort-on-container-exit"
83
88
  },
84
89
  "dependencies": {
85
90
  "@git-stunts/alfred": "^0.4.0",
@@ -178,6 +178,15 @@ export default class WarpGraph {
178
178
 
179
179
  /** @type {import('./services/TemporalQuery.js').TemporalQuery|null} */
180
180
  this._temporalQuery = null;
181
+
182
+ /** @type {number|null} */
183
+ this._seekCeiling = null;
184
+
185
+ /** @type {number|null} */
186
+ this._cachedCeiling = null;
187
+
188
+ /** @type {Map<string, string>|null} */
189
+ this._cachedFrontier = null;
181
190
  }
182
191
 
183
192
  /**
@@ -570,11 +579,16 @@ export default class WarpGraph {
570
579
  * When false or omitted (default), returns just the state for backward
571
580
  * compatibility with zero receipt overhead.
572
581
  *
582
+ * When a Lamport ceiling is active (via `options.ceiling` or the
583
+ * instance-level `_seekCeiling`), delegates to a ceiling-aware path
584
+ * that replays only patches with `lamport <= ceiling`, bypassing
585
+ * checkpoints, auto-checkpoint, and GC.
586
+ *
573
587
  * Side effects: Updates internal cached state, version vector, last frontier,
574
588
  * and patches-since-checkpoint counter. May trigger auto-checkpoint and GC
575
589
  * based on configured policies. Notifies subscribers if state changed.
576
590
  *
577
- * @param {{receipts?: boolean}} [options] - Optional configuration
591
+ * @param {{receipts?: boolean, ceiling?: number|null}} [options] - Optional configuration
578
592
  * @returns {Promise<import('./services/JoinReducer.js').WarpStateV5|{state: import('./services/JoinReducer.js').WarpStateV5, receipts: import('./types/TickReceipt.js').TickReceipt[]}>} The materialized graph state, or { state, receipts } when receipts enabled
579
593
  * @throws {Error} If checkpoint loading fails or patch decoding fails
580
594
  * @throws {Error} If writer ref access or patch blob reading fails
@@ -583,6 +597,13 @@ export default class WarpGraph {
583
597
  const t0 = this._clock.now();
584
598
  // ZERO-COST: only resolve receipts flag when options provided
585
599
  const collectReceipts = options && options.receipts;
600
+ // Resolve ceiling: explicit option > instance-level seek ceiling > null (latest)
601
+ const ceiling = this._resolveCeiling(options);
602
+
603
+ // When ceiling is active, delegate to ceiling-aware path (with its own cache)
604
+ if (ceiling !== null) {
605
+ return await this._materializeWithCeiling(ceiling, collectReceipts, t0);
606
+ }
586
607
 
587
608
  try {
588
609
  // Check for checkpoint
@@ -658,6 +679,8 @@ export default class WarpGraph {
658
679
  }
659
680
 
660
681
  await this._setMaterializedState(state);
682
+ this._cachedCeiling = null;
683
+ this._cachedFrontier = null;
661
684
  this._lastFrontier = await this.getFrontier();
662
685
  this._patchesSinceCheckpoint = patchCount;
663
686
 
@@ -698,6 +721,124 @@ export default class WarpGraph {
698
721
  }
699
722
  }
700
723
 
724
+ /**
725
+ * Resolves the effective ceiling from options and instance state.
726
+ *
727
+ * Precedence: explicit `ceiling` in options overrides the instance-level
728
+ * `_seekCeiling`. Uses the `'ceiling' in options` check, so passing
729
+ * `{ ceiling: null }` explicitly clears the seek ceiling for that call
730
+ * (returns `null`), while omitting the key falls through to `_seekCeiling`.
731
+ *
732
+ * @param {{ceiling?: number|null}} [options] - Options object; when the
733
+ * `ceiling` key is present (even if `null`), its value takes precedence
734
+ * @returns {number|null} Lamport ceiling to apply, or `null` for latest
735
+ * @private
736
+ */
737
+ _resolveCeiling(options) {
738
+ if (options && options.ceiling !== undefined) {
739
+ return options.ceiling;
740
+ }
741
+ return this._seekCeiling;
742
+ }
743
+
744
+ /**
745
+ * Materializes the graph with a Lamport ceiling (time-travel).
746
+ *
747
+ * Bypasses checkpoints entirely — replays all patches from all writers,
748
+ * filtering to only those with `lamport <= ceiling`. Skips auto-checkpoint
749
+ * and GC since this is an exploratory read.
750
+ *
751
+ * Uses a dedicated cache keyed on `ceiling` + frontier snapshot. Cache
752
+ * is bypassed when the writer frontier has advanced (new writers or
753
+ * updated tips) or when `collectReceipts` is `true` because the cached
754
+ * path does not retain receipt data.
755
+ *
756
+ * @param {number} ceiling - Maximum Lamport tick to include (patches with
757
+ * `lamport <= ceiling` are replayed; `ceiling <= 0` yields empty state)
758
+ * @param {boolean} collectReceipts - When `true`, return receipts alongside
759
+ * state and skip the ceiling cache
760
+ * @param {number} t0 - Start timestamp for performance logging
761
+ * @returns {Promise<import('./services/JoinReducer.js').WarpStateV5 |
762
+ * {state: import('./services/JoinReducer.js').WarpStateV5,
763
+ * receipts: import('./types/TickReceipt.js').TickReceipt[]}>}
764
+ * Plain state when `collectReceipts` is falsy; `{ state, receipts }`
765
+ * when truthy
766
+ * @private
767
+ */
768
+ async _materializeWithCeiling(ceiling, collectReceipts, t0) {
769
+ const frontier = await this.getFrontier();
770
+
771
+ // Cache hit: same ceiling, clean state, AND frontier unchanged.
772
+ // Bypass cache when collectReceipts is true — cached path has no receipts.
773
+ if (
774
+ this._cachedState && !this._stateDirty &&
775
+ ceiling === this._cachedCeiling && !collectReceipts &&
776
+ this._cachedFrontier !== null &&
777
+ this._cachedFrontier.size === frontier.size &&
778
+ [...frontier].every(([w, sha]) => this._cachedFrontier.get(w) === sha)
779
+ ) {
780
+ return this._cachedState;
781
+ }
782
+
783
+ const writerIds = [...frontier.keys()];
784
+
785
+ if (writerIds.length === 0 || ceiling <= 0) {
786
+ const state = createEmptyStateV5();
787
+ this._provenanceIndex = new ProvenanceIndex();
788
+ await this._setMaterializedState(state);
789
+ this._cachedCeiling = ceiling;
790
+ this._cachedFrontier = frontier;
791
+ this._logTiming('materialize', t0, { metrics: '0 patches (ceiling)' });
792
+ if (collectReceipts) {
793
+ return { state, receipts: [] };
794
+ }
795
+ return state;
796
+ }
797
+
798
+ const allPatches = [];
799
+ for (const writerId of writerIds) {
800
+ const writerPatches = await this._loadWriterPatches(writerId);
801
+ for (const entry of writerPatches) {
802
+ if (entry.patch.lamport <= ceiling) {
803
+ allPatches.push(entry);
804
+ }
805
+ }
806
+ }
807
+
808
+ let state;
809
+ let receipts;
810
+
811
+ if (allPatches.length === 0) {
812
+ state = createEmptyStateV5();
813
+ if (collectReceipts) {
814
+ receipts = [];
815
+ }
816
+ } else if (collectReceipts) {
817
+ const result = reduceV5(allPatches, undefined, { receipts: true });
818
+ state = result.state;
819
+ receipts = result.receipts;
820
+ } else {
821
+ state = reduceV5(allPatches);
822
+ }
823
+
824
+ this._provenanceIndex = new ProvenanceIndex();
825
+ for (const { patch, sha } of allPatches) {
826
+ this._provenanceIndex.addPatch(sha, patch.reads, patch.writes);
827
+ }
828
+
829
+ await this._setMaterializedState(state);
830
+ this._cachedCeiling = ceiling;
831
+ this._cachedFrontier = frontier;
832
+
833
+ // Skip auto-checkpoint and GC — this is an exploratory read
834
+ this._logTiming('materialize', t0, { metrics: `${allPatches.length} patches (ceiling=${ceiling})` });
835
+
836
+ if (collectReceipts) {
837
+ return { state, receipts };
838
+ }
839
+ return state;
840
+ }
841
+
701
842
  /**
702
843
  * Joins (merges) another state into the current cached state.
703
844
  *
@@ -1010,6 +1151,79 @@ export default class WarpGraph {
1010
1151
  return writerIds.sort();
1011
1152
  }
1012
1153
 
1154
+ /**
1155
+ * Discovers all distinct Lamport ticks across all writers.
1156
+ *
1157
+ * Walks each writer's patch chain from tip to root, reading commit
1158
+ * messages (no CBOR blob deserialization) to extract Lamport timestamps.
1159
+ * Stops when a non-patch commit (e.g. checkpoint) is encountered.
1160
+ * Logs a warning for any non-monotonic lamport sequence within a single
1161
+ * writer's chain.
1162
+ *
1163
+ * @returns {Promise<{
1164
+ * ticks: number[],
1165
+ * maxTick: number,
1166
+ * perWriter: Map<string, {ticks: number[], tipSha: string|null}>
1167
+ * }>} `ticks` is the sorted (ascending) deduplicated union of all
1168
+ * Lamport values; `maxTick` is the largest value (0 if none);
1169
+ * `perWriter` maps each writer ID to its ticks in ascending order
1170
+ * and its current tip SHA (or `null` if the writer ref is missing)
1171
+ * @throws {Error} If reading refs or commit metadata fails
1172
+ */
1173
+ async discoverTicks() {
1174
+ const writerIds = await this.discoverWriters();
1175
+ const globalTickSet = new Set();
1176
+ const perWriter = new Map();
1177
+
1178
+ for (const writerId of writerIds) {
1179
+ const writerRef = buildWriterRef(this._graphName, writerId);
1180
+ const tipSha = await this._persistence.readRef(writerRef);
1181
+ const writerTicks = [];
1182
+ const tickShas = {};
1183
+
1184
+ if (tipSha) {
1185
+ let currentSha = tipSha;
1186
+ let lastLamport = Infinity;
1187
+
1188
+ while (currentSha) {
1189
+ const nodeInfo = await this._persistence.getNodeInfo(currentSha);
1190
+ const kind = detectMessageKind(nodeInfo.message);
1191
+ if (kind !== 'patch') {
1192
+ break;
1193
+ }
1194
+
1195
+ const patchMeta = decodePatchMessage(nodeInfo.message);
1196
+ globalTickSet.add(patchMeta.lamport);
1197
+ writerTicks.push(patchMeta.lamport);
1198
+ tickShas[patchMeta.lamport] = currentSha;
1199
+
1200
+ // Check monotonic invariant (walking newest→oldest, lamport should decrease)
1201
+ if (patchMeta.lamport > lastLamport && this._logger) {
1202
+ this._logger.warn(`[warp] non-monotonic lamport for writer ${writerId}: ${patchMeta.lamport} > ${lastLamport}`);
1203
+ }
1204
+ lastLamport = patchMeta.lamport;
1205
+
1206
+ if (nodeInfo.parents && nodeInfo.parents.length > 0) {
1207
+ currentSha = nodeInfo.parents[0];
1208
+ } else {
1209
+ break;
1210
+ }
1211
+ }
1212
+ }
1213
+
1214
+ perWriter.set(writerId, {
1215
+ ticks: writerTicks.reverse(),
1216
+ tipSha: tipSha || null,
1217
+ tickShas,
1218
+ });
1219
+ }
1220
+
1221
+ const ticks = [...globalTickSet].sort((a, b) => a - b);
1222
+ const maxTick = ticks.length > 0 ? ticks[ticks.length - 1] : 0;
1223
+
1224
+ return { ticks, maxTick, perWriter };
1225
+ }
1226
+
1013
1227
  // ============================================================================
1014
1228
  // Schema Migration Support
1015
1229
  // ============================================================================
@@ -3025,6 +3239,23 @@ export default class WarpGraph {
3025
3239
  return cone;
3026
3240
  }
3027
3241
 
3242
+ /**
3243
+ * Loads a single patch by its SHA.
3244
+ *
3245
+ * @param {string} sha - The patch commit SHA
3246
+ * @returns {Promise<Object>} The decoded patch object
3247
+ * @throws {Error} If the commit is not a patch or loading fails
3248
+ *
3249
+ * @public
3250
+ * @remarks
3251
+ * Thin wrapper around the internal `_loadPatchBySha` helper. Exposed for
3252
+ * CLI/debug tooling (e.g. seek tick receipts) that needs to inspect patch
3253
+ * operations without re-materializing intermediate states.
3254
+ */
3255
+ async loadPatchBySha(sha) {
3256
+ return await this._loadPatchBySha(sha);
3257
+ }
3258
+
3028
3259
  /**
3029
3260
  * Loads a single patch by its SHA.
3030
3261
  *
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Immutable value object representing a single graph node backed by a
3
+ * Git commit pointing to the empty tree.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ /**
9
+ * Immutable entity representing a graph node.
10
+ */
11
+ export default class GraphNode {
12
+ /** Commit SHA */
13
+ readonly sha: string;
14
+ /** Author name */
15
+ readonly author: string | undefined;
16
+ /** Commit date */
17
+ readonly date: string | undefined;
18
+ /** Node message/data */
19
+ readonly message: string;
20
+ /** Array of parent SHAs */
21
+ readonly parents: readonly string[];
22
+
23
+ constructor(data: {
24
+ sha: string;
25
+ message: string;
26
+ author?: string;
27
+ date?: string;
28
+ parents?: string[];
29
+ });
30
+ }
@@ -1,3 +1,12 @@
1
+ /* @ts-self-types="./GraphNode.d.ts" */
2
+
3
+ /**
4
+ * @module
5
+ *
6
+ * Immutable value object representing a single graph node backed by a
7
+ * Git commit pointing to the empty tree.
8
+ */
9
+
1
10
  /**
2
11
  * Immutable domain entity representing a node in the graph.
3
12
  *
@@ -8,6 +8,8 @@
8
8
  * - refs/warp/<graph>/writers/<writer_id>
9
9
  * - refs/warp/<graph>/checkpoints/head
10
10
  * - refs/warp/<graph>/coverage/head
11
+ * - refs/warp/<graph>/cursor/active
12
+ * - refs/warp/<graph>/cursor/saved/<name>
11
13
  *
12
14
  * @module domain/utils/RefLayout
13
15
  */
@@ -49,20 +51,22 @@ const PATH_TRAVERSAL_PATTERN = /\.\./;
49
51
  * Validates a graph name and throws if invalid.
50
52
  *
51
53
  * Graph names must not contain:
52
- * - Path traversal sequences (`../`)
54
+ * - Path traversal sequences (`..`)
53
55
  * - Semicolons (`;`)
54
56
  * - Spaces
55
57
  * - Null bytes (`\0`)
56
58
  * - Empty strings
57
59
  *
58
60
  * @param {string} name - The graph name to validate
59
- * @throws {Error} If the graph name is invalid
61
+ * @throws {Error} If the name is not a string, is empty, or contains
62
+ * forbidden characters (`..`, `;`, space, `\0`)
60
63
  * @returns {void}
61
64
  *
62
65
  * @example
63
- * validateGraphName('events'); // OK
64
- * validateGraphName('../etc'); // throws
65
- * validateGraphName('my graph'); // throws
66
+ * validateGraphName('events'); // OK
67
+ * validateGraphName('team/proj'); // OK (slashes allowed)
68
+ * validateGraphName('../etc'); // throws — path traversal
69
+ * validateGraphName('my graph'); // throws — contains space
66
70
  */
67
71
  export function validateGraphName(name) {
68
72
  if (typeof name !== 'string') {
@@ -94,18 +98,20 @@ export function validateGraphName(name) {
94
98
  * Validates a writer ID and throws if invalid.
95
99
  *
96
100
  * Writer IDs must:
97
- * - Be ASCII ref-safe: only [A-Za-z0-9._-]
101
+ * - Be ASCII ref-safe: only `[A-Za-z0-9._-]`
98
102
  * - Be 1-64 characters long
99
103
  * - Not contain `/`, `..`, whitespace, or NUL
100
104
  *
101
105
  * @param {string} id - The writer ID to validate
102
- * @throws {Error} If the writer ID is invalid
106
+ * @throws {Error} If the ID is not a string, is empty, exceeds 64 characters,
107
+ * or contains forbidden characters (`/`, `..`, whitespace, NUL, non-ASCII)
103
108
  * @returns {void}
104
109
  *
105
110
  * @example
106
- * validateWriterId('node-1'); // OK
107
- * validateWriterId('a/b'); // throws (contains /)
108
- * validateWriterId('x'.repeat(65)); // throws (too long)
111
+ * validateWriterId('node-1'); // OK
112
+ * validateWriterId('a/b'); // throws contains forward slash
113
+ * validateWriterId('x'.repeat(65)); // throws exceeds max length
114
+ * validateWriterId('has space'); // throws — contains whitespace
109
115
  */
110
116
  export function validateWriterId(id) {
111
117
  if (typeof id !== 'string') {
@@ -157,7 +163,7 @@ export function validateWriterId(id) {
157
163
  *
158
164
  * @param {string} graphName - The name of the graph
159
165
  * @param {string} writerId - The writer's unique identifier
160
- * @returns {string} The full ref path
166
+ * @returns {string} The full ref path, e.g. `refs/warp/<graphName>/writers/<writerId>`
161
167
  * @throws {Error} If graphName or writerId is invalid
162
168
  *
163
169
  * @example
@@ -174,7 +180,7 @@ export function buildWriterRef(graphName, writerId) {
174
180
  * Builds the checkpoint head ref path for the given graph.
175
181
  *
176
182
  * @param {string} graphName - The name of the graph
177
- * @returns {string} The full ref path
183
+ * @returns {string} The full ref path, e.g. `refs/warp/<graphName>/checkpoints/head`
178
184
  * @throws {Error} If graphName is invalid
179
185
  *
180
186
  * @example
@@ -190,7 +196,7 @@ export function buildCheckpointRef(graphName) {
190
196
  * Builds the coverage head ref path for the given graph.
191
197
  *
192
198
  * @param {string} graphName - The name of the graph
193
- * @returns {string} The full ref path
199
+ * @returns {string} The full ref path, e.g. `refs/warp/<graphName>/coverage/head`
194
200
  * @throws {Error} If graphName is invalid
195
201
  *
196
202
  * @example
@@ -204,10 +210,12 @@ export function buildCoverageRef(graphName) {
204
210
 
205
211
  /**
206
212
  * Builds the writers prefix path for the given graph.
207
- * Useful for listing all writer refs under a graph.
213
+ * Useful for listing all writer refs under a graph
214
+ * (e.g. via `git for-each-ref`).
208
215
  *
209
216
  * @param {string} graphName - The name of the graph
210
- * @returns {string} The writers prefix path
217
+ * @returns {string} The writers prefix path (with trailing slash),
218
+ * e.g. `refs/warp/<graphName>/writers/`
211
219
  * @throws {Error} If graphName is invalid
212
220
  *
213
221
  * @example
@@ -219,6 +227,70 @@ export function buildWritersPrefix(graphName) {
219
227
  return `${REF_PREFIX}/${graphName}/writers/`;
220
228
  }
221
229
 
230
+ /**
231
+ * Builds the active cursor ref path for the given graph.
232
+ *
233
+ * The active cursor is a single ref that stores the current time-travel
234
+ * position used by `git warp seek`. It points to a commit SHA representing
235
+ * the materialization frontier the user has seeked to.
236
+ *
237
+ * @param {string} graphName - The name of the graph
238
+ * @returns {string} The full ref path, e.g. `refs/warp/<graphName>/cursor/active`
239
+ * @throws {Error} If graphName is invalid
240
+ *
241
+ * @example
242
+ * buildCursorActiveRef('events');
243
+ * // => 'refs/warp/events/cursor/active'
244
+ */
245
+ export function buildCursorActiveRef(graphName) {
246
+ validateGraphName(graphName);
247
+ return `${REF_PREFIX}/${graphName}/cursor/active`;
248
+ }
249
+
250
+ /**
251
+ * Builds a saved (named) cursor ref path for the given graph and cursor name.
252
+ *
253
+ * Saved cursors are bookmarks created by `git warp seek --save <name>`.
254
+ * Each saved cursor persists a time-travel position that can be restored
255
+ * later without re-seeking.
256
+ *
257
+ * The cursor name is validated with the same rules as a writer ID
258
+ * (ASCII ref-safe: `[A-Za-z0-9._-]`, 1-64 characters).
259
+ *
260
+ * @param {string} graphName - The name of the graph
261
+ * @param {string} name - The cursor bookmark name (validated like a writer ID)
262
+ * @returns {string} The full ref path, e.g. `refs/warp/<graphName>/cursor/saved/<name>`
263
+ * @throws {Error} If graphName or name is invalid
264
+ *
265
+ * @example
266
+ * buildCursorSavedRef('events', 'before-tui');
267
+ * // => 'refs/warp/events/cursor/saved/before-tui'
268
+ */
269
+ export function buildCursorSavedRef(graphName, name) {
270
+ validateGraphName(graphName);
271
+ validateWriterId(name);
272
+ return `${REF_PREFIX}/${graphName}/cursor/saved/${name}`;
273
+ }
274
+
275
+ /**
276
+ * Builds the saved cursor prefix path for the given graph.
277
+ * Useful for listing all saved cursor bookmarks under a graph
278
+ * (e.g. via `git for-each-ref`).
279
+ *
280
+ * @param {string} graphName - The name of the graph
281
+ * @returns {string} The saved cursor prefix path (with trailing slash),
282
+ * e.g. `refs/warp/<graphName>/cursor/saved/`
283
+ * @throws {Error} If graphName is invalid
284
+ *
285
+ * @example
286
+ * buildCursorSavedPrefix('events');
287
+ * // => 'refs/warp/events/cursor/saved/'
288
+ */
289
+ export function buildCursorSavedPrefix(graphName) {
290
+ validateGraphName(graphName);
291
+ return `${REF_PREFIX}/${graphName}/cursor/saved/`;
292
+ }
293
+
222
294
  // -----------------------------------------------------------------------------
223
295
  // Parsers
224
296
  // -----------------------------------------------------------------------------
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Utilities for parsing seek-cursor blobs stored as Git refs.
3
+ *
4
+ * @module parseCursorBlob
5
+ */
6
+
7
+ /**
8
+ * Parses and validates a cursor blob (Buffer) into a cursor object.
9
+ *
10
+ * The blob must contain UTF-8-encoded JSON representing a plain object with at
11
+ * minimum a finite numeric `tick` field. Any additional fields (e.g. `mode`,
12
+ * `name`) are preserved in the returned object.
13
+ *
14
+ * @param {Buffer} buf - Raw blob contents (UTF-8 encoded JSON)
15
+ * @param {string} label - Human-readable label used in error messages
16
+ * (e.g. `"active cursor"`, `"saved cursor 'foo'"`)
17
+ * @returns {{ tick: number, mode?: string, [key: string]: unknown }}
18
+ * The validated cursor object. `tick` is guaranteed to be a finite number.
19
+ * @throws {Error} If `buf` is not valid JSON
20
+ * @throws {Error} If the parsed value is not a plain JSON object (e.g. array,
21
+ * null, or primitive)
22
+ * @throws {Error} If the `tick` field is missing, non-numeric, NaN, or
23
+ * Infinity
24
+ *
25
+ * @example
26
+ * const buf = Buffer.from('{"tick":5,"mode":"lamport"}', 'utf8');
27
+ * const cursor = parseCursorBlob(buf, 'active cursor');
28
+ * // => { tick: 5, mode: 'lamport' }
29
+ *
30
+ * @example
31
+ * // Throws: "Corrupted active cursor: blob is not valid JSON"
32
+ * parseCursorBlob(Buffer.from('not json', 'utf8'), 'active cursor');
33
+ */
34
+ export function parseCursorBlob(buf, label) {
35
+ let obj;
36
+ try {
37
+ obj = JSON.parse(new TextDecoder().decode(buf));
38
+ } catch {
39
+ throw new Error(`Corrupted ${label}: blob is not valid JSON`);
40
+ }
41
+
42
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
43
+ throw new Error(`Corrupted ${label}: expected a JSON object`);
44
+ }
45
+
46
+ if (typeof obj.tick !== 'number' || !Number.isFinite(obj.tick)) {
47
+ throw new Error(`Corrupted ${label}: missing or invalid numeric tick`);
48
+ }
49
+
50
+ return obj;
51
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Visualization module for rendering WARP graph data as ASCII tables,
3
+ * SVG diagrams, and interactive browser views.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ // ASCII renderers
9
+ export declare const colors: Record<string, (s: string) => string>;
10
+ export declare const colorsDefault: Record<string, (s: string) => string>;
11
+ export declare function createBox(content: string, options?: Record<string, unknown>): string;
12
+ export declare function createTable(rows: unknown[], options?: Record<string, unknown>): string;
13
+ export declare function progressBar(current: number, total: number, options?: Record<string, unknown>): string;
14
+ export declare function renderInfoView(data: Record<string, unknown>): string;
15
+ export declare function renderCheckView(data: Record<string, unknown>): string;
16
+ export declare function renderMaterializeView(data: Record<string, unknown>): string;
17
+ export declare function renderHistoryView(data: Record<string, unknown>): string;
18
+ export declare function summarizeOps(ops: unknown[]): string;
19
+ export declare function renderPathView(data: Record<string, unknown>): string;
20
+ export declare function renderGraphView(data: Record<string, unknown>): string;
21
+
22
+ // SVG renderer
23
+ export declare function renderSvg(positionedGraph: Record<string, unknown>, options?: Record<string, unknown>): string;
24
+
25
+ // Layout engine
26
+ export declare function layoutGraph(graphData: { nodes: unknown[]; edges: unknown[] }, options?: Record<string, unknown>): Promise<Record<string, unknown>>;
27
+ export declare function queryResultToGraphData(result: Record<string, unknown>): { nodes: unknown[]; edges: unknown[] };
28
+ export declare function pathResultToGraphData(result: Record<string, unknown>): { nodes: unknown[]; edges: unknown[] };
29
+ export declare function rawGraphToGraphData(result: Record<string, unknown>): { nodes: unknown[]; edges: unknown[] };
30
+ export declare function toElkGraph(graphData: { nodes: unknown[]; edges: unknown[] }, options?: Record<string, unknown>): Record<string, unknown>;
31
+ export declare function getDefaultLayoutOptions(): Record<string, string>;
32
+ export declare function runLayout(elkGraph: Record<string, unknown>): Promise<Record<string, unknown>>;
33
+
34
+ // Utils
35
+ export declare function truncate(str: string, maxWidth: number): string;
36
+ export declare function timeAgo(dateStr: string): string;
37
+ export declare function formatDuration(ms: number): string;
38
+ export declare function padRight(str: string, width: number): string;
39
+ export declare function padLeft(str: string, width: number): string;
40
+ export declare function center(str: string, width: number): string;
41
+ export declare function stripAnsi(str: string): string;
@@ -1,5 +1,11 @@
1
+ /* @ts-self-types="./index.d.ts" */
2
+
1
3
  /**
2
- * Visualization module - main exports
4
+ * @module
5
+ *
6
+ * Visualization module for rendering WARP graph data as ASCII tables,
7
+ * SVG diagrams, and interactive browser views. Includes ELK-based
8
+ * graph layout, ANSI formatting utilities, and CLI renderers.
3
9
  */
4
10
 
5
11
  // ASCII renderers