@git-stunts/git-warp 10.1.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 (143) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +16 -0
  3. package/README.md +480 -0
  4. package/SECURITY.md +30 -0
  5. package/bin/git-warp +24 -0
  6. package/bin/warp-graph.js +1574 -0
  7. package/index.d.ts +2366 -0
  8. package/index.js +180 -0
  9. package/package.json +129 -0
  10. package/scripts/install-git-warp.sh +258 -0
  11. package/scripts/uninstall-git-warp.sh +139 -0
  12. package/src/domain/WarpGraph.js +3157 -0
  13. package/src/domain/crdt/Dot.js +160 -0
  14. package/src/domain/crdt/LWW.js +154 -0
  15. package/src/domain/crdt/ORSet.js +371 -0
  16. package/src/domain/crdt/VersionVector.js +222 -0
  17. package/src/domain/entities/GraphNode.js +60 -0
  18. package/src/domain/errors/EmptyMessageError.js +47 -0
  19. package/src/domain/errors/ForkError.js +30 -0
  20. package/src/domain/errors/IndexError.js +23 -0
  21. package/src/domain/errors/OperationAbortedError.js +22 -0
  22. package/src/domain/errors/QueryError.js +39 -0
  23. package/src/domain/errors/SchemaUnsupportedError.js +17 -0
  24. package/src/domain/errors/ShardCorruptionError.js +56 -0
  25. package/src/domain/errors/ShardLoadError.js +57 -0
  26. package/src/domain/errors/ShardValidationError.js +61 -0
  27. package/src/domain/errors/StorageError.js +57 -0
  28. package/src/domain/errors/SyncError.js +30 -0
  29. package/src/domain/errors/TraversalError.js +23 -0
  30. package/src/domain/errors/WarpError.js +31 -0
  31. package/src/domain/errors/WormholeError.js +28 -0
  32. package/src/domain/errors/WriterError.js +39 -0
  33. package/src/domain/errors/index.js +21 -0
  34. package/src/domain/services/AnchorMessageCodec.js +99 -0
  35. package/src/domain/services/BitmapIndexBuilder.js +225 -0
  36. package/src/domain/services/BitmapIndexReader.js +435 -0
  37. package/src/domain/services/BoundaryTransitionRecord.js +463 -0
  38. package/src/domain/services/CheckpointMessageCodec.js +147 -0
  39. package/src/domain/services/CheckpointSerializerV5.js +281 -0
  40. package/src/domain/services/CheckpointService.js +384 -0
  41. package/src/domain/services/CommitDagTraversalService.js +156 -0
  42. package/src/domain/services/DagPathFinding.js +712 -0
  43. package/src/domain/services/DagTopology.js +239 -0
  44. package/src/domain/services/DagTraversal.js +245 -0
  45. package/src/domain/services/Frontier.js +108 -0
  46. package/src/domain/services/GCMetrics.js +101 -0
  47. package/src/domain/services/GCPolicy.js +122 -0
  48. package/src/domain/services/GitLogParser.js +205 -0
  49. package/src/domain/services/HealthCheckService.js +246 -0
  50. package/src/domain/services/HookInstaller.js +326 -0
  51. package/src/domain/services/HttpSyncServer.js +262 -0
  52. package/src/domain/services/IndexRebuildService.js +426 -0
  53. package/src/domain/services/IndexStalenessChecker.js +103 -0
  54. package/src/domain/services/JoinReducer.js +582 -0
  55. package/src/domain/services/KeyCodec.js +113 -0
  56. package/src/domain/services/LegacyAnchorDetector.js +67 -0
  57. package/src/domain/services/LogicalTraversal.js +351 -0
  58. package/src/domain/services/MessageCodecInternal.js +132 -0
  59. package/src/domain/services/MessageSchemaDetector.js +145 -0
  60. package/src/domain/services/MigrationService.js +55 -0
  61. package/src/domain/services/ObserverView.js +265 -0
  62. package/src/domain/services/PatchBuilderV2.js +669 -0
  63. package/src/domain/services/PatchMessageCodec.js +140 -0
  64. package/src/domain/services/ProvenanceIndex.js +337 -0
  65. package/src/domain/services/ProvenancePayload.js +242 -0
  66. package/src/domain/services/QueryBuilder.js +835 -0
  67. package/src/domain/services/StateDiff.js +300 -0
  68. package/src/domain/services/StateSerializerV5.js +156 -0
  69. package/src/domain/services/StreamingBitmapIndexBuilder.js +709 -0
  70. package/src/domain/services/SyncProtocol.js +593 -0
  71. package/src/domain/services/TemporalQuery.js +201 -0
  72. package/src/domain/services/TranslationCost.js +221 -0
  73. package/src/domain/services/TraversalService.js +8 -0
  74. package/src/domain/services/WarpMessageCodec.js +29 -0
  75. package/src/domain/services/WarpStateIndexBuilder.js +127 -0
  76. package/src/domain/services/WormholeService.js +353 -0
  77. package/src/domain/types/TickReceipt.js +285 -0
  78. package/src/domain/types/WarpTypes.js +209 -0
  79. package/src/domain/types/WarpTypesV2.js +200 -0
  80. package/src/domain/utils/CachedValue.js +140 -0
  81. package/src/domain/utils/EventId.js +89 -0
  82. package/src/domain/utils/LRUCache.js +112 -0
  83. package/src/domain/utils/MinHeap.js +114 -0
  84. package/src/domain/utils/RefLayout.js +280 -0
  85. package/src/domain/utils/WriterId.js +205 -0
  86. package/src/domain/utils/cancellation.js +33 -0
  87. package/src/domain/utils/canonicalStringify.js +42 -0
  88. package/src/domain/utils/defaultClock.js +20 -0
  89. package/src/domain/utils/defaultCodec.js +51 -0
  90. package/src/domain/utils/nullLogger.js +21 -0
  91. package/src/domain/utils/roaring.js +181 -0
  92. package/src/domain/utils/shardVersion.js +9 -0
  93. package/src/domain/warp/PatchSession.js +217 -0
  94. package/src/domain/warp/Writer.js +181 -0
  95. package/src/hooks/post-merge.sh +60 -0
  96. package/src/infrastructure/adapters/BunHttpAdapter.js +225 -0
  97. package/src/infrastructure/adapters/ClockAdapter.js +57 -0
  98. package/src/infrastructure/adapters/ConsoleLogger.js +150 -0
  99. package/src/infrastructure/adapters/DenoHttpAdapter.js +230 -0
  100. package/src/infrastructure/adapters/GitGraphAdapter.js +787 -0
  101. package/src/infrastructure/adapters/GlobalClockAdapter.js +5 -0
  102. package/src/infrastructure/adapters/NoOpLogger.js +62 -0
  103. package/src/infrastructure/adapters/NodeCryptoAdapter.js +32 -0
  104. package/src/infrastructure/adapters/NodeHttpAdapter.js +98 -0
  105. package/src/infrastructure/adapters/PerformanceClockAdapter.js +5 -0
  106. package/src/infrastructure/adapters/WebCryptoAdapter.js +121 -0
  107. package/src/infrastructure/codecs/CborCodec.js +384 -0
  108. package/src/ports/BlobPort.js +30 -0
  109. package/src/ports/ClockPort.js +25 -0
  110. package/src/ports/CodecPort.js +25 -0
  111. package/src/ports/CommitPort.js +114 -0
  112. package/src/ports/ConfigPort.js +31 -0
  113. package/src/ports/CryptoPort.js +38 -0
  114. package/src/ports/GraphPersistencePort.js +57 -0
  115. package/src/ports/HttpServerPort.js +25 -0
  116. package/src/ports/IndexStoragePort.js +39 -0
  117. package/src/ports/LoggerPort.js +68 -0
  118. package/src/ports/RefPort.js +51 -0
  119. package/src/ports/TreePort.js +51 -0
  120. package/src/visualization/index.js +26 -0
  121. package/src/visualization/layouts/converters.js +75 -0
  122. package/src/visualization/layouts/elkAdapter.js +86 -0
  123. package/src/visualization/layouts/elkLayout.js +95 -0
  124. package/src/visualization/layouts/index.js +29 -0
  125. package/src/visualization/renderers/ascii/box.js +16 -0
  126. package/src/visualization/renderers/ascii/check.js +271 -0
  127. package/src/visualization/renderers/ascii/colors.js +13 -0
  128. package/src/visualization/renderers/ascii/formatters.js +73 -0
  129. package/src/visualization/renderers/ascii/graph.js +344 -0
  130. package/src/visualization/renderers/ascii/history.js +335 -0
  131. package/src/visualization/renderers/ascii/index.js +14 -0
  132. package/src/visualization/renderers/ascii/info.js +245 -0
  133. package/src/visualization/renderers/ascii/materialize.js +255 -0
  134. package/src/visualization/renderers/ascii/path.js +240 -0
  135. package/src/visualization/renderers/ascii/progress.js +32 -0
  136. package/src/visualization/renderers/ascii/symbols.js +33 -0
  137. package/src/visualization/renderers/ascii/table.js +19 -0
  138. package/src/visualization/renderers/browser/index.js +1 -0
  139. package/src/visualization/renderers/svg/index.js +159 -0
  140. package/src/visualization/utils/ansi.js +14 -0
  141. package/src/visualization/utils/time.js +40 -0
  142. package/src/visualization/utils/truncate.js +40 -0
  143. package/src/visualization/utils/unicode.js +52 -0
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Cancellation utilities for async operations.
3
+ *
4
+ * @module domain/utils/cancellation
5
+ */
6
+
7
+ import OperationAbortedError from '../errors/OperationAbortedError.js';
8
+
9
+ /**
10
+ * Checks if an abort signal has been aborted and throws if so.
11
+ *
12
+ * @param {AbortSignal} [signal] - The abort signal to check
13
+ * @param {string} [operation] - Name of the operation being checked
14
+ * @throws {OperationAbortedError} If signal is aborted
15
+ */
16
+ export function checkAborted(signal, operation) {
17
+ if (signal?.aborted) {
18
+ throw new OperationAbortedError(operation || 'unknown', { context: { operation } });
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Creates an AbortSignal that will abort after the specified timeout.
24
+ *
25
+ * Note: This signal cannot be manually cancelled. If callers need early
26
+ * cancellation, they should use AbortController directly.
27
+ *
28
+ * @param {number} ms - Timeout in milliseconds
29
+ * @returns {AbortSignal} The abort signal
30
+ */
31
+ export function createTimeoutSignal(ms) {
32
+ return AbortSignal.timeout(ms);
33
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Recursively stringifies a value with sorted object keys for deterministic output.
3
+ * Used for computing checksums that must match across builders and readers.
4
+ *
5
+ * Matches JSON.stringify semantics:
6
+ * - Top-level undefined returns "null"
7
+ * - Array elements that are undefined/function/symbol become "null"
8
+ * - Object properties with undefined/function/symbol values are omitted
9
+ *
10
+ * @param {*} value - Any JSON-serializable value
11
+ * @returns {string} Canonical JSON string with sorted keys
12
+ */
13
+ export function canonicalStringify(value) {
14
+ if (value === undefined) {
15
+ return 'null';
16
+ }
17
+ if (value === null) {
18
+ return 'null';
19
+ }
20
+ if (Array.isArray(value)) {
21
+ // Map elements: undefined/function/symbol -> "null", others recurse
22
+ const elements = value.map(el => {
23
+ if (el === undefined || typeof el === 'function' || typeof el === 'symbol') {
24
+ return 'null';
25
+ }
26
+ return canonicalStringify(el);
27
+ });
28
+ return `[${elements.join(',')}]`;
29
+ }
30
+ if (typeof value === 'object') {
31
+ // Filter out keys with undefined/function/symbol values, then sort
32
+ const keys = Object.keys(value)
33
+ .filter(k => {
34
+ const v = value[k];
35
+ return v !== undefined && typeof v !== 'function' && typeof v !== 'symbol';
36
+ })
37
+ .sort();
38
+ const pairs = keys.map(k => `${JSON.stringify(k)}:${canonicalStringify(value[k])}`);
39
+ return `{${pairs.join(',')}}`;
40
+ }
41
+ return JSON.stringify(value);
42
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Default clock implementation for domain services.
3
+ *
4
+ * Uses standard globalThis.performance.now() for high-resolution timing
5
+ * and Date for wall-clock timestamps, avoiding concrete adapter imports.
6
+ *
7
+ * @module domain/utils/defaultClock
8
+ */
9
+
10
+ /** @type {import('../../ports/ClockPort.js').default} */
11
+ const defaultClock = {
12
+ now() {
13
+ return performance.now();
14
+ },
15
+ timestamp() {
16
+ return new Date().toISOString();
17
+ },
18
+ };
19
+
20
+ export default defaultClock;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Default codec implementation for domain services.
3
+ *
4
+ * Provides canonical CBOR encoding/decoding using cbor-x directly,
5
+ * avoiding concrete adapter imports from the infrastructure layer.
6
+ * This follows the same pattern as defaultClock.js.
7
+ *
8
+ * Keys are recursively sorted before encoding for deterministic output,
9
+ * which is critical for content-addressed storage (Git SHA matching).
10
+ *
11
+ * @module domain/utils/defaultCodec
12
+ */
13
+
14
+ import { Encoder, decode as cborDecode } from 'cbor-x';
15
+
16
+ const encoder = new Encoder({
17
+ useRecords: false,
18
+ mapsAsObjects: true,
19
+ });
20
+
21
+ function sortKeys(value) {
22
+ if (value === null || value === undefined) { return value; }
23
+ if (Array.isArray(value)) { return value.map(sortKeys); }
24
+ if (value instanceof Map) {
25
+ const sorted = {};
26
+ for (const key of Array.from(value.keys()).sort()) {
27
+ sorted[key] = sortKeys(value.get(key));
28
+ }
29
+ return sorted;
30
+ }
31
+ if (typeof value === 'object' && (value.constructor === Object || value.constructor === undefined)) {
32
+ const sorted = {};
33
+ for (const key of Object.keys(value).sort()) {
34
+ sorted[key] = sortKeys(value[key]);
35
+ }
36
+ return sorted;
37
+ }
38
+ return value;
39
+ }
40
+
41
+ /** @type {import('../../ports/CodecPort.js').default} */
42
+ const defaultCodec = {
43
+ encode(data) {
44
+ return encoder.encode(sortKeys(data));
45
+ },
46
+ decode(buffer) {
47
+ return cborDecode(buffer);
48
+ },
49
+ };
50
+
51
+ export default defaultCodec;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Null-object logger for use as a default when no logger is provided.
3
+ *
4
+ * All methods are no-ops. This keeps the domain layer free of
5
+ * adapter dependencies by providing an inline null object.
6
+ *
7
+ * @module domain/utils/nullLogger
8
+ */
9
+
10
+ /** @type {import('../../ports/LoggerPort.js').default} */
11
+ const nullLogger = {
12
+ debug() {},
13
+ info() {},
14
+ warn() {},
15
+ error() {},
16
+ child() {
17
+ return nullLogger;
18
+ },
19
+ };
20
+
21
+ export default nullLogger;
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Lazy-loading wrapper for the roaring-wasm/roaring native bitmap library.
3
+ *
4
+ * This module provides deferred loading of the `roaring` npm package to avoid
5
+ * incurring the startup cost of loading native C++ bindings until they are
6
+ * actually needed. The roaring package provides highly efficient compressed
7
+ * bitmap data structures used by the bitmap index system for O(1) neighbor lookups.
8
+ *
9
+ * ## Why Lazy Loading?
10
+ *
11
+ * The `roaring` package includes native C++ bindings that can take 50-100ms to
12
+ * initialize on cold start. By deferring the load until first use,
13
+ * applications that don't use bitmap indexes avoid this overhead entirely.
14
+ *
15
+ * ## Module Caching
16
+ *
17
+ * Once loaded, the module reference is cached in `roaringModule` and reused
18
+ * for all subsequent calls. Similarly, native availability is cached after
19
+ * the first check to avoid repeated introspection.
20
+ *
21
+ * @module roaring
22
+ * @see BitmapIndexBuilder - Primary consumer of roaring bitmaps
23
+ * @see StreamingBitmapIndexBuilder - Memory-bounded variant
24
+ */
25
+
26
+ /**
27
+ * Sentinel indicating availability has not been checked yet.
28
+ * @const {symbol}
29
+ * @private
30
+ */
31
+ const NOT_CHECKED = Symbol('NOT_CHECKED');
32
+
33
+ /**
34
+ * Cached reference to the loaded roaring module.
35
+ * @type {Object|null}
36
+ * @private
37
+ */
38
+ let roaringModule = null;
39
+
40
+ /**
41
+ * Cached result of native availability check.
42
+ * `NOT_CHECKED` means not yet checked, `null` means indeterminate.
43
+ * @type {boolean|symbol|null}
44
+ * @private
45
+ */
46
+ let nativeAvailability = NOT_CHECKED;
47
+
48
+ /**
49
+ * Lazily loads and caches the roaring module.
50
+ *
51
+ * Uses a top-level-await-friendly pattern with dynamic import.
52
+ * The module is cached after first load.
53
+ *
54
+ * @returns {Object} The roaring module exports
55
+ * @throws {Error} If the roaring package is not installed or fails to load
56
+ * @private
57
+ */
58
+ function loadRoaring() {
59
+ if (!roaringModule) {
60
+ throw new Error('Roaring module not loaded. Call initRoaring() first or ensure top-level await import completed.');
61
+ }
62
+ return roaringModule;
63
+ }
64
+
65
+ /**
66
+ * Initializes the roaring module. Must be called before getRoaringBitmap32().
67
+ * This is called automatically via top-level await when the module is imported,
68
+ * but can also be called manually with a pre-loaded module for testing.
69
+ *
70
+ * @param {Object} [mod] - Pre-loaded roaring module (for testing/DI)
71
+ * @returns {Promise<void>}
72
+ */
73
+ export async function initRoaring(mod) {
74
+ if (mod) {
75
+ roaringModule = mod;
76
+ return;
77
+ }
78
+ if (!roaringModule) {
79
+ roaringModule = await import('roaring');
80
+ // Handle both ESM default export and CJS module.exports
81
+ if (roaringModule.default && roaringModule.default.RoaringBitmap32) {
82
+ roaringModule = roaringModule.default;
83
+ }
84
+ }
85
+ }
86
+
87
+ // Auto-initialize on module load (top-level await)
88
+ try {
89
+ await initRoaring();
90
+ } catch {
91
+ // Roaring may not be installed; functions will throw on use
92
+ }
93
+
94
+ /**
95
+ * Returns the RoaringBitmap32 class from the roaring library.
96
+ *
97
+ * RoaringBitmap32 is a compressed bitmap implementation that provides
98
+ * efficient set operations (union, intersection, difference) on large
99
+ * sets of 32-bit integers. It's used by the bitmap index system to
100
+ * store edge adjacency lists in a highly compressed format.
101
+ *
102
+ * @returns {typeof import('roaring').RoaringBitmap32} The RoaringBitmap32 constructor
103
+ * @throws {Error} If the roaring package is not installed
104
+ *
105
+ * @example
106
+ * const RoaringBitmap32 = getRoaringBitmap32();
107
+ * const bitmap = new RoaringBitmap32([1, 2, 3, 100, 1000]);
108
+ * bitmap.has(100); // true
109
+ * bitmap.size; // 5
110
+ *
111
+ * @example
112
+ * // Set operations
113
+ * const a = new RoaringBitmap32([1, 2, 3]);
114
+ * const b = new RoaringBitmap32([2, 3, 4]);
115
+ * const union = RoaringBitmap32.or(a, b); // [1, 2, 3, 4]
116
+ * const intersection = RoaringBitmap32.and(a, b); // [2, 3]
117
+ */
118
+ export function getRoaringBitmap32() {
119
+ return loadRoaring().RoaringBitmap32;
120
+ }
121
+
122
+ /**
123
+ * Checks whether the native C++ roaring implementation is available.
124
+ *
125
+ * The `roaring` package can operate in two modes:
126
+ * - **Native mode**: Uses prebuilt C++ bindings for maximum performance
127
+ * - **WASM fallback**: Uses WebAssembly when native bindings aren't available
128
+ *
129
+ * This function checks which mode is active by introspecting the loaded
130
+ * module. The result is cached after the first call.
131
+ *
132
+ * @returns {boolean|null} `true` if native bindings are installed,
133
+ * `false` if using WASM fallback or if loading failed,
134
+ * `null` if the installation status could not be determined
135
+ *
136
+ * @example
137
+ * if (getNativeRoaringAvailable()) {
138
+ * console.log('Using native roaring bindings (fastest)');
139
+ * } else if (getNativeRoaringAvailable() === false) {
140
+ * console.log('Using WASM fallback (slower but portable)');
141
+ * } else {
142
+ * console.log('Could not determine roaring installation type');
143
+ * }
144
+ *
145
+ * @example
146
+ * // Useful for diagnostics and performance tuning
147
+ * const diagnostics = {
148
+ * roaringNative: getNativeRoaringAvailable(),
149
+ * // ... other system info
150
+ * };
151
+ */
152
+ export function getNativeRoaringAvailable() {
153
+ if (nativeAvailability !== NOT_CHECKED) {
154
+ return nativeAvailability;
155
+ }
156
+
157
+ try {
158
+ const roaring = loadRoaring();
159
+ const { RoaringBitmap32 } = roaring;
160
+
161
+ // Try the method-based API first (roaring >= 2.x)
162
+ if (typeof RoaringBitmap32.isNativelyInstalled === 'function') {
163
+ nativeAvailability = RoaringBitmap32.isNativelyInstalled();
164
+ return nativeAvailability;
165
+ }
166
+
167
+ // Fall back to property-based API (roaring 1.x)
168
+ if (roaring.isNativelyInstalled !== undefined) {
169
+ nativeAvailability = roaring.isNativelyInstalled;
170
+ return nativeAvailability;
171
+ }
172
+
173
+ // Could not determine - leave as null (indeterminate)
174
+ nativeAvailability = null;
175
+ return nativeAvailability;
176
+ } catch {
177
+ // Loading failed entirely - definitely not available
178
+ nativeAvailability = false;
179
+ return nativeAvailability;
180
+ }
181
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared shard format version constant.
3
+ * Used by BitmapIndexBuilder, StreamingBitmapIndexBuilder, and BitmapIndexReader.
4
+ *
5
+ * Increment when changing the shard structure to ensure reader/writer compatibility.
6
+ *
7
+ * @const {number}
8
+ */
9
+ export const SHARD_VERSION = 2;
@@ -0,0 +1,217 @@
1
+ /**
2
+ * PatchSession - Fluent patch builder with CAS-safe commit.
3
+ *
4
+ * A PatchSession is created by Writer.beginPatch() and provides a fluent API
5
+ * for building graph mutations. The commit uses compare-and-swap semantics
6
+ * to prevent concurrent forks of the writer chain.
7
+ *
8
+ * @module domain/warp/PatchSession
9
+ * @see WARP Writer Spec v1
10
+ */
11
+
12
+ import { buildWriterRef } from '../utils/RefLayout.js';
13
+ import WriterError from '../errors/WriterError.js';
14
+
15
+ /**
16
+ * Fluent patch session for building and committing graph mutations.
17
+ */
18
+ export class PatchSession {
19
+ /**
20
+ * Creates a new PatchSession.
21
+ *
22
+ * @param {Object} options
23
+ * @param {import('../services/PatchBuilderV2.js').PatchBuilderV2} options.builder - Internal builder
24
+ * @param {import('../../ports/GraphPersistencePort.js').default} options.persistence - Git adapter
25
+ * @param {string} options.graphName - Graph namespace
26
+ * @param {string} options.writerId - Writer ID
27
+ * @param {string|null} options.expectedOldHead - Expected parent SHA for CAS
28
+ */
29
+ constructor({ builder, persistence, graphName, writerId, expectedOldHead }) {
30
+ /** @type {import('../services/PatchBuilderV2.js').PatchBuilderV2} */
31
+ this._builder = builder;
32
+
33
+ /** @type {import('../../ports/GraphPersistencePort.js').default} */
34
+ this._persistence = persistence;
35
+
36
+ /** @type {string} */
37
+ this._graphName = graphName;
38
+
39
+ /** @type {string} */
40
+ this._writerId = writerId;
41
+
42
+ /** @type {string|null} */
43
+ this._expectedOldHead = expectedOldHead;
44
+
45
+ /** @type {boolean} */
46
+ this._committed = false;
47
+ }
48
+
49
+ /**
50
+ * Gets the expected old head SHA (for testing).
51
+ * @returns {string|null}
52
+ * @internal
53
+ */
54
+ get _expectedOldHeadForTest() {
55
+ return this._expectedOldHead;
56
+ }
57
+
58
+ /**
59
+ * Adds a node to the graph.
60
+ *
61
+ * @param {string} nodeId - The node ID to add
62
+ * @returns {this} This session for chaining
63
+ * @throws {Error} If this session has already been committed
64
+ */
65
+ addNode(nodeId) {
66
+ this._ensureNotCommitted();
67
+ this._builder.addNode(nodeId);
68
+ return this;
69
+ }
70
+
71
+ /**
72
+ * Removes a node from the graph.
73
+ *
74
+ * Uses observed dots from materialized state for OR-Set removal.
75
+ *
76
+ * @param {string} nodeId - The node ID to remove
77
+ * @returns {this} This session for chaining
78
+ * @throws {Error} If this session has already been committed
79
+ */
80
+ removeNode(nodeId) {
81
+ this._ensureNotCommitted();
82
+ this._builder.removeNode(nodeId);
83
+ return this;
84
+ }
85
+
86
+ /**
87
+ * Adds an edge between two nodes.
88
+ *
89
+ * @param {string} from - Source node ID
90
+ * @param {string} to - Target node ID
91
+ * @param {string} label - Edge label/type
92
+ * @returns {this} This session for chaining
93
+ * @throws {Error} If this session has already been committed
94
+ */
95
+ addEdge(from, to, label) {
96
+ this._ensureNotCommitted();
97
+ this._builder.addEdge(from, to, label);
98
+ return this;
99
+ }
100
+
101
+ /**
102
+ * Removes an edge between two nodes.
103
+ *
104
+ * Uses observed dots from materialized state for OR-Set removal.
105
+ *
106
+ * @param {string} from - Source node ID
107
+ * @param {string} to - Target node ID
108
+ * @param {string} label - Edge label/type
109
+ * @returns {this} This session for chaining
110
+ * @throws {Error} If this session has already been committed
111
+ */
112
+ removeEdge(from, to, label) {
113
+ this._ensureNotCommitted();
114
+ this._builder.removeEdge(from, to, label);
115
+ return this;
116
+ }
117
+
118
+ /**
119
+ * Sets a property on a node.
120
+ *
121
+ * @param {string} nodeId - The node ID
122
+ * @param {string} key - Property key
123
+ * @param {*} value - Property value (must be JSON-serializable)
124
+ * @returns {this} This session for chaining
125
+ * @throws {Error} If this session has already been committed
126
+ */
127
+ setProperty(nodeId, key, value) {
128
+ this._ensureNotCommitted();
129
+ this._builder.setProperty(nodeId, key, value);
130
+ return this;
131
+ }
132
+
133
+ /**
134
+ * Builds the PatchV2 object without committing.
135
+ *
136
+ * @returns {import('../types/WarpTypesV2.js').PatchV2} The constructed patch
137
+ */
138
+ build() {
139
+ return this._builder.build();
140
+ }
141
+
142
+ /**
143
+ * Commits the patch to the graph with CAS protection.
144
+ *
145
+ * @returns {Promise<string>} The commit SHA of the new patch
146
+ * @throws {WriterError} EMPTY_PATCH if no operations were added
147
+ * @throws {WriterError} WRITER_REF_ADVANCED if CAS fails (ref moved since beginPatch)
148
+ * @throws {WriterError} PERSIST_WRITE_FAILED if git operations fail
149
+ *
150
+ * @example
151
+ * const sha = await patch.commit();
152
+ */
153
+ async commit() {
154
+ this._ensureNotCommitted();
155
+
156
+ // Validate not empty
157
+ if (this._builder.ops.length === 0) {
158
+ throw new WriterError('EMPTY_PATCH', 'Cannot commit empty patch: no operations added');
159
+ }
160
+
161
+ const writerRef = buildWriterRef(this._graphName, this._writerId);
162
+
163
+ // Pre-commit CAS check: verify ref hasn't moved
164
+ const currentHead = await this._persistence.readRef(writerRef);
165
+ if (currentHead !== this._expectedOldHead) {
166
+ throw new WriterError(
167
+ 'WRITER_REF_ADVANCED',
168
+ `Writer ref ${writerRef} has advanced since beginPatch(). ` +
169
+ `Expected ${this._expectedOldHead || '(none)'}, found ${currentHead || '(none)'}. ` +
170
+ `Call beginPatch() again to retry.`
171
+ );
172
+ }
173
+
174
+ try {
175
+ // Delegate to PatchBuilderV2.commit() which handles the git operations
176
+ const sha = await this._builder.commit();
177
+ this._committed = true;
178
+ return sha;
179
+ } catch (err) {
180
+ // Check if it's a concurrent commit error from PatchBuilderV2
181
+ if (err.message?.includes('Concurrent commit detected') ||
182
+ err.message?.includes('has advanced')) {
183
+ throw new WriterError(
184
+ 'WRITER_REF_ADVANCED',
185
+ err.message,
186
+ err
187
+ );
188
+ }
189
+
190
+ // Wrap other errors
191
+ throw new WriterError(
192
+ 'PERSIST_WRITE_FAILED',
193
+ `Failed to persist patch: ${err.message}`,
194
+ err
195
+ );
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Gets the number of operations in this patch.
201
+ * @returns {number}
202
+ */
203
+ get opCount() {
204
+ return this._builder.ops.length;
205
+ }
206
+
207
+ /**
208
+ * Ensures the session hasn't been committed yet.
209
+ * @throws {Error} If already committed
210
+ * @private
211
+ */
212
+ _ensureNotCommitted() {
213
+ if (this._committed) {
214
+ throw new Error('PatchSession already committed. Call beginPatch() to create a new session.');
215
+ }
216
+ }
217
+ }