@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.
- package/LICENSE +201 -0
- package/NOTICE +16 -0
- package/README.md +480 -0
- package/SECURITY.md +30 -0
- package/bin/git-warp +24 -0
- package/bin/warp-graph.js +1574 -0
- package/index.d.ts +2366 -0
- package/index.js +180 -0
- package/package.json +129 -0
- package/scripts/install-git-warp.sh +258 -0
- package/scripts/uninstall-git-warp.sh +139 -0
- package/src/domain/WarpGraph.js +3157 -0
- package/src/domain/crdt/Dot.js +160 -0
- package/src/domain/crdt/LWW.js +154 -0
- package/src/domain/crdt/ORSet.js +371 -0
- package/src/domain/crdt/VersionVector.js +222 -0
- package/src/domain/entities/GraphNode.js +60 -0
- package/src/domain/errors/EmptyMessageError.js +47 -0
- package/src/domain/errors/ForkError.js +30 -0
- package/src/domain/errors/IndexError.js +23 -0
- package/src/domain/errors/OperationAbortedError.js +22 -0
- package/src/domain/errors/QueryError.js +39 -0
- package/src/domain/errors/SchemaUnsupportedError.js +17 -0
- package/src/domain/errors/ShardCorruptionError.js +56 -0
- package/src/domain/errors/ShardLoadError.js +57 -0
- package/src/domain/errors/ShardValidationError.js +61 -0
- package/src/domain/errors/StorageError.js +57 -0
- package/src/domain/errors/SyncError.js +30 -0
- package/src/domain/errors/TraversalError.js +23 -0
- package/src/domain/errors/WarpError.js +31 -0
- package/src/domain/errors/WormholeError.js +28 -0
- package/src/domain/errors/WriterError.js +39 -0
- package/src/domain/errors/index.js +21 -0
- package/src/domain/services/AnchorMessageCodec.js +99 -0
- package/src/domain/services/BitmapIndexBuilder.js +225 -0
- package/src/domain/services/BitmapIndexReader.js +435 -0
- package/src/domain/services/BoundaryTransitionRecord.js +463 -0
- package/src/domain/services/CheckpointMessageCodec.js +147 -0
- package/src/domain/services/CheckpointSerializerV5.js +281 -0
- package/src/domain/services/CheckpointService.js +384 -0
- package/src/domain/services/CommitDagTraversalService.js +156 -0
- package/src/domain/services/DagPathFinding.js +712 -0
- package/src/domain/services/DagTopology.js +239 -0
- package/src/domain/services/DagTraversal.js +245 -0
- package/src/domain/services/Frontier.js +108 -0
- package/src/domain/services/GCMetrics.js +101 -0
- package/src/domain/services/GCPolicy.js +122 -0
- package/src/domain/services/GitLogParser.js +205 -0
- package/src/domain/services/HealthCheckService.js +246 -0
- package/src/domain/services/HookInstaller.js +326 -0
- package/src/domain/services/HttpSyncServer.js +262 -0
- package/src/domain/services/IndexRebuildService.js +426 -0
- package/src/domain/services/IndexStalenessChecker.js +103 -0
- package/src/domain/services/JoinReducer.js +582 -0
- package/src/domain/services/KeyCodec.js +113 -0
- package/src/domain/services/LegacyAnchorDetector.js +67 -0
- package/src/domain/services/LogicalTraversal.js +351 -0
- package/src/domain/services/MessageCodecInternal.js +132 -0
- package/src/domain/services/MessageSchemaDetector.js +145 -0
- package/src/domain/services/MigrationService.js +55 -0
- package/src/domain/services/ObserverView.js +265 -0
- package/src/domain/services/PatchBuilderV2.js +669 -0
- package/src/domain/services/PatchMessageCodec.js +140 -0
- package/src/domain/services/ProvenanceIndex.js +337 -0
- package/src/domain/services/ProvenancePayload.js +242 -0
- package/src/domain/services/QueryBuilder.js +835 -0
- package/src/domain/services/StateDiff.js +300 -0
- package/src/domain/services/StateSerializerV5.js +156 -0
- package/src/domain/services/StreamingBitmapIndexBuilder.js +709 -0
- package/src/domain/services/SyncProtocol.js +593 -0
- package/src/domain/services/TemporalQuery.js +201 -0
- package/src/domain/services/TranslationCost.js +221 -0
- package/src/domain/services/TraversalService.js +8 -0
- package/src/domain/services/WarpMessageCodec.js +29 -0
- package/src/domain/services/WarpStateIndexBuilder.js +127 -0
- package/src/domain/services/WormholeService.js +353 -0
- package/src/domain/types/TickReceipt.js +285 -0
- package/src/domain/types/WarpTypes.js +209 -0
- package/src/domain/types/WarpTypesV2.js +200 -0
- package/src/domain/utils/CachedValue.js +140 -0
- package/src/domain/utils/EventId.js +89 -0
- package/src/domain/utils/LRUCache.js +112 -0
- package/src/domain/utils/MinHeap.js +114 -0
- package/src/domain/utils/RefLayout.js +280 -0
- package/src/domain/utils/WriterId.js +205 -0
- package/src/domain/utils/cancellation.js +33 -0
- package/src/domain/utils/canonicalStringify.js +42 -0
- package/src/domain/utils/defaultClock.js +20 -0
- package/src/domain/utils/defaultCodec.js +51 -0
- package/src/domain/utils/nullLogger.js +21 -0
- package/src/domain/utils/roaring.js +181 -0
- package/src/domain/utils/shardVersion.js +9 -0
- package/src/domain/warp/PatchSession.js +217 -0
- package/src/domain/warp/Writer.js +181 -0
- package/src/hooks/post-merge.sh +60 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +225 -0
- package/src/infrastructure/adapters/ClockAdapter.js +57 -0
- package/src/infrastructure/adapters/ConsoleLogger.js +150 -0
- package/src/infrastructure/adapters/DenoHttpAdapter.js +230 -0
- package/src/infrastructure/adapters/GitGraphAdapter.js +787 -0
- package/src/infrastructure/adapters/GlobalClockAdapter.js +5 -0
- package/src/infrastructure/adapters/NoOpLogger.js +62 -0
- package/src/infrastructure/adapters/NodeCryptoAdapter.js +32 -0
- package/src/infrastructure/adapters/NodeHttpAdapter.js +98 -0
- package/src/infrastructure/adapters/PerformanceClockAdapter.js +5 -0
- package/src/infrastructure/adapters/WebCryptoAdapter.js +121 -0
- package/src/infrastructure/codecs/CborCodec.js +384 -0
- package/src/ports/BlobPort.js +30 -0
- package/src/ports/ClockPort.js +25 -0
- package/src/ports/CodecPort.js +25 -0
- package/src/ports/CommitPort.js +114 -0
- package/src/ports/ConfigPort.js +31 -0
- package/src/ports/CryptoPort.js +38 -0
- package/src/ports/GraphPersistencePort.js +57 -0
- package/src/ports/HttpServerPort.js +25 -0
- package/src/ports/IndexStoragePort.js +39 -0
- package/src/ports/LoggerPort.js +68 -0
- package/src/ports/RefPort.js +51 -0
- package/src/ports/TreePort.js +51 -0
- package/src/visualization/index.js +26 -0
- package/src/visualization/layouts/converters.js +75 -0
- package/src/visualization/layouts/elkAdapter.js +86 -0
- package/src/visualization/layouts/elkLayout.js +95 -0
- package/src/visualization/layouts/index.js +29 -0
- package/src/visualization/renderers/ascii/box.js +16 -0
- package/src/visualization/renderers/ascii/check.js +271 -0
- package/src/visualization/renderers/ascii/colors.js +13 -0
- package/src/visualization/renderers/ascii/formatters.js +73 -0
- package/src/visualization/renderers/ascii/graph.js +344 -0
- package/src/visualization/renderers/ascii/history.js +335 -0
- package/src/visualization/renderers/ascii/index.js +14 -0
- package/src/visualization/renderers/ascii/info.js +245 -0
- package/src/visualization/renderers/ascii/materialize.js +255 -0
- package/src/visualization/renderers/ascii/path.js +240 -0
- package/src/visualization/renderers/ascii/progress.js +32 -0
- package/src/visualization/renderers/ascii/symbols.js +33 -0
- package/src/visualization/renderers/ascii/table.js +19 -0
- package/src/visualization/renderers/browser/index.js +1 -0
- package/src/visualization/renderers/svg/index.js +159 -0
- package/src/visualization/utils/ansi.js +14 -0
- package/src/visualization/utils/time.js +40 -0
- package/src/visualization/utils/truncate.js +40 -0
- package/src/visualization/utils/unicode.js +52 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch message encoding and decoding for WARP commit messages.
|
|
3
|
+
*
|
|
4
|
+
* Handles the 'patch' message type which contains graph mutations from a
|
|
5
|
+
* single writer. See {@link module:domain/services/WarpMessageCodec} for the
|
|
6
|
+
* facade that re-exports all codec functions.
|
|
7
|
+
*
|
|
8
|
+
* @module domain/services/PatchMessageCodec
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { validateGraphName, validateWriterId } from '../utils/RefLayout.js';
|
|
12
|
+
import {
|
|
13
|
+
getCodec,
|
|
14
|
+
MESSAGE_TITLES,
|
|
15
|
+
TRAILER_KEYS,
|
|
16
|
+
validateOid,
|
|
17
|
+
validatePositiveInteger,
|
|
18
|
+
validateSchema,
|
|
19
|
+
} from './MessageCodecInternal.js';
|
|
20
|
+
|
|
21
|
+
// -----------------------------------------------------------------------------
|
|
22
|
+
// Encoder
|
|
23
|
+
// -----------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Encodes a patch commit message.
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} options - The patch message options
|
|
29
|
+
* @param {string} options.graph - The graph name
|
|
30
|
+
* @param {string} options.writer - The writer ID
|
|
31
|
+
* @param {number} options.lamport - The Lamport timestamp (must be a positive integer)
|
|
32
|
+
* @param {string} options.patchOid - The OID of the patch blob
|
|
33
|
+
* @param {number} [options.schema=2] - The schema version (defaults to 2 for new messages)
|
|
34
|
+
* @returns {string} The encoded commit message
|
|
35
|
+
* @throws {Error} If any validation fails
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const message = encodePatchMessage({
|
|
39
|
+
* graph: 'events',
|
|
40
|
+
* writer: 'node-1',
|
|
41
|
+
* lamport: 42,
|
|
42
|
+
* patchOid: 'abc123...' // 40-char hex
|
|
43
|
+
* });
|
|
44
|
+
*/
|
|
45
|
+
export function encodePatchMessage({ graph, writer, lamport, patchOid, schema = 2 }) {
|
|
46
|
+
// Validate inputs
|
|
47
|
+
validateGraphName(graph);
|
|
48
|
+
validateWriterId(writer);
|
|
49
|
+
validatePositiveInteger(lamport, 'lamport');
|
|
50
|
+
validateOid(patchOid, 'patchOid');
|
|
51
|
+
validateSchema(schema);
|
|
52
|
+
|
|
53
|
+
const codec = getCodec();
|
|
54
|
+
return codec.encode({
|
|
55
|
+
title: MESSAGE_TITLES.patch,
|
|
56
|
+
trailers: {
|
|
57
|
+
[TRAILER_KEYS.kind]: 'patch',
|
|
58
|
+
[TRAILER_KEYS.graph]: graph,
|
|
59
|
+
[TRAILER_KEYS.writer]: writer,
|
|
60
|
+
[TRAILER_KEYS.lamport]: String(lamport),
|
|
61
|
+
[TRAILER_KEYS.patchOid]: patchOid,
|
|
62
|
+
[TRAILER_KEYS.schema]: String(schema),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// -----------------------------------------------------------------------------
|
|
68
|
+
// Decoder
|
|
69
|
+
// -----------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Decodes a patch commit message.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} message - The raw commit message
|
|
75
|
+
* @returns {Object} The decoded patch message
|
|
76
|
+
* @returns {string} return.kind - Always 'patch'
|
|
77
|
+
* @returns {string} return.graph - The graph name
|
|
78
|
+
* @returns {string} return.writer - The writer ID
|
|
79
|
+
* @returns {number} return.lamport - The Lamport timestamp
|
|
80
|
+
* @returns {string} return.patchOid - The patch blob OID
|
|
81
|
+
* @returns {number} return.schema - The schema version
|
|
82
|
+
* @throws {Error} If the message is not a valid patch message
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* const { kind, graph, writer, lamport, patchOid, schema } = decodePatchMessage(message);
|
|
86
|
+
*/
|
|
87
|
+
export function decodePatchMessage(message) {
|
|
88
|
+
const codec = getCodec();
|
|
89
|
+
const decoded = codec.decode(message);
|
|
90
|
+
const { trailers } = decoded;
|
|
91
|
+
|
|
92
|
+
// Validate kind discriminator
|
|
93
|
+
const kind = trailers[TRAILER_KEYS.kind];
|
|
94
|
+
if (kind !== 'patch') {
|
|
95
|
+
throw new Error(`Invalid patch message: eg-kind must be 'patch', got '${kind}'`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Extract and validate required fields
|
|
99
|
+
const graph = trailers[TRAILER_KEYS.graph];
|
|
100
|
+
if (!graph) {
|
|
101
|
+
throw new Error('Invalid patch message: missing required trailer eg-graph');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const writer = trailers[TRAILER_KEYS.writer];
|
|
105
|
+
if (!writer) {
|
|
106
|
+
throw new Error('Invalid patch message: missing required trailer eg-writer');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const lamportStr = trailers[TRAILER_KEYS.lamport];
|
|
110
|
+
if (!lamportStr) {
|
|
111
|
+
throw new Error('Invalid patch message: missing required trailer eg-lamport');
|
|
112
|
+
}
|
|
113
|
+
const lamport = parseInt(lamportStr, 10);
|
|
114
|
+
if (!Number.isInteger(lamport) || lamport < 1) {
|
|
115
|
+
throw new Error(`Invalid patch message: eg-lamport must be a positive integer, got '${lamportStr}'`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const patchOid = trailers[TRAILER_KEYS.patchOid];
|
|
119
|
+
if (!patchOid) {
|
|
120
|
+
throw new Error('Invalid patch message: missing required trailer eg-patch-oid');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const schemaStr = trailers[TRAILER_KEYS.schema];
|
|
124
|
+
if (!schemaStr) {
|
|
125
|
+
throw new Error('Invalid patch message: missing required trailer eg-schema');
|
|
126
|
+
}
|
|
127
|
+
const schema = parseInt(schemaStr, 10);
|
|
128
|
+
if (!Number.isInteger(schema) || schema < 1) {
|
|
129
|
+
throw new Error(`Invalid patch message: eg-schema must be a positive integer, got '${schemaStr}'`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
kind: 'patch',
|
|
134
|
+
graph,
|
|
135
|
+
writer,
|
|
136
|
+
lamport,
|
|
137
|
+
patchOid,
|
|
138
|
+
schema,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import defaultCodec from '../utils/defaultCodec.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ProvenanceIndex - Node-to-Patch SHA Index
|
|
5
|
+
*
|
|
6
|
+
* Implements HG/IO/2: Build nodeId-to-patchSha index from I/O declarations.
|
|
7
|
+
* This enables quick answers to "which patches affected node X?" without
|
|
8
|
+
* replaying all patches.
|
|
9
|
+
*
|
|
10
|
+
* The index maps:
|
|
11
|
+
* - nodeId -> Set<patchSha> (patches that read or wrote this node)
|
|
12
|
+
*
|
|
13
|
+
* This supports the computational holography theorem from Paper III:
|
|
14
|
+
* given a target node, we can compute its backward causal cone D(v)
|
|
15
|
+
* by walking the index.
|
|
16
|
+
*
|
|
17
|
+
* @module domain/services/ProvenanceIndex
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* ProvenanceIndex - Maps node/edge IDs to contributing patch SHAs.
|
|
22
|
+
*
|
|
23
|
+
* This index is built incrementally during materialization by extracting
|
|
24
|
+
* the `reads` and `writes` arrays from each patch's I/O declarations
|
|
25
|
+
* (implemented by HG/IO/1).
|
|
26
|
+
*
|
|
27
|
+
* ## Usage
|
|
28
|
+
*
|
|
29
|
+
* ```javascript
|
|
30
|
+
* const index = new ProvenanceIndex();
|
|
31
|
+
*
|
|
32
|
+
* // During materialization, add each patch's I/O declarations
|
|
33
|
+
* index.addPatch(patchSha, patch.reads, patch.writes);
|
|
34
|
+
*
|
|
35
|
+
* // Query: which patches affected this node?
|
|
36
|
+
* const shas = index.patchesFor('user:alice');
|
|
37
|
+
* // Returns: ['abc123', 'def456', ...]
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* ## Persistence
|
|
41
|
+
*
|
|
42
|
+
* The index can be serialized for checkpoint storage:
|
|
43
|
+
* ```javascript
|
|
44
|
+
* const buffer = index.serialize();
|
|
45
|
+
* // Store in checkpoint tree as provenanceIndex.cbor
|
|
46
|
+
*
|
|
47
|
+
* // Later, restore from checkpoint
|
|
48
|
+
* const index = ProvenanceIndex.deserialize(buffer);
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
class ProvenanceIndex {
|
|
52
|
+
/**
|
|
53
|
+
* Internal index mapping nodeId/edgeKey to Set of patch SHAs.
|
|
54
|
+
* @type {Map<string, Set<string>>}
|
|
55
|
+
* @private
|
|
56
|
+
*/
|
|
57
|
+
#index;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates a new ProvenanceIndex.
|
|
61
|
+
*
|
|
62
|
+
* @param {Map<string, Set<string>>} [initialIndex] - Optional initial index data (defensively copied)
|
|
63
|
+
*/
|
|
64
|
+
constructor(initialIndex) {
|
|
65
|
+
if (initialIndex) {
|
|
66
|
+
// Defensive copy to prevent external mutation
|
|
67
|
+
this.#index = new Map();
|
|
68
|
+
for (const [k, v] of initialIndex) {
|
|
69
|
+
this.#index.set(k, new Set(v));
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
this.#index = new Map();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Creates an empty ProvenanceIndex.
|
|
78
|
+
*
|
|
79
|
+
* @returns {ProvenanceIndex} A fresh, empty index
|
|
80
|
+
*/
|
|
81
|
+
static empty() {
|
|
82
|
+
return new ProvenanceIndex();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Adds a patch's I/O declarations to the index.
|
|
87
|
+
*
|
|
88
|
+
* Both reads and writes are indexed because both indicate that
|
|
89
|
+
* the patch "affected" the entity:
|
|
90
|
+
* - Writes: the patch modified the entity
|
|
91
|
+
* - Reads: the patch's result depends on the entity's state
|
|
92
|
+
*
|
|
93
|
+
* This enables computing the full backward causal cone for slicing.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} patchSha - The Git SHA of the patch commit
|
|
96
|
+
* @param {string[]|undefined} reads - Array of nodeIds/edgeKeys read by this patch
|
|
97
|
+
* @param {string[]|undefined} writes - Array of nodeIds/edgeKeys written by this patch
|
|
98
|
+
* @returns {ProvenanceIndex} This index for chaining
|
|
99
|
+
*/
|
|
100
|
+
addPatch(patchSha, reads, writes) {
|
|
101
|
+
// Index all reads
|
|
102
|
+
if (reads && reads.length > 0) {
|
|
103
|
+
for (const entityId of reads) {
|
|
104
|
+
this.#addEntry(entityId, patchSha);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Index all writes
|
|
109
|
+
if (writes && writes.length > 0) {
|
|
110
|
+
for (const entityId of writes) {
|
|
111
|
+
this.#addEntry(entityId, patchSha);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Adds a single entry to the index.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} entityId - The node ID or edge key
|
|
122
|
+
* @param {string} patchSha - The patch SHA
|
|
123
|
+
* @private
|
|
124
|
+
*/
|
|
125
|
+
#addEntry(entityId, patchSha) {
|
|
126
|
+
let shas = this.#index.get(entityId);
|
|
127
|
+
if (!shas) {
|
|
128
|
+
shas = new Set();
|
|
129
|
+
this.#index.set(entityId, shas);
|
|
130
|
+
}
|
|
131
|
+
shas.add(patchSha);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Returns all patch SHAs that affected a given node or edge.
|
|
136
|
+
*
|
|
137
|
+
* "Affected" means the patch either read from or wrote to the entity.
|
|
138
|
+
* The returned array is sorted alphabetically for determinism.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} entityId - The node ID or edge key to query
|
|
141
|
+
* @returns {string[]} Array of patch SHAs, sorted alphabetically
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* const shas = index.patchesFor('user:alice');
|
|
145
|
+
* // Returns: ['abc123', 'def456', 'ghi789']
|
|
146
|
+
*/
|
|
147
|
+
patchesFor(entityId) {
|
|
148
|
+
const shas = this.#index.get(entityId);
|
|
149
|
+
if (!shas || shas.size === 0) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
return [...shas].sort();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Returns whether the index has any entries for a given entity.
|
|
157
|
+
*
|
|
158
|
+
* @param {string} entityId - The node ID or edge key to check
|
|
159
|
+
* @returns {boolean} True if the entity has at least one contributing patch
|
|
160
|
+
*/
|
|
161
|
+
has(entityId) {
|
|
162
|
+
const shas = this.#index.get(entityId);
|
|
163
|
+
return shas !== undefined && shas.size > 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Returns the number of entities in the index.
|
|
168
|
+
*
|
|
169
|
+
* @returns {number} Count of indexed entities
|
|
170
|
+
*/
|
|
171
|
+
get size() {
|
|
172
|
+
return this.#index.size;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Returns all entity IDs in the index.
|
|
177
|
+
*
|
|
178
|
+
* @returns {string[]} Array of entity IDs, sorted alphabetically
|
|
179
|
+
*/
|
|
180
|
+
entities() {
|
|
181
|
+
return [...this.#index.keys()].sort();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Clears all entries from the index.
|
|
186
|
+
*
|
|
187
|
+
* @returns {ProvenanceIndex} This index for chaining
|
|
188
|
+
*/
|
|
189
|
+
clear() {
|
|
190
|
+
this.#index.clear();
|
|
191
|
+
return this;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Merges another index into this one.
|
|
196
|
+
*
|
|
197
|
+
* All entries from the other index are added to this index.
|
|
198
|
+
* This is useful for combining indexes from different sources
|
|
199
|
+
* (e.g., checkpoint index + incremental patches).
|
|
200
|
+
*
|
|
201
|
+
* @param {ProvenanceIndex} other - The index to merge in
|
|
202
|
+
* @returns {ProvenanceIndex} This index for chaining
|
|
203
|
+
*/
|
|
204
|
+
merge(other) {
|
|
205
|
+
for (const [entityId, shas] of other.#index) {
|
|
206
|
+
for (const sha of shas) {
|
|
207
|
+
this.#addEntry(entityId, sha);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return this;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Creates a clone of this index.
|
|
215
|
+
*
|
|
216
|
+
* @returns {ProvenanceIndex} A new index with the same data
|
|
217
|
+
*/
|
|
218
|
+
clone() {
|
|
219
|
+
const clonedMap = new Map();
|
|
220
|
+
for (const [entityId, shas] of this.#index) {
|
|
221
|
+
clonedMap.set(entityId, new Set(shas));
|
|
222
|
+
}
|
|
223
|
+
return new ProvenanceIndex(clonedMap);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Returns sorted entries for deterministic output.
|
|
228
|
+
*
|
|
229
|
+
* @returns {Array<[string, string[]]>} Sorted array of [entityId, sortedShas[]] pairs
|
|
230
|
+
* @private
|
|
231
|
+
*/
|
|
232
|
+
#sortedEntries() {
|
|
233
|
+
const entries = [];
|
|
234
|
+
for (const [entityId, shas] of this.#index) {
|
|
235
|
+
entries.push([entityId, [...shas].sort()]);
|
|
236
|
+
}
|
|
237
|
+
entries.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
238
|
+
return entries;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Serializes the index to CBOR format for checkpoint storage.
|
|
243
|
+
*
|
|
244
|
+
* The serialized format is a sorted array of [entityId, sortedShas[]] pairs
|
|
245
|
+
* for deterministic output.
|
|
246
|
+
*
|
|
247
|
+
* @param {Object} [options]
|
|
248
|
+
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
|
|
249
|
+
* @returns {Buffer} CBOR-encoded index
|
|
250
|
+
*/
|
|
251
|
+
serialize({ codec } = {}) {
|
|
252
|
+
const c = codec || defaultCodec;
|
|
253
|
+
return c.encode({ version: 1, entries: this.#sortedEntries() });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Builds an index Map from an entries array.
|
|
258
|
+
*
|
|
259
|
+
* @param {Array<[string, string[]]>} entries - Array of [entityId, shas[]] pairs
|
|
260
|
+
* @returns {Map<string, Set<string>>} The built index
|
|
261
|
+
* @private
|
|
262
|
+
*/
|
|
263
|
+
static #buildIndex(entries) {
|
|
264
|
+
const index = new Map();
|
|
265
|
+
for (const [entityId, shas] of entries) {
|
|
266
|
+
index.set(entityId, new Set(shas));
|
|
267
|
+
}
|
|
268
|
+
return index;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Deserializes an index from CBOR format.
|
|
273
|
+
*
|
|
274
|
+
* @param {Buffer} buffer - CBOR-encoded index
|
|
275
|
+
* @param {Object} [options]
|
|
276
|
+
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
|
|
277
|
+
* @returns {ProvenanceIndex} The deserialized index
|
|
278
|
+
* @throws {Error} If the buffer contains an unsupported version
|
|
279
|
+
*/
|
|
280
|
+
static deserialize(buffer, { codec } = {}) {
|
|
281
|
+
const c = codec || defaultCodec;
|
|
282
|
+
const obj = c.decode(buffer);
|
|
283
|
+
|
|
284
|
+
if (obj.version !== 1) {
|
|
285
|
+
throw new Error(`Unsupported ProvenanceIndex version: ${obj.version}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!obj.entries || !Array.isArray(obj.entries)) {
|
|
289
|
+
throw new Error('Missing or invalid ProvenanceIndex entries');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return new ProvenanceIndex(ProvenanceIndex.#buildIndex(obj.entries));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Returns a JSON-serializable representation of this index.
|
|
297
|
+
*
|
|
298
|
+
* @returns {Object} Object with version and entries array
|
|
299
|
+
*/
|
|
300
|
+
toJSON() {
|
|
301
|
+
return { version: 1, entries: this.#sortedEntries() };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Creates a ProvenanceIndex from a JSON representation.
|
|
306
|
+
*
|
|
307
|
+
* @param {Object} json - Object with version and entries array
|
|
308
|
+
* @returns {ProvenanceIndex} The deserialized index
|
|
309
|
+
* @throws {Error} If the JSON contains an unsupported version
|
|
310
|
+
*/
|
|
311
|
+
static fromJSON(json) {
|
|
312
|
+
if (json.version !== 1) {
|
|
313
|
+
throw new Error(`Unsupported ProvenanceIndex version: ${json.version}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!json.entries || !Array.isArray(json.entries)) {
|
|
317
|
+
throw new Error('Missing or invalid ProvenanceIndex entries');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return new ProvenanceIndex(ProvenanceIndex.#buildIndex(json.entries || []));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Returns an iterator over [entityId, patchShas[]] pairs in deterministic order.
|
|
325
|
+
* Uses #sortedEntries() to ensure consistent ordering across iterations.
|
|
326
|
+
*
|
|
327
|
+
* @returns {Iterator<[string, string[]]>} Iterator over index entries
|
|
328
|
+
*/
|
|
329
|
+
*[Symbol.iterator]() {
|
|
330
|
+
for (const entry of this.#sortedEntries()) {
|
|
331
|
+
yield entry;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export default ProvenanceIndex;
|
|
337
|
+
export { ProvenanceIndex };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProvenancePayload - Transferable Provenance as a Monoid
|
|
3
|
+
*
|
|
4
|
+
* Implements the provenance payload from Paper III (Computational Holography):
|
|
5
|
+
* P = (mu_0, ..., mu_{n-1}) - an ordered sequence of tick patches.
|
|
6
|
+
*
|
|
7
|
+
* The payload monoid (Payload, ., epsilon):
|
|
8
|
+
* - Composition is concatenation
|
|
9
|
+
* - Identity is empty sequence
|
|
10
|
+
*
|
|
11
|
+
* Monoid laws hold:
|
|
12
|
+
* - identity.concat(p) === p (left identity)
|
|
13
|
+
* - p.concat(identity) === p (right identity)
|
|
14
|
+
* - (a.concat(b)).concat(c) === a.concat(b.concat(c)) (associativity)
|
|
15
|
+
*
|
|
16
|
+
* @module domain/services/ProvenancePayload
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { reduceV5, createEmptyStateV5, cloneStateV5 } from './JoinReducer.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A single patch entry in the provenance payload.
|
|
23
|
+
*
|
|
24
|
+
* @typedef {Object} PatchEntry
|
|
25
|
+
* @property {Object} patch - The decoded patch object (writer, lamport, ops, context)
|
|
26
|
+
* @property {string} sha - The Git SHA of the patch commit
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* ProvenancePayload - Immutable sequence of patches forming a monoid.
|
|
31
|
+
*
|
|
32
|
+
* This class packages an ordered sequence of patches as a transferable
|
|
33
|
+
* provenance payload. Combined with an initial state (boundary encoding),
|
|
34
|
+
* it enables deterministic replay (computational holography).
|
|
35
|
+
*
|
|
36
|
+
* ## Monoid Structure
|
|
37
|
+
*
|
|
38
|
+
* ProvenancePayload forms a monoid under concatenation:
|
|
39
|
+
* - **Identity**: `ProvenancePayload.identity()` returns the empty payload
|
|
40
|
+
* - **Composition**: `a.concat(b)` concatenates two payloads
|
|
41
|
+
*
|
|
42
|
+
* ## Computational Holography
|
|
43
|
+
*
|
|
44
|
+
* Given a boundary encoding B = (U_0, P) where:
|
|
45
|
+
* - U_0 is the initial state
|
|
46
|
+
* - P is the provenance payload
|
|
47
|
+
*
|
|
48
|
+
* The `replay(U_0)` method uniquely determines the interior worldline,
|
|
49
|
+
* producing the final materialized state.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```javascript
|
|
53
|
+
* // Create payload from patches
|
|
54
|
+
* const payload = new ProvenancePayload([
|
|
55
|
+
* { patch: patch1, sha: 'abc123' },
|
|
56
|
+
* { patch: patch2, sha: 'def456' },
|
|
57
|
+
* ]);
|
|
58
|
+
*
|
|
59
|
+
* // Monoid operations
|
|
60
|
+
* const empty = ProvenancePayload.identity();
|
|
61
|
+
* const combined = payload1.concat(payload2);
|
|
62
|
+
*
|
|
63
|
+
* // Replay to materialize state
|
|
64
|
+
* const state = payload.replay();
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
class ProvenancePayload {
|
|
68
|
+
/**
|
|
69
|
+
* The internal array of patch entries. Frozen after construction.
|
|
70
|
+
* @type {ReadonlyArray<PatchEntry>}
|
|
71
|
+
* @private
|
|
72
|
+
*/
|
|
73
|
+
#patches;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Creates a new ProvenancePayload from an ordered sequence of patches.
|
|
77
|
+
*
|
|
78
|
+
* The payload is immutable after construction - the patches array is
|
|
79
|
+
* frozen to prevent modification.
|
|
80
|
+
*
|
|
81
|
+
* @param {Array<PatchEntry>} patches - Ordered sequence of patch entries.
|
|
82
|
+
* Each entry must have { patch, sha } where patch is the decoded patch
|
|
83
|
+
* object and sha is the Git commit SHA.
|
|
84
|
+
* @throws {TypeError} If patches is not an array
|
|
85
|
+
*/
|
|
86
|
+
constructor(patches = []) {
|
|
87
|
+
if (!Array.isArray(patches)) {
|
|
88
|
+
throw new TypeError('ProvenancePayload requires an array of patches');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Shallow copy and freeze to ensure immutability
|
|
92
|
+
this.#patches = Object.freeze([...patches]);
|
|
93
|
+
|
|
94
|
+
// Freeze the instance itself
|
|
95
|
+
Object.freeze(this);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns the identity element of the payload monoid.
|
|
100
|
+
*
|
|
101
|
+
* The identity payload contains no patches. It satisfies:
|
|
102
|
+
* - `identity.concat(p)` equals `p` for any payload `p`
|
|
103
|
+
* - `p.concat(identity)` equals `p` for any payload `p`
|
|
104
|
+
*
|
|
105
|
+
* @returns {ProvenancePayload} The empty/identity payload
|
|
106
|
+
*/
|
|
107
|
+
static identity() {
|
|
108
|
+
return new ProvenancePayload([]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Returns the number of patches in this payload.
|
|
113
|
+
*
|
|
114
|
+
* @returns {number} The patch count
|
|
115
|
+
*/
|
|
116
|
+
get length() {
|
|
117
|
+
return this.#patches.length;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Concatenates this payload with another, forming a new payload.
|
|
122
|
+
*
|
|
123
|
+
* This is the monoid composition operation. The resulting payload
|
|
124
|
+
* contains all patches from this payload followed by all patches
|
|
125
|
+
* from the other payload.
|
|
126
|
+
*
|
|
127
|
+
* Monoid laws:
|
|
128
|
+
* - `identity.concat(p) === p` (left identity)
|
|
129
|
+
* - `p.concat(identity) === p` (right identity)
|
|
130
|
+
* - `(a.concat(b)).concat(c) === a.concat(b.concat(c))` (associativity)
|
|
131
|
+
*
|
|
132
|
+
* @param {ProvenancePayload} other - The payload to append
|
|
133
|
+
* @returns {ProvenancePayload} A new payload with combined patches
|
|
134
|
+
* @throws {TypeError} If other is not a ProvenancePayload
|
|
135
|
+
*/
|
|
136
|
+
concat(other) {
|
|
137
|
+
if (!(other instanceof ProvenancePayload)) {
|
|
138
|
+
throw new TypeError('concat requires a ProvenancePayload');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Optimization: avoid array allocation for identity cases
|
|
142
|
+
if (this.#patches.length === 0) {
|
|
143
|
+
return other;
|
|
144
|
+
}
|
|
145
|
+
if (other.#patches.length === 0) {
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return new ProvenancePayload([...this.#patches, ...other.#patches]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Replays the payload to produce a materialized state.
|
|
154
|
+
*
|
|
155
|
+
* This implements the computational holography theorem (Paper III):
|
|
156
|
+
* Given a boundary encoding B = (U_0, P), Replay(B) uniquely
|
|
157
|
+
* determines the interior worldline.
|
|
158
|
+
*
|
|
159
|
+
* The replay applies patches in order using CRDT merge semantics:
|
|
160
|
+
* - Nodes/edges use OR-Set (add-wins)
|
|
161
|
+
* - Properties use LWW (Last-Write-Wins)
|
|
162
|
+
*
|
|
163
|
+
* @param {import('./JoinReducer.js').WarpStateV5} [initialState] - The initial
|
|
164
|
+
* state U_0 to replay from. If omitted, starts from empty state.
|
|
165
|
+
* @returns {import('./JoinReducer.js').WarpStateV5} The final materialized state
|
|
166
|
+
*/
|
|
167
|
+
replay(initialState) {
|
|
168
|
+
// Handle empty payload - return clone of initial or fresh empty state
|
|
169
|
+
if (this.#patches.length === 0) {
|
|
170
|
+
return initialState ? cloneStateV5(initialState) : createEmptyStateV5();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Use JoinReducer's reduceV5 for deterministic materialization.
|
|
174
|
+
// Note: reduceV5 returns { state, receipts } when options.receipts is truthy,
|
|
175
|
+
// but returns bare WarpStateV5 when no options passed (as here).
|
|
176
|
+
return reduceV5(this.#patches, initialState);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Returns an iterator over the patch entries.
|
|
181
|
+
*
|
|
182
|
+
* This allows using the payload in for...of loops and spread syntax.
|
|
183
|
+
*
|
|
184
|
+
* @returns {Iterator<PatchEntry>} Iterator over patch entries
|
|
185
|
+
*/
|
|
186
|
+
[Symbol.iterator]() {
|
|
187
|
+
return this.#patches[Symbol.iterator]();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Returns the patch entry at the given index.
|
|
192
|
+
*
|
|
193
|
+
* Supports negative indices like Array.prototype.at() (e.g., -1 for last element).
|
|
194
|
+
*
|
|
195
|
+
* @param {number} index - The index (negative indices count from end)
|
|
196
|
+
* @returns {PatchEntry|undefined} The patch entry, or undefined if out of bounds
|
|
197
|
+
*/
|
|
198
|
+
at(index) {
|
|
199
|
+
return this.#patches.at(index);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Returns a new payload containing a slice of this payload's patches.
|
|
204
|
+
*
|
|
205
|
+
* This enables the slicing operation from Paper III: materializing
|
|
206
|
+
* only the causal cone for a target value.
|
|
207
|
+
*
|
|
208
|
+
* @param {number} [start=0] - Start index (inclusive)
|
|
209
|
+
* @param {number} [end] - End index (exclusive), defaults to length
|
|
210
|
+
* @returns {ProvenancePayload} A new payload with the sliced patches
|
|
211
|
+
*/
|
|
212
|
+
slice(start = 0, end = this.#patches.length) {
|
|
213
|
+
const sliced = this.#patches.slice(start, end);
|
|
214
|
+
return new ProvenancePayload(sliced);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Returns a JSON-serializable representation of this payload.
|
|
219
|
+
*
|
|
220
|
+
* The serialized form is an array of patch entries, suitable for
|
|
221
|
+
* transmission as a Boundary Transition Record (BTR).
|
|
222
|
+
*
|
|
223
|
+
* @returns {Array<PatchEntry>} Array of patch entries
|
|
224
|
+
*/
|
|
225
|
+
toJSON() {
|
|
226
|
+
return [...this.#patches];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Creates a ProvenancePayload from a JSON-serialized array.
|
|
231
|
+
*
|
|
232
|
+
* @param {Array<PatchEntry>} json - Array of patch entries
|
|
233
|
+
* @returns {ProvenancePayload} The deserialized payload
|
|
234
|
+
* @throws {TypeError} If json is not an array
|
|
235
|
+
*/
|
|
236
|
+
static fromJSON(json) {
|
|
237
|
+
return new ProvenancePayload(json);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export default ProvenancePayload;
|
|
242
|
+
export { ProvenancePayload };
|