@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,57 @@
1
+ import IndexError from './IndexError.js';
2
+
3
+ /**
4
+ * Error thrown when a storage operation fails.
5
+ *
6
+ * This error indicates that a read or write operation to storage failed,
7
+ * typically due to I/O errors, permission issues, or storage unavailability.
8
+ *
9
+ * @class StorageError
10
+ * @extends IndexError
11
+ *
12
+ * @property {string} name - The error name ('StorageError')
13
+ * @property {string} code - Error code ('STORAGE_ERROR')
14
+ * @property {string} operation - The operation that failed ('read' or 'write')
15
+ * @property {string} oid - Object ID associated with the operation
16
+ * @property {Error} cause - The original error that caused the failure
17
+ * @property {Object} context - Serializable context object for debugging
18
+ *
19
+ * @example
20
+ * try {
21
+ * await storage.write(oid, data);
22
+ * } catch (err) {
23
+ * throw new StorageError('Failed to write to storage', {
24
+ * operation: 'write',
25
+ * oid: 'abc123',
26
+ * cause: err
27
+ * });
28
+ * }
29
+ */
30
+ export default class StorageError extends IndexError {
31
+ /**
32
+ * Creates a new StorageError.
33
+ *
34
+ * @param {string} message - Human-readable error message
35
+ * @param {Object} [options={}] - Error options
36
+ * @param {string} [options.operation] - The operation that failed ('read' or 'write')
37
+ * @param {string} [options.oid] - Object ID associated with the operation
38
+ * @param {Error} [options.cause] - The original error that caused the failure
39
+ * @param {Object} [options.context={}] - Additional context for debugging
40
+ */
41
+ constructor(message, options = {}) {
42
+ const context = {
43
+ ...options.context,
44
+ operation: options.operation,
45
+ oid: options.oid,
46
+ };
47
+
48
+ super(message, {
49
+ code: 'STORAGE_ERROR',
50
+ context,
51
+ });
52
+
53
+ this.operation = options.operation;
54
+ this.oid = options.oid;
55
+ this.cause = options.cause;
56
+ }
57
+ }
@@ -0,0 +1,30 @@
1
+ import WarpError from './WarpError.js';
2
+
3
+ /**
4
+ * Error class for sync transport and replication operations.
5
+ *
6
+ * SyncError is thrown when synchronization between WARP graph instances fails.
7
+ *
8
+ * ## Error Codes
9
+ *
10
+ * | Code | Description |
11
+ * |------|-------------|
12
+ * | `E_SYNC_REMOTE_URL` | Invalid or unsupported remote URL |
13
+ * | `E_SYNC_NETWORK` | Network-level failure |
14
+ * | `E_SYNC_TIMEOUT` | Sync request exceeded timeout |
15
+ * | `E_SYNC_REMOTE` | Remote server returned a 5xx error |
16
+ * | `E_SYNC_PROTOCOL` | Protocol violation: 4xx, invalid JSON, or malformed response |
17
+ * | `SYNC_ERROR` | Generic/default sync error |
18
+ *
19
+ * @class SyncError
20
+ * @extends WarpError
21
+ *
22
+ * @property {string} name - Always 'SyncError' for instanceof checks
23
+ * @property {string} code - Machine-readable error code for programmatic handling
24
+ * @property {Object} context - Serializable context object with error details
25
+ */
26
+ export default class SyncError extends WarpError {
27
+ constructor(message, options = {}) {
28
+ super(message, 'SYNC_ERROR', options);
29
+ }
30
+ }
@@ -0,0 +1,23 @@
1
+ import WarpError from './WarpError.js';
2
+
3
+ /**
4
+ * Error class for graph traversal operations.
5
+ *
6
+ * @class TraversalError
7
+ * @extends WarpError
8
+ *
9
+ * @property {string} name - The error name ('TraversalError')
10
+ * @property {string} code - Error code for programmatic handling (default: 'TRAVERSAL_ERROR')
11
+ * @property {Object} context - Serializable context object for debugging
12
+ *
13
+ * @example
14
+ * throw new TraversalError('Node not found in index', {
15
+ * code: 'NODE_NOT_FOUND',
16
+ * context: { sha: 'abc123', operation: 'bfs' }
17
+ * });
18
+ */
19
+ export default class TraversalError extends WarpError {
20
+ constructor(message, options = {}) {
21
+ super(message, 'TRAVERSAL_ERROR', options);
22
+ }
23
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Base error class for all WARP domain errors.
3
+ *
4
+ * Provides shared constructor logic: name (from constructor), code,
5
+ * context, and stack trace capture. Subclasses reduce to a one-line
6
+ * constructor calling super(message, defaultCode, options).
7
+ *
8
+ * @class WarpError
9
+ * @extends Error
10
+ *
11
+ * @property {string} name - Error name (set from constructor.name)
12
+ * @property {string} code - Machine-readable error code
13
+ * @property {Object} context - Serializable context for debugging
14
+ */
15
+ export default class WarpError extends Error {
16
+ /**
17
+ * @param {string} message - Human-readable error message
18
+ * @param {string} defaultCode - Default error code if not overridden by options
19
+ * @param {Object} [options={}] - Error options
20
+ * @param {string} [options.code] - Override error code
21
+ * @param {Object} [options.context={}] - Serializable context for debugging
22
+ */
23
+ constructor(message, defaultCode, options = {}) {
24
+ super(message);
25
+ const opts = options || {};
26
+ this.name = this.constructor.name;
27
+ this.code = opts.code || defaultCode;
28
+ this.context = opts.context || {};
29
+ Error.captureStackTrace?.(this, this.constructor);
30
+ }
31
+ }
@@ -0,0 +1,28 @@
1
+ import WarpError from './WarpError.js';
2
+
3
+ /**
4
+ * Error class for wormhole compression operations.
5
+ *
6
+ * ## Error Codes
7
+ *
8
+ * | Code | Description |
9
+ * |------|-------------|
10
+ * | `E_WORMHOLE_SHA_NOT_FOUND` | A specified SHA does not exist |
11
+ * | `E_WORMHOLE_INVALID_RANGE` | The from SHA is not an ancestor of to SHA |
12
+ * | `E_WORMHOLE_MULTI_WRITER` | The range spans multiple writers |
13
+ * | `E_WORMHOLE_EMPTY_RANGE` | No patches found in the specified range |
14
+ * | `E_WORMHOLE_NOT_PATCH` | A commit in the range is not a patch commit |
15
+ * | `WORMHOLE_ERROR` | Generic/default wormhole error |
16
+ *
17
+ * @class WormholeError
18
+ * @extends WarpError
19
+ *
20
+ * @property {string} name - Always 'WormholeError' for instanceof checks
21
+ * @property {string} code - Machine-readable error code for programmatic handling
22
+ * @property {Object} context - Serializable context object with error details
23
+ */
24
+ export default class WormholeError extends WarpError {
25
+ constructor(message, options = {}) {
26
+ super(message, 'WORMHOLE_ERROR', options);
27
+ }
28
+ }
@@ -0,0 +1,39 @@
1
+ import WarpError from './WarpError.js';
2
+
3
+ /**
4
+ * Error class for Writer operations.
5
+ *
6
+ * Preserves the existing (code, message, cause) positional constructor
7
+ * signature used throughout PatchSession and PatchBuilderV2, while
8
+ * inheriting from WarpError for unified error hierarchy.
9
+ *
10
+ * ## Error Codes
11
+ *
12
+ * | Code | Description |
13
+ * |------|-------------|
14
+ * | `EMPTY_PATCH` | Patch commit attempted with zero operations |
15
+ * | `WRITER_REF_ADVANCED` | Writer ref moved since beginPatch() |
16
+ * | `WRITER_CAS_CONFLICT` | Compare-and-swap failure during commit |
17
+ * | `PERSIST_WRITE_FAILED` | Git persistence operation failed |
18
+ * | `WRITER_ERROR` | Generic/default writer error |
19
+ *
20
+ * @class WriterError
21
+ * @extends WarpError
22
+ *
23
+ * @property {string} name - Always 'WriterError'
24
+ * @property {string} code - Machine-readable error code
25
+ * @property {Error} [cause] - Original error that caused this error
26
+ */
27
+ export default class WriterError extends WarpError {
28
+ /**
29
+ * @param {string} code - Error code
30
+ * @param {string} message - Human-readable error message
31
+ * @param {Error} [cause] - Original error that caused this error
32
+ */
33
+ constructor(code, message, cause) {
34
+ super(message, 'WRITER_ERROR', { code });
35
+ if (cause !== undefined) {
36
+ this.cause = cause;
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Custom error classes for domain operations.
3
+ *
4
+ * @module domain/errors
5
+ */
6
+
7
+ export { default as EmptyMessageError } from './EmptyMessageError.js';
8
+ export { default as WarpError } from './WarpError.js';
9
+ export { default as ForkError } from './ForkError.js';
10
+ export { default as IndexError } from './IndexError.js';
11
+ export { default as OperationAbortedError } from './OperationAbortedError.js';
12
+ export { default as QueryError } from './QueryError.js';
13
+ export { default as SyncError } from './SyncError.js';
14
+ export { default as ShardCorruptionError } from './ShardCorruptionError.js';
15
+ export { default as ShardLoadError } from './ShardLoadError.js';
16
+ export { default as ShardValidationError } from './ShardValidationError.js';
17
+ export { default as StorageError } from './StorageError.js';
18
+ export { default as SchemaUnsupportedError } from './SchemaUnsupportedError.js';
19
+ export { default as TraversalError } from './TraversalError.js';
20
+ export { default as WriterError } from './WriterError.js';
21
+ export { default as WormholeError } from './WormholeError.js';
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Anchor message encoding and decoding for WARP commit messages.
3
+ *
4
+ * Handles the 'anchor' message type which marks a merge point in the WARP
5
+ * DAG. See {@link module:domain/services/WarpMessageCodec} for the facade
6
+ * that re-exports all codec functions.
7
+ *
8
+ * @module domain/services/AnchorMessageCodec
9
+ */
10
+
11
+ import { validateGraphName } from '../utils/RefLayout.js';
12
+ import {
13
+ getCodec,
14
+ MESSAGE_TITLES,
15
+ TRAILER_KEYS,
16
+ validateSchema,
17
+ } from './MessageCodecInternal.js';
18
+
19
+ // -----------------------------------------------------------------------------
20
+ // Encoder
21
+ // -----------------------------------------------------------------------------
22
+
23
+ /**
24
+ * Encodes an anchor commit message.
25
+ *
26
+ * @param {Object} options - The anchor message options
27
+ * @param {string} options.graph - The graph name
28
+ * @param {number} [options.schema=2] - The schema version (defaults to 2 for new messages)
29
+ * @returns {string} The encoded commit message
30
+ * @throws {Error} If any validation fails
31
+ *
32
+ * @example
33
+ * const message = encodeAnchorMessage({ graph: 'events' });
34
+ */
35
+ export function encodeAnchorMessage({ graph, schema = 2 }) {
36
+ // Validate inputs
37
+ validateGraphName(graph);
38
+ validateSchema(schema);
39
+
40
+ const codec = getCodec();
41
+ return codec.encode({
42
+ title: MESSAGE_TITLES.anchor,
43
+ trailers: {
44
+ [TRAILER_KEYS.kind]: 'anchor',
45
+ [TRAILER_KEYS.graph]: graph,
46
+ [TRAILER_KEYS.schema]: String(schema),
47
+ },
48
+ });
49
+ }
50
+
51
+ // -----------------------------------------------------------------------------
52
+ // Decoder
53
+ // -----------------------------------------------------------------------------
54
+
55
+ /**
56
+ * Decodes an anchor commit message.
57
+ *
58
+ * @param {string} message - The raw commit message
59
+ * @returns {Object} The decoded anchor message
60
+ * @returns {string} return.kind - Always 'anchor'
61
+ * @returns {string} return.graph - The graph name
62
+ * @returns {number} return.schema - The schema version
63
+ * @throws {Error} If the message is not a valid anchor message
64
+ *
65
+ * @example
66
+ * const { kind, graph, schema } = decodeAnchorMessage(message);
67
+ */
68
+ export function decodeAnchorMessage(message) {
69
+ const codec = getCodec();
70
+ const decoded = codec.decode(message);
71
+ const { trailers } = decoded;
72
+
73
+ // Validate kind discriminator
74
+ const kind = trailers[TRAILER_KEYS.kind];
75
+ if (kind !== 'anchor') {
76
+ throw new Error(`Invalid anchor message: eg-kind must be 'anchor', got '${kind}'`);
77
+ }
78
+
79
+ // Extract and validate required fields
80
+ const graph = trailers[TRAILER_KEYS.graph];
81
+ if (!graph) {
82
+ throw new Error('Invalid anchor message: missing required trailer eg-graph');
83
+ }
84
+
85
+ const schemaStr = trailers[TRAILER_KEYS.schema];
86
+ if (!schemaStr) {
87
+ throw new Error('Invalid anchor message: missing required trailer eg-schema');
88
+ }
89
+ const schema = parseInt(schemaStr, 10);
90
+ if (!Number.isInteger(schema) || schema < 1) {
91
+ throw new Error(`Invalid anchor message: eg-schema must be a positive integer, got '${schemaStr}'`);
92
+ }
93
+
94
+ return {
95
+ kind: 'anchor',
96
+ graph,
97
+ schema,
98
+ };
99
+ }
@@ -0,0 +1,225 @@
1
+ import defaultCodec from '../utils/defaultCodec.js';
2
+ import { getRoaringBitmap32, getNativeRoaringAvailable } from '../utils/roaring.js';
3
+ import { canonicalStringify } from '../utils/canonicalStringify.js';
4
+ import { SHARD_VERSION } from '../utils/shardVersion.js';
5
+
6
+ // Re-export for backwards compatibility
7
+ export { SHARD_VERSION };
8
+
9
+ /**
10
+ * Computes a SHA-256 checksum of the given data.
11
+ * Uses canonical JSON stringification for deterministic output
12
+ * across different JavaScript engines.
13
+ *
14
+ * @param {Object} data - The data object to checksum
15
+ * @param {import('../../ports/CryptoPort.js').default} crypto - CryptoPort instance
16
+ * @returns {Promise<string|null>} Hex-encoded SHA-256 hash
17
+ */
18
+ const computeChecksum = async (data, crypto) => {
19
+ if (!crypto) { return null; }
20
+ const json = canonicalStringify(data);
21
+ return await crypto.hash('sha256', json);
22
+ };
23
+
24
+ /** @type {boolean|null} Whether native Roaring bindings are available (null = unknown until first use) */
25
+ export let NATIVE_ROARING_AVAILABLE = null;
26
+
27
+ const ensureRoaringBitmap32 = () => {
28
+ const RoaringBitmap32 = getRoaringBitmap32();
29
+ if (NATIVE_ROARING_AVAILABLE === null) {
30
+ NATIVE_ROARING_AVAILABLE = getNativeRoaringAvailable();
31
+ }
32
+ return RoaringBitmap32;
33
+ };
34
+
35
+ /**
36
+ * Wraps data in a version/checksum envelope.
37
+ * @param {Object} data - The data to wrap
38
+ * @param {import('../../ports/CryptoPort.js').default} crypto - CryptoPort instance
39
+ * @returns {Promise<Object>} Envelope with version, checksum, and data
40
+ */
41
+ const wrapShard = async (data, crypto) => ({
42
+ version: SHARD_VERSION,
43
+ checksum: await computeChecksum(data, crypto),
44
+ data,
45
+ });
46
+
47
+ /**
48
+ * Serializes a frontier Map into CBOR and JSON blobs in the given tree.
49
+ * @param {Map<string, string>} frontier - Writer→tip SHA map
50
+ * @param {Record<string, Buffer>} tree - Target tree to add entries to
51
+ * @param {import('../../ports/CodecPort.js').default} codec - Codec for CBOR serialization
52
+ */
53
+ function serializeFrontierToTree(frontier, tree, codec) {
54
+ const sorted = {};
55
+ for (const key of Array.from(frontier.keys()).sort()) {
56
+ sorted[key] = frontier.get(key);
57
+ }
58
+ const envelope = { version: 1, writerCount: frontier.size, frontier: sorted };
59
+ tree['frontier.cbor'] = Buffer.from(codec.encode(envelope));
60
+ tree['frontier.json'] = Buffer.from(canonicalStringify(envelope));
61
+ }
62
+
63
+ /**
64
+ * Builder for constructing bitmap indexes in memory.
65
+ *
66
+ * This is a pure domain class with no infrastructure dependencies.
67
+ * Create an instance, add nodes and edges, then serialize to persist.
68
+ *
69
+ * Callers that persist the serialized output typically need
70
+ * BlobPort + TreePort + RefPort from the persistence layer.
71
+ *
72
+ * **Performance Note**: Uses Roaring Bitmaps for compression. Native bindings
73
+ * provide best performance. Check `NATIVE_ROARING_AVAILABLE` export if
74
+ * performance is critical.
75
+ *
76
+ * @example
77
+ * import BitmapIndexBuilder, { NATIVE_ROARING_AVAILABLE } from './BitmapIndexBuilder.js';
78
+ * if (NATIVE_ROARING_AVAILABLE === false) {
79
+ * console.warn('Consider installing native Roaring bindings for better performance');
80
+ * }
81
+ * const builder = new BitmapIndexBuilder();
82
+ */
83
+ export default class BitmapIndexBuilder {
84
+ /**
85
+ * Creates a new BitmapIndexBuilder instance.
86
+ *
87
+ * The builder tracks:
88
+ * - SHA to numeric ID mappings (for compact bitmap storage)
89
+ * - Forward edge bitmaps (parent → children)
90
+ * - Reverse edge bitmaps (child → parents)
91
+ *
92
+ * @param {Object} [options] - Configuration options
93
+ * @param {import('../../ports/CryptoPort.js').default} [options.crypto] - CryptoPort instance for hashing
94
+ * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
95
+ */
96
+ constructor({ crypto, codec } = {}) {
97
+ /** @type {import('../../ports/CryptoPort.js').default} */
98
+ this._crypto = crypto;
99
+ /** @type {import('../../ports/CodecPort.js').default|undefined} */
100
+ this._codec = codec || defaultCodec;
101
+ /** @type {Map<string, number>} */
102
+ this.shaToId = new Map();
103
+ /** @type {string[]} */
104
+ this.idToSha = [];
105
+ /** @type {Map<string, RoaringBitmap32>} */
106
+ this.bitmaps = new Map();
107
+ }
108
+
109
+ /**
110
+ * Registers a node without adding edges.
111
+ * Useful for root nodes with no parents.
112
+ *
113
+ * @param {string} sha - The node's SHA
114
+ * @returns {number} The assigned numeric ID
115
+ */
116
+ registerNode(sha) {
117
+ return this._getOrCreateId(sha);
118
+ }
119
+
120
+ /**
121
+ * Adds a directed edge from source to target node.
122
+ *
123
+ * Updates both forward (src → tgt) and reverse (tgt → src) bitmaps.
124
+ *
125
+ * @param {string} srcSha - Source node SHA (parent)
126
+ * @param {string} tgtSha - Target node SHA (child)
127
+ * @returns {void}
128
+ */
129
+ addEdge(srcSha, tgtSha) {
130
+ const srcId = this._getOrCreateId(srcSha);
131
+ const tgtId = this._getOrCreateId(tgtSha);
132
+ this._addToBitmap({ sha: srcSha, id: tgtId, type: 'fwd' });
133
+ this._addToBitmap({ sha: tgtSha, id: srcId, type: 'rev' });
134
+ }
135
+
136
+ /**
137
+ * Serializes the index to a tree structure of buffers.
138
+ *
139
+ * Output structure (sharded by SHA prefix for lazy loading):
140
+ * - `meta_XX.json`: {version, checksum, data: {sha: id, ...}} for SHAs with prefix XX
141
+ * - `shards_fwd_XX.json`: {version, checksum, data: {sha: base64Bitmap, ...}} for forward edges
142
+ * - `shards_rev_XX.json`: {version, checksum, data: {sha: base64Bitmap, ...}} for reverse edges
143
+ *
144
+ * Each shard is wrapped in a version/checksum envelope for integrity verification.
145
+ *
146
+ * @param {Object} [options] - Serialization options
147
+ * @param {Map<string, string>} [options.frontier] - Writer→tip SHA map to include in the tree
148
+ * @returns {Promise<Record<string, Buffer>>} Map of path → serialized content
149
+ */
150
+ async serialize({ frontier } = {}) {
151
+ const tree = {};
152
+
153
+ // Serialize ID mappings (sharded by prefix)
154
+ const idShards = {};
155
+ for (const [sha, id] of this.shaToId) {
156
+ const prefix = sha.substring(0, 2);
157
+ if (!idShards[prefix]) {
158
+ idShards[prefix] = {};
159
+ }
160
+ idShards[prefix][sha] = id;
161
+ }
162
+ for (const [prefix, map] of Object.entries(idShards)) {
163
+ tree[`meta_${prefix}.json`] = Buffer.from(JSON.stringify(await wrapShard(map, this._crypto)));
164
+ }
165
+
166
+ // Serialize bitmaps (sharded by prefix, per-node within shard)
167
+ // Keys are constructed as '${type}_${sha}' by _addToBitmap (e.g., 'fwd_abc123', 'rev_def456')
168
+ const bitmapShards = { fwd: {}, rev: {} };
169
+ for (const [key, bitmap] of this.bitmaps) {
170
+ const [type, sha] = [key.substring(0, 3), key.substring(4)];
171
+ const prefix = sha.substring(0, 2);
172
+
173
+ if (!bitmapShards[type][prefix]) {
174
+ bitmapShards[type][prefix] = {};
175
+ }
176
+ // Encode bitmap as base64 for JSON storage
177
+ bitmapShards[type][prefix][sha] = bitmap.serialize(true).toString('base64');
178
+ }
179
+
180
+ for (const type of ['fwd', 'rev']) {
181
+ for (const [prefix, shardData] of Object.entries(bitmapShards[type])) {
182
+ tree[`shards_${type}_${prefix}.json`] = Buffer.from(JSON.stringify(await wrapShard(shardData, this._crypto)));
183
+ }
184
+ }
185
+
186
+ if (frontier) {
187
+ serializeFrontierToTree(frontier, tree, this._codec);
188
+ }
189
+
190
+ return tree;
191
+ }
192
+
193
+ /**
194
+ * Gets or creates a numeric ID for a SHA.
195
+ * @param {string} sha - The SHA to look up or register
196
+ * @returns {number} The numeric ID
197
+ * @private
198
+ */
199
+ _getOrCreateId(sha) {
200
+ if (this.shaToId.has(sha)) {
201
+ return this.shaToId.get(sha);
202
+ }
203
+ const id = this.idToSha.length;
204
+ this.idToSha.push(sha);
205
+ this.shaToId.set(sha, id);
206
+ return id;
207
+ }
208
+
209
+ /**
210
+ * Adds an ID to a node's bitmap.
211
+ * @param {Object} opts - Options
212
+ * @param {string} opts.sha - The SHA to use as key
213
+ * @param {number} opts.id - The ID to add to the bitmap
214
+ * @param {string} opts.type - 'fwd' or 'rev'
215
+ * @private
216
+ */
217
+ _addToBitmap({ sha, id, type }) {
218
+ const key = `${type}_${sha}`;
219
+ if (!this.bitmaps.has(key)) {
220
+ const RoaringBitmap32 = ensureRoaringBitmap32();
221
+ this.bitmaps.set(key, new RoaringBitmap32());
222
+ }
223
+ this.bitmaps.get(key).add(id);
224
+ }
225
+ }