@git-stunts/git-warp 12.2.1 → 12.4.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 (121) hide show
  1. package/README.md +5 -5
  2. package/bin/cli/commands/info.js +1 -5
  3. package/bin/cli/infrastructure.js +6 -9
  4. package/bin/cli/shared.js +8 -0
  5. package/bin/presenters/text.js +10 -3
  6. package/bin/warp-graph.js +6 -6
  7. package/package.json +1 -1
  8. package/src/domain/WarpGraph.js +5 -35
  9. package/src/domain/crdt/ORSet.js +3 -0
  10. package/src/domain/crdt/VersionVector.js +1 -1
  11. package/src/domain/entities/GraphNode.js +1 -6
  12. package/src/domain/errors/ForkError.js +1 -1
  13. package/src/domain/errors/IndexError.js +1 -1
  14. package/src/domain/errors/OperationAbortedError.js +1 -1
  15. package/src/domain/errors/PatchError.js +1 -1
  16. package/src/domain/errors/PersistenceError.js +45 -0
  17. package/src/domain/errors/QueryError.js +1 -1
  18. package/src/domain/errors/SchemaUnsupportedError.js +1 -1
  19. package/src/domain/errors/SyncError.js +1 -1
  20. package/src/domain/errors/TraversalError.js +1 -1
  21. package/src/domain/errors/TrustError.js +1 -1
  22. package/src/domain/errors/WormholeError.js +1 -1
  23. package/src/domain/errors/index.js +1 -0
  24. package/src/domain/services/AdjacencyNeighborProvider.js +1 -4
  25. package/src/domain/services/AnchorMessageCodec.js +1 -3
  26. package/src/domain/services/AuditMessageCodec.js +1 -5
  27. package/src/domain/services/AuditReceiptService.js +4 -18
  28. package/src/domain/services/AuditVerifierService.js +3 -7
  29. package/src/domain/services/BitmapIndexBuilder.js +6 -12
  30. package/src/domain/services/BitmapIndexReader.js +7 -20
  31. package/src/domain/services/BitmapNeighborProvider.js +1 -3
  32. package/src/domain/services/BoundaryTransitionRecord.js +7 -23
  33. package/src/domain/services/CheckpointMessageCodec.js +6 -6
  34. package/src/domain/services/CheckpointSerializerV5.js +8 -12
  35. package/src/domain/services/CheckpointService.js +28 -40
  36. package/src/domain/services/CommitDagTraversalService.js +1 -3
  37. package/src/domain/services/DagPathFinding.js +9 -59
  38. package/src/domain/services/DagTopology.js +4 -16
  39. package/src/domain/services/DagTraversal.js +7 -31
  40. package/src/domain/services/Frontier.js +4 -6
  41. package/src/domain/services/GitLogParser.js +1 -2
  42. package/src/domain/services/GraphTraversal.js +14 -114
  43. package/src/domain/services/HealthCheckService.js +3 -9
  44. package/src/domain/services/HookInstaller.js +2 -8
  45. package/src/domain/services/HttpSyncServer.js +24 -25
  46. package/src/domain/services/IncrementalIndexUpdater.js +4 -6
  47. package/src/domain/services/IndexRebuildService.js +6 -52
  48. package/src/domain/services/IndexStalenessChecker.js +2 -3
  49. package/src/domain/services/JoinReducer.js +200 -100
  50. package/src/domain/services/KeyCodec.js +48 -0
  51. package/src/domain/services/LogicalBitmapIndexBuilder.js +1 -2
  52. package/src/domain/services/LogicalIndexBuildService.js +2 -6
  53. package/src/domain/services/LogicalIndexReader.js +1 -2
  54. package/src/domain/services/LogicalTraversal.js +13 -64
  55. package/src/domain/services/MaterializedViewService.js +5 -19
  56. package/src/domain/services/MessageSchemaDetector.js +35 -5
  57. package/src/domain/services/MigrationService.js +1 -4
  58. package/src/domain/services/ObserverView.js +1 -7
  59. package/src/domain/services/OpNormalizer.js +79 -0
  60. package/src/domain/services/PatchBuilderV2.js +67 -38
  61. package/src/domain/services/PatchMessageCodec.js +1 -6
  62. package/src/domain/services/PropertyIndexBuilder.js +1 -2
  63. package/src/domain/services/PropertyIndexReader.js +1 -4
  64. package/src/domain/services/ProvenanceIndex.js +5 -7
  65. package/src/domain/services/ProvenancePayload.js +1 -1
  66. package/src/domain/services/QueryBuilder.js +3 -16
  67. package/src/domain/services/StateDiff.js +3 -9
  68. package/src/domain/services/StateSerializerV5.js +10 -10
  69. package/src/domain/services/StreamingBitmapIndexBuilder.js +13 -41
  70. package/src/domain/services/SyncAuthService.js +8 -32
  71. package/src/domain/services/SyncController.js +5 -25
  72. package/src/domain/services/SyncProtocol.js +10 -13
  73. package/src/domain/services/SyncTrustGate.js +4 -9
  74. package/src/domain/services/TemporalQuery.js +9 -27
  75. package/src/domain/services/TranslationCost.js +2 -8
  76. package/src/domain/services/WarpMessageCodec.js +2 -0
  77. package/src/domain/services/WarpStateIndexBuilder.js +2 -4
  78. package/src/domain/services/WormholeService.js +9 -25
  79. package/src/domain/trust/TrustCrypto.js +9 -10
  80. package/src/domain/trust/TrustEvaluator.js +1 -8
  81. package/src/domain/trust/TrustRecordService.js +5 -10
  82. package/src/domain/types/TickReceipt.js +9 -11
  83. package/src/domain/types/WarpTypes.js +1 -5
  84. package/src/domain/types/WarpTypesV2.js +78 -13
  85. package/src/domain/utils/CachedValue.js +1 -4
  86. package/src/domain/utils/MinHeap.js +3 -3
  87. package/src/domain/utils/RefLayout.js +26 -0
  88. package/src/domain/utils/WriterId.js +2 -7
  89. package/src/domain/utils/canonicalCbor.js +1 -1
  90. package/src/domain/utils/defaultClock.js +1 -0
  91. package/src/domain/utils/defaultCodec.js +1 -1
  92. package/src/domain/utils/parseCursorBlob.js +4 -4
  93. package/src/domain/warp/PatchSession.js +3 -8
  94. package/src/domain/warp/Writer.js +9 -12
  95. package/src/domain/warp/_wire.js +2 -2
  96. package/src/domain/warp/_wiredMethods.d.ts +5 -7
  97. package/src/domain/warp/checkpoint.methods.js +1 -1
  98. package/src/domain/warp/fork.methods.js +2 -6
  99. package/src/domain/warp/materializeAdvanced.methods.js +3 -3
  100. package/src/domain/warp/patch.methods.js +8 -8
  101. package/src/domain/warp/provenance.methods.js +5 -5
  102. package/src/domain/warp/query.methods.js +9 -18
  103. package/src/domain/warp/subscribe.methods.js +2 -8
  104. package/src/globals.d.ts +7 -0
  105. package/src/infrastructure/adapters/BunHttpAdapter.js +14 -18
  106. package/src/infrastructure/adapters/ConsoleLogger.js +2 -9
  107. package/src/infrastructure/adapters/DenoHttpAdapter.js +15 -15
  108. package/src/infrastructure/adapters/GitGraphAdapter.js +234 -58
  109. package/src/infrastructure/adapters/InMemoryGraphAdapter.js +9 -2
  110. package/src/infrastructure/adapters/NodeHttpAdapter.js +14 -14
  111. package/src/infrastructure/adapters/WebCryptoAdapter.js +1 -2
  112. package/src/ports/BlobPort.js +2 -2
  113. package/src/ports/HttpServerPort.js +24 -2
  114. package/src/ports/RefPort.js +2 -1
  115. package/src/visualization/renderers/ascii/box.js +1 -1
  116. package/src/visualization/renderers/ascii/check.js +1 -5
  117. package/src/visualization/renderers/ascii/history.js +1 -6
  118. package/src/visualization/renderers/ascii/path.js +4 -22
  119. package/src/visualization/renderers/ascii/progress.js +1 -4
  120. package/src/visualization/renderers/ascii/seek.js +1 -5
  121. package/src/visualization/renderers/ascii/table.js +1 -3
@@ -86,12 +86,7 @@ function extractNodeSnapshot(state, nodeId) {
86
86
  /**
87
87
  * Evaluates checkpoint boundary semantics for `always()`.
88
88
  *
89
- * @param {Object} params
90
- * @param {import('./JoinReducer.js').WarpStateV5} params.state
91
- * @param {string} params.nodeId
92
- * @param {Function} params.predicate
93
- * @param {number|null} params.checkpointMaxLamport
94
- * @param {number} params.since
89
+ * @param {{ state: import('./JoinReducer.js').WarpStateV5, nodeId: string, predicate: (snapshot: {id: string, exists: boolean, props: Record<string, unknown>}) => boolean, checkpointMaxLamport: number|null, since: number }} params
95
90
  * @returns {{ nodeEverExisted: boolean, shouldReturn: boolean, returnValue: boolean }}
96
91
  * @private
97
92
  */
@@ -118,12 +113,7 @@ function evaluateAlwaysCheckpointBoundary({
118
113
  /**
119
114
  * Evaluates checkpoint boundary semantics for `eventually()`.
120
115
  *
121
- * @param {Object} params
122
- * @param {import('./JoinReducer.js').WarpStateV5} params.state
123
- * @param {string} params.nodeId
124
- * @param {Function} params.predicate
125
- * @param {number|null} params.checkpointMaxLamport
126
- * @param {number} params.since
116
+ * @param {{ state: import('./JoinReducer.js').WarpStateV5, nodeId: string, predicate: (snapshot: {id: string, exists: boolean, props: Record<string, unknown>}) => boolean, checkpointMaxLamport: number|null, since: number }} params
127
117
  * @returns {boolean}
128
118
  * @private
129
119
  */
@@ -149,16 +139,12 @@ function evaluateEventuallyCheckpointBoundary({
149
139
  */
150
140
  export class TemporalQuery {
151
141
  /**
152
- * @param {Object} options
153
- * @param {Function} options.loadAllPatches - Async function that returns
154
- * all patches as Array<{ patch, sha }> in causal order.
155
- * @param {Function} [options.loadCheckpoint] - Async function returning
156
- * { state: WarpStateV5, maxLamport: number } or null.
142
+ * @param {{ loadAllPatches: () => Promise<Array<{patch: import('../types/WarpTypesV2.js').PatchV2, sha: string}>>, loadCheckpoint?: () => Promise<{state: import('./JoinReducer.js').WarpStateV5, maxLamport: number}|null> }} options
157
143
  */
158
144
  constructor({ loadAllPatches, loadCheckpoint }) {
159
- /** @type {Function} */
145
+ /** @type {() => Promise<Array<{patch: import('../types/WarpTypesV2.js').PatchV2, sha: string}>>} */
160
146
  this._loadAllPatches = loadAllPatches;
161
- /** @type {Function|null} */
147
+ /** @type {(() => Promise<{state: import('./JoinReducer.js').WarpStateV5, maxLamport: number}|null>)|null} */
162
148
  this._loadCheckpoint = loadCheckpoint || null;
163
149
  }
164
150
 
@@ -172,11 +158,9 @@ export class TemporalQuery {
172
158
  * Returns false if the node never existed in the range.
173
159
  *
174
160
  * @param {string} nodeId - The node ID to evaluate
175
- * @param {Function} predicate - Predicate receiving node snapshot
161
+ * @param {(snapshot: {id: string, exists: boolean, props: Record<string, unknown>}) => boolean} predicate - Predicate receiving node snapshot
176
162
  * `{ id, exists, props }`. Should return boolean.
177
- * @param {Object} [options={}] - Options
178
- * @param {number} [options.since=0] - Minimum Lamport tick (inclusive).
179
- * Only patches with lamport >= since are considered.
163
+ * @param {{ since?: number }} [options={}] - Options
180
164
  * @returns {Promise<boolean>} True if predicate held at every tick
181
165
  *
182
166
  * @example
@@ -232,11 +216,9 @@ export class TemporalQuery {
232
216
  * soon as the predicate returns true at any tick.
233
217
  *
234
218
  * @param {string} nodeId - The node ID to evaluate
235
- * @param {Function} predicate - Predicate receiving node snapshot
219
+ * @param {(snapshot: {id: string, exists: boolean, props: Record<string, unknown>}) => boolean} predicate - Predicate receiving node snapshot
236
220
  * `{ id, exists, props }`. Should return boolean.
237
- * @param {Object} [options={}] - Options
238
- * @param {number} [options.since=0] - Minimum Lamport tick (inclusive).
239
- * Only patches with lamport >= since are considered.
221
+ * @param {{ since?: number }} [options={}] - Options
240
222
  * @returns {Promise<boolean>} True if predicate held at any tick
241
223
  *
242
224
  * @example
@@ -169,14 +169,8 @@ function computePropLoss(state, { nodesA, nodesBSet, configA, configB }) {
169
169
  * The cost measures how much information is lost when translating from
170
170
  * A's view to B's view. It is asymmetric: cost(A->B) != cost(B->A) in general.
171
171
  *
172
- * @param {Object} configA - Observer configuration for A
173
- * @param {string|string[]} configA.match - Glob pattern(s) for visible nodes
174
- * @param {string[]} [configA.expose] - Property keys to include
175
- * @param {string[]} [configA.redact] - Property keys to exclude
176
- * @param {Object} configB - Observer configuration for B
177
- * @param {string|string[]} configB.match - Glob pattern(s) for visible nodes
178
- * @param {string[]} [configB.expose] - Property keys to include
179
- * @param {string[]} [configB.redact] - Property keys to exclude
172
+ * @param {{ match: string|string[], expose?: string[], redact?: string[] }} configA - Observer configuration for A
173
+ * @param {{ match: string|string[], expose?: string[], redact?: string[] }} configB - Observer configuration for B
180
174
  * @param {WarpStateV5} state - WarpStateV5 materialized state
181
175
  * @returns {{ cost: number, breakdown: { nodeLoss: number, edgeLoss: number, propLoss: number } }}
182
176
  */
@@ -29,4 +29,6 @@ export {
29
29
  assertOpsCompatible,
30
30
  SCHEMA_V2,
31
31
  SCHEMA_V3,
32
+ PATCH_SCHEMA_V2,
33
+ PATCH_SCHEMA_V3,
32
34
  } from './MessageSchemaDetector.js';
@@ -30,8 +30,7 @@ import { decodeEdgeKey } from './KeyCodec.js';
30
30
  export default class WarpStateIndexBuilder {
31
31
  /**
32
32
  * Creates a new WarpStateIndexBuilder.
33
- * @param {Object} [options] - Configuration
34
- * @param {import('../../ports/CryptoPort.js').default} [options.crypto] - CryptoPort for shard checksums
33
+ * @param {{ crypto?: import('../../ports/CryptoPort.js').default }} [options] - Configuration
35
34
  */
36
35
  constructor({ crypto } = {}) {
37
36
  /** @type {BitmapIndexBuilder} */
@@ -109,8 +108,7 @@ export default class WarpStateIndexBuilder {
109
108
  * Convenience function to build and serialize a WARP state index.
110
109
  *
111
110
  * @param {import('./JoinReducer.js').WarpStateV5} state - The materialized state
112
- * @param {Object} [options] - Configuration
113
- * @param {import('../../ports/CryptoPort.js').default} [options.crypto] - CryptoPort for shard checksums
111
+ * @param {{ crypto?: import('../../ports/CryptoPort.js').default }} [options] - Configuration
114
112
  * @returns {Promise<{tree: Record<string, Buffer>, stats: {nodes: number, edges: number}}>} Serialized index and stats
115
113
  *
116
114
  * @example
@@ -61,13 +61,8 @@ async function verifyShaExists(persistence, sha, paramName) {
61
61
 
62
62
  /**
63
63
  * Processes a single commit in the wormhole chain.
64
- * @param {Object} opts - Options
65
- * @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default} opts.persistence - Git persistence adapter
66
- * @param {string} opts.sha - The commit SHA
67
- * @param {string} opts.graphName - Expected graph name
68
- * @param {string|null} opts.expectedWriter - Expected writer ID (null for first commit)
69
- * @param {import('../../ports/CodecPort.js').default} [opts.codec] - Codec for deserialization
70
- * @returns {Promise<{patch: Object, sha: string, writerId: string, parentSha: string|null}>}
64
+ * @param {{ persistence: import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default, sha: string, graphName: string, expectedWriter: string|null, codec?: import('../../ports/CodecPort.js').default }} opts - Options
65
+ * @returns {Promise<{patch: import('../types/WarpTypesV2.js').PatchV2, sha: string, writerId: string, parentSha: string|null}>}
71
66
  * @throws {WormholeError} On validation errors
72
67
  * @private
73
68
  */
@@ -101,7 +96,7 @@ async function processCommit({ persistence, sha, graphName, expectedWriter, code
101
96
  }
102
97
 
103
98
  const patchBuffer = await persistence.readBlob(patchMeta.patchOid);
104
- const patch = /** @type {Object} */ (codec.decode(patchBuffer));
99
+ const patch = /** @type {import('../types/WarpTypesV2.js').PatchV2} */ (codec.decode(patchBuffer));
105
100
 
106
101
  return {
107
102
  patch,
@@ -135,12 +130,7 @@ async function processCommit({ persistence, sha, graphName, expectedWriter, code
135
130
  * must be an ancestor of `toSha` in the writer's patch chain. Both endpoints
136
131
  * are inclusive in the wormhole.
137
132
  *
138
- * @param {Object} options - Wormhole creation options
139
- * @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default} options.persistence - Git persistence adapter
140
- * @param {string} options.graphName - Name of the graph
141
- * @param {string} options.fromSha - SHA of the first (oldest) patch commit
142
- * @param {string} options.toSha - SHA of the last (newest) patch commit
143
- * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
133
+ * @param {{ persistence: import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default, graphName: string, fromSha: string, toSha: string, codec?: import('../../ports/CodecPort.js').default }} options - Wormhole creation options
144
134
  * @returns {Promise<WormholeEdge>} The created wormhole
145
135
  * @throws {WormholeError} If fromSha or toSha doesn't exist (E_WORMHOLE_SHA_NOT_FOUND)
146
136
  * @throws {WormholeError} If fromSha is not an ancestor of toSha (E_WORMHOLE_INVALID_RANGE)
@@ -171,13 +161,8 @@ export async function createWormhole({ persistence, graphName, fromSha, toSha, c
171
161
  * Walks the parent chain from toSha towards fromSha, collecting and
172
162
  * validating each commit along the way.
173
163
  *
174
- * @param {Object} options
175
- * @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default} options.persistence - Git persistence adapter
176
- * @param {string} options.graphName - Expected graph name
177
- * @param {string} options.fromSha - SHA of the first (oldest) patch commit
178
- * @param {string} options.toSha - SHA of the last (newest) patch commit
179
- * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
180
- * @returns {Promise<Array<{patch: Object, sha: string, writerId: string}>>} Patches in newest-first order
164
+ * @param {{ persistence: import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default, graphName: string, fromSha: string, toSha: string, codec?: import('../../ports/CodecPort.js').default }} options
165
+ * @returns {Promise<Array<{patch: import('../types/WarpTypesV2.js').PatchV2, sha: string, writerId: string}>>} Patches in newest-first order
181
166
  * @throws {WormholeError} If fromSha is not an ancestor of toSha or range is empty
182
167
  * @private
183
168
  */
@@ -232,8 +217,7 @@ async function collectPatchRange({ persistence, graphName, fromSha, toSha, codec
232
217
  *
233
218
  * @param {WormholeEdge} first - The earlier (older) wormhole
234
219
  * @param {WormholeEdge} second - The later (newer) wormhole
235
- * @param {Object} [options] - Composition options
236
- * @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default} [options.persistence] - Git persistence adapter (for validation)
220
+ * @param {{ persistence?: import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default }} [options] - Composition options
237
221
  * @returns {Promise<WormholeEdge>} The composed wormhole
238
222
  * @throws {WormholeError} If wormholes are from different writers (E_WORMHOLE_MULTI_WRITER)
239
223
  * @throws {WormholeError} If wormholes are not consecutive (E_WORMHOLE_INVALID_RANGE)
@@ -294,7 +278,7 @@ export function replayWormhole(wormhole, initialState) {
294
278
  * Serializes a wormhole to a JSON-serializable object.
295
279
  *
296
280
  * @param {WormholeEdge} wormhole - The wormhole to serialize
297
- * @returns {Object} JSON-serializable representation
281
+ * @returns {Record<string, unknown>} JSON-serializable representation
298
282
  */
299
283
  export function serializeWormhole(wormhole) {
300
284
  return {
@@ -309,7 +293,7 @@ export function serializeWormhole(wormhole) {
309
293
  /**
310
294
  * Deserializes a wormhole from a JSON object.
311
295
  *
312
- * @param {Object} json - The JSON object to deserialize
296
+ * @param {Record<string, unknown>} json - The JSON object to deserialize
313
297
  * @returns {WormholeEdge} The deserialized wormhole
314
298
  * @throws {WormholeError} If the JSON structure is invalid
315
299
  */
@@ -16,6 +16,13 @@ export const SUPPORTED_ALGORITHMS = new Set(['ed25519']);
16
16
 
17
17
  const ED25519_PUBLIC_KEY_LENGTH = 32;
18
18
 
19
+ /**
20
+ * DER-encoded SPKI prefix for Ed25519 public keys (RFC 8410, Section 4).
21
+ * Prepend to a 32-byte raw key to form a valid SPKI structure for `createPublicKey()`.
22
+ * @see https://www.rfc-editor.org/rfc/rfc8410#section-4
23
+ */
24
+ const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
25
+
19
26
  /**
20
27
  * Decodes a base64-encoded Ed25519 public key and validates its length.
21
28
  *
@@ -56,11 +63,7 @@ function decodePublicKey(base64) {
56
63
  /**
57
64
  * Verifies an Ed25519 signature against a payload.
58
65
  *
59
- * @param {Object} params
60
- * @param {string} params.algorithm - Must be 'ed25519'
61
- * @param {string} params.publicKeyBase64 - Base64-encoded 32-byte public key
62
- * @param {string} params.signatureBase64 - Base64-encoded signature
63
- * @param {Buffer} params.payload - Bytes to verify
66
+ * @param {{ algorithm: string, publicKeyBase64: string, signatureBase64: string, payload: Buffer }} params
64
67
  * @returns {boolean} true if signature is valid
65
68
  * @throws {TrustError} E_TRUST_UNSUPPORTED_ALGORITHM for non-ed25519
66
69
  * @throws {TrustError} E_TRUST_INVALID_KEY for malformed public key
@@ -81,11 +84,7 @@ export function verifySignature({
81
84
  const raw = decodePublicKey(publicKeyBase64);
82
85
 
83
86
  const keyObject = createPublicKey({
84
- key: Buffer.concat([
85
- // DER prefix for Ed25519 public key (RFC 8410)
86
- Buffer.from('302a300506032b6570032100', 'hex'),
87
- raw,
88
- ]),
87
+ key: Buffer.concat([ED25519_SPKI_PREFIX, raw]),
89
88
  format: 'der',
90
89
  type: 'spki',
91
90
  });
@@ -21,14 +21,7 @@ import { deriveTrustVerdict } from './verdict.js';
21
21
  * @property {number} trustSchemaVersion
22
22
  * @property {string} mode
23
23
  * @property {string} trustVerdict
24
- * @property {Object} trust
25
- * @property {'configured'|'pinned'|'error'|'not_configured'} trust.status
26
- * @property {string} trust.source
27
- * @property {string|null} trust.sourceDetail
28
- * @property {string[]} trust.evaluatedWriters
29
- * @property {string[]} trust.untrustedWriters
30
- * @property {ReadonlyArray<{writerId: string, trusted: boolean, reasonCode: string, reason: string}>} trust.explanations
31
- * @property {Record<string, number> & {recordsScanned: number, activeKeys: number, revokedKeys: number, activeBindings: number, revokedBindings: number}} trust.evidenceSummary
24
+ * @property {{ status: 'configured'|'pinned'|'error'|'not_configured', source: string, sourceDetail: string|null, evaluatedWriters: string[], untrustedWriters: string[], explanations: ReadonlyArray<{writerId: string, trusted: boolean, reasonCode: string, reason: string}>, evidenceSummary: Record<string, number> & {recordsScanned: number, activeKeys: number, revokedKeys: number, activeBindings: number, revokedBindings: number} }} trust
32
25
  */
33
26
 
34
27
  /**
@@ -12,6 +12,7 @@
12
12
  import { buildTrustRecordRef } from '../utils/RefLayout.js';
13
13
  import { TrustRecordSchema } from './schemas.js';
14
14
  import { verifyRecordId } from './TrustCanonical.js';
15
+ import PersistenceError from '../errors/PersistenceError.js';
15
16
  import TrustError from '../errors/TrustError.js';
16
17
 
17
18
  /**
@@ -32,9 +33,7 @@ const MAX_CAS_ATTEMPTS = 3;
32
33
 
33
34
  export class TrustRecordService {
34
35
  /**
35
- * @param {Object} options
36
- * @param {import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default & import('../../ports/TreePort.js').default & import('../../ports/RefPort.js').default} options.persistence - GraphPersistencePort adapter
37
- * @param {import('../../ports/CodecPort.js').default} options.codec - CodecPort adapter (CBOR)
36
+ * @param {{ persistence: import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default & import('../../ports/TreePort.js').default & import('../../ports/RefPort.js').default, codec: import('../../ports/CodecPort.js').default }} options
38
37
  */
39
38
  constructor({ persistence, codec }) {
40
39
  this._persistence = persistence;
@@ -104,8 +103,7 @@ export class TrustRecordService {
104
103
  * Reads all trust records from the chain, oldest first.
105
104
  *
106
105
  * @param {string} graphName
107
- * @param {Object} [options]
108
- * @param {string} [options.tip] - Override tip commit (for pinned reads)
106
+ * @param {{ tip?: string }} [options]
109
107
  * @returns {Promise<ReadRecordsResult>}
110
108
  */
111
109
  async readRecords(graphName, options = {}) {
@@ -118,7 +116,7 @@ export class TrustRecordService {
118
116
  tip = await this._persistence.readRef(ref);
119
117
  } catch (err) {
120
118
  // Distinguish "ref not found" from operational error (J15)
121
- if (err instanceof Error && (err.message?.includes('not found') || err.message?.includes('does not exist'))) {
119
+ if (err instanceof PersistenceError && err.code === PersistenceError.E_REF_NOT_FOUND) {
122
120
  return { ok: true, records: [] };
123
121
  }
124
122
  return {
@@ -234,10 +232,7 @@ export class TrustRecordService {
234
232
  *
235
233
  * @param {string} graphName
236
234
  * @param {Record<string, unknown>} record - Complete signed trust record
237
- * @param {Object} [options]
238
- * @param {number} [options.maxRetries=3] - Maximum rebuild-and-retry attempts
239
- * @param {((record: Record<string, unknown>) => Promise<Record<string, unknown>>)|null} [options.resign] - Function to re-sign a rebuilt record (null for unsigned)
240
- * @param {boolean} [options.skipSignatureVerify=false] - Skip signature verification
235
+ * @param {{ maxRetries?: number, resign?: ((record: Record<string, unknown>) => Promise<Record<string, unknown>>)|null, skipSignatureVerify?: boolean }} [options]
241
236
  * @returns {Promise<{commitSha: string, ref: string, attempts: number}>}
242
237
  * @throws {TrustError} E_TRUST_CAS_EXHAUSTED if all retries fail
243
238
  */
@@ -25,6 +25,8 @@ export const OP_TYPES = Object.freeze([
25
25
  'EdgeAdd',
26
26
  'EdgeTombstone',
27
27
  'PropSet',
28
+ 'NodePropSet',
29
+ 'EdgePropSet',
28
30
  'BlobValue',
29
31
  ]);
30
32
 
@@ -80,9 +82,9 @@ function validateOp(op, index) {
80
82
  /**
81
83
  * Validates that an operation type is one of the allowed OP_TYPES.
82
84
  *
83
- * Valid operation types correspond to the six patch operations defined
84
- * in PatchBuilderV2: NodeAdd, NodeTombstone, EdgeAdd, EdgeTombstone,
85
- * PropSet, and BlobValue.
85
+ * Valid operation types correspond to the eight receipt operation types:
86
+ * NodeAdd, NodeTombstone, EdgeAdd, EdgeTombstone, PropSet, NodePropSet,
87
+ * EdgePropSet, and BlobValue.
86
88
  *
87
89
  * @param {unknown} value - The operation type to validate
88
90
  * @param {number} i - Index of the operation in the ops array (for error messages)
@@ -156,7 +158,7 @@ function validateOpResult(value, i) {
156
158
 
157
159
  /**
158
160
  * @typedef {Object} OpOutcome
159
- * @property {string} op - Operation type ('NodeAdd' | 'NodeTombstone' | 'EdgeAdd' | 'EdgeTombstone' | 'PropSet' | 'BlobValue')
161
+ * @property {string} op - Operation type ('NodeAdd' | 'NodeTombstone' | 'EdgeAdd' | 'EdgeTombstone' | 'PropSet' | 'NodePropSet' | 'EdgePropSet' | 'BlobValue')
160
162
  * @property {string} target - Node ID or edge key
161
163
  * @property {'applied' | 'superseded' | 'redundant'} result - Outcome of the operation
162
164
  * @property {string} [reason] - Human-readable explanation (e.g., "LWW: writer bob at lamport 43 wins")
@@ -173,11 +175,7 @@ function validateOpResult(value, i) {
173
175
  /**
174
176
  * Creates an immutable TickReceipt.
175
177
  *
176
- * @param {Object} params
177
- * @param {string} params.patchSha - SHA of the patch commit
178
- * @param {string} params.writer - Writer ID
179
- * @param {number} params.lamport - Lamport timestamp (non-negative integer)
180
- * @param {OpOutcome[]} params.ops - Per-operation outcome records
178
+ * @param {{ patchSha: string, writer: string, lamport: number, ops: OpOutcome[] }} params
181
179
  * @returns {Readonly<TickReceipt>} Frozen tick receipt
182
180
  * @throws {Error} If any parameter is invalid
183
181
  */
@@ -277,9 +275,9 @@ export function canonicalJson(receipt) {
277
275
  */
278
276
  function sortedReplacer(_key, value) {
279
277
  if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
280
- /** @type {{ [x: string]: * }} */
278
+ /** @type {{ [x: string]: unknown }} */
281
279
  const sorted = {};
282
- const obj = /** @type {{ [x: string]: * }} */ (value);
280
+ const obj = /** @type {{ [x: string]: unknown }} */ (value);
283
281
  for (const k of Object.keys(obj).sort()) {
284
282
  sorted[k] = obj[k];
285
283
  }
@@ -197,11 +197,7 @@ export function createPropSet(node, key, value) {
197
197
 
198
198
  /**
199
199
  * Creates an EventId
200
- * @param {Object} options - EventId options
201
- * @param {number} options.lamport - Lamport timestamp
202
- * @param {string} options.writerId - Writer ID
203
- * @param {string} options.patchSha - Patch SHA
204
- * @param {number} options.opIndex - Operation index within patch
200
+ * @param {{ lamport: number, writerId: string, patchSha: string, opIndex: number }} options - EventId options
205
201
  * @returns {EventId} EventId object
206
202
  */
207
203
  export function createEventId({ lamport, writerId, patchSha, opIndex }) {
@@ -74,18 +74,64 @@
74
74
  */
75
75
 
76
76
  /**
77
- * Property set operation - sets a property value on a node
78
- * Uses EventId for identification (derived from patch context)
77
+ * Property set operation - sets a property value on a node (raw/persisted form).
78
+ * Uses EventId for identification (derived from patch context).
79
+ *
80
+ * In raw patches, edge properties are also encoded as PropSet with the node
81
+ * field carrying a \x01-prefixed edge identity. See {@link OpV2NodePropSet}
82
+ * and {@link OpV2EdgePropSet} for the canonical (internal) representations.
83
+ *
79
84
  * @typedef {Object} OpV2PropSet
80
85
  * @property {'PropSet'} type - Operation type discriminator
86
+ * @property {NodeId} node - Node ID to set property on (may contain \x01 prefix for edge props)
87
+ * @property {string} key - Property key
88
+ * @property {unknown} value - Property value (any JSON-serializable type)
89
+ */
90
+
91
+ /**
92
+ * Canonical node property set operation (internal only — never persisted).
93
+ * @typedef {Object} OpV2NodePropSet
94
+ * @property {'NodePropSet'} type - Operation type discriminator
81
95
  * @property {NodeId} node - Node ID to set property on
82
96
  * @property {string} key - Property key
83
97
  * @property {unknown} value - Property value (any JSON-serializable type)
84
98
  */
85
99
 
86
100
  /**
87
- * Union of all v2 operation types
88
- * @typedef {OpV2NodeAdd | OpV2NodeRemove | OpV2EdgeAdd | OpV2EdgeRemove | OpV2PropSet} OpV2
101
+ * Canonical edge property set operation (internal only — never persisted).
102
+ * @typedef {Object} OpV2EdgePropSet
103
+ * @property {'EdgePropSet'} type - Operation type discriminator
104
+ * @property {NodeId} from - Source node ID
105
+ * @property {NodeId} to - Target node ID
106
+ * @property {string} label - Edge label
107
+ * @property {string} key - Property key
108
+ * @property {unknown} value - Property value (any JSON-serializable type)
109
+ */
110
+
111
+ /**
112
+ * Blob value reference operation.
113
+ * @typedef {Object} OpV2BlobValue
114
+ * @property {'BlobValue'} type - Operation type discriminator
115
+ * @property {string} node - Node ID the blob is attached to
116
+ * @property {string} oid - Blob object ID in the Git object store
117
+ */
118
+
119
+ /**
120
+ * Union of all raw (persisted) v2 operation types.
121
+ * @typedef {OpV2NodeAdd | OpV2NodeRemove | OpV2EdgeAdd | OpV2EdgeRemove | OpV2PropSet | OpV2BlobValue} RawOpV2
122
+ */
123
+
124
+ /**
125
+ * Union of all canonical (internal) v2 operation types.
126
+ * Reducers, provenance, receipts, and queries operate on canonical ops only.
127
+ * @typedef {OpV2NodeAdd | OpV2NodeRemove | OpV2EdgeAdd | OpV2EdgeRemove | OpV2NodePropSet | OpV2EdgePropSet | OpV2BlobValue} CanonicalOpV2
128
+ */
129
+
130
+ /**
131
+ * Union of all v2 operation types (raw + canonical).
132
+ * Used in patch containers that may hold either raw ops (from disk)
133
+ * or canonical ops (after normalization).
134
+ * @typedef {RawOpV2 | CanonicalOpV2} OpV2
89
135
  */
90
136
 
91
137
  // ============================================================================
@@ -153,7 +199,9 @@ export function createEdgeRemoveV2(from, to, label, observedDots) {
153
199
  }
154
200
 
155
201
  /**
156
- * Creates a PropSet operation (no dot - uses EventId)
202
+ * Creates a raw PropSet operation (no dot - uses EventId).
203
+ * This is the persisted form. For internal use, prefer
204
+ * {@link createNodePropSetV2} or {@link createEdgePropSetV2}.
157
205
  * @param {NodeId} node - Node ID to set property on
158
206
  * @param {string} key - Property key
159
207
  * @param {unknown} value - Property value (any JSON-serializable type)
@@ -163,20 +211,37 @@ export function createPropSetV2(node, key, value) {
163
211
  return { type: 'PropSet', node, key, value };
164
212
  }
165
213
 
214
+ /**
215
+ * Creates a canonical NodePropSet operation (internal only).
216
+ * @param {NodeId} node - Node ID to set property on
217
+ * @param {string} key - Property key
218
+ * @param {unknown} value - Property value (any JSON-serializable type)
219
+ * @returns {OpV2NodePropSet} NodePropSet operation
220
+ */
221
+ export function createNodePropSetV2(node, key, value) {
222
+ return { type: 'NodePropSet', node, key, value };
223
+ }
224
+
225
+ /**
226
+ * Creates a canonical EdgePropSet operation (internal only).
227
+ * @param {NodeId} from - Source node ID
228
+ * @param {NodeId} to - Target node ID
229
+ * @param {string} label - Edge label
230
+ * @param {string} key - Property key
231
+ * @param {unknown} value - Property value (any JSON-serializable type)
232
+ * @returns {OpV2EdgePropSet} EdgePropSet operation
233
+ */
234
+ export function createEdgePropSetV2(from, to, label, key, value) {
235
+ return { type: 'EdgePropSet', from, to, label, key, value };
236
+ }
237
+
166
238
  // ============================================================================
167
239
  // Factory Functions - Patch
168
240
  // ============================================================================
169
241
 
170
242
  /**
171
243
  * Creates a PatchV2
172
- * @param {Object} options - Patch options
173
- * @param {2|3} [options.schema=2] - Schema version (2 for node-only, 3 for edge properties)
174
- * @param {string} options.writer - Writer ID
175
- * @param {number} options.lamport - Lamport timestamp
176
- * @param {VersionVector} options.context - Writer's observed frontier
177
- * @param {OpV2[]} options.ops - Array of operations
178
- * @param {string[]} [options.reads] - Node/edge IDs read by this patch (for provenance tracking)
179
- * @param {string[]} [options.writes] - Node/edge IDs written by this patch (for provenance tracking)
244
+ * @param {{ schema?: 2|3, writer: string, lamport: number, context: VersionVector, ops: OpV2[], reads?: string[], writes?: string[] }} options - Patch options
180
245
  * @returns {PatchV2} PatchV2 object
181
246
  */
182
247
  export function createPatchV2({ schema = 2, writer, lamport, context, ops, reads, writes }) {
@@ -28,10 +28,7 @@ class CachedValue {
28
28
  /**
29
29
  * Creates a CachedValue instance.
30
30
  *
31
- * @param {Object} options
32
- * @param {import('../../ports/ClockPort.js').default} options.clock - Clock port for timing
33
- * @param {number} options.ttlMs - Time-to-live in milliseconds
34
- * @param {() => T | Promise<T>} options.compute - Function to compute the value when cache is stale
31
+ * @param {{ clock: import('../../ports/ClockPort.js').default, ttlMs: number, compute: () => T | Promise<T> }} options
35
32
  * @throws {Error} If ttlMs is not a positive number
36
33
  */
37
34
  constructor({ clock, ttlMs, compute }) {
@@ -9,9 +9,9 @@ class MinHeap {
9
9
  /**
10
10
  * Creates an empty MinHeap.
11
11
  *
12
- * @param {Object} [options] - Configuration options
13
- * @param {((a: T, b: T) => number)} [options.tieBreaker] - Comparator invoked when two
14
- * entries have equal priority. Negative return = a wins (comes out first).
12
+ * @param {{ tieBreaker?: (a: T, b: T) => number }} [options] - Configuration options.
13
+ * `tieBreaker`: comparator invoked when two entries have equal priority.
14
+ * Negative return = a wins (comes out first).
15
15
  * When omitted, equal-priority extraction order is unspecified (heap-natural).
16
16
  */
17
17
  constructor(options) {
@@ -45,6 +45,23 @@ const WRITER_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
45
45
  */
46
46
  const PATH_TRAVERSAL_PATTERN = /\.\./;
47
47
 
48
+ /**
49
+ * Ref-layout keywords that must not appear as any `/`-delimited segment
50
+ * of a graph name. Using one of these would create an ambiguous ref path
51
+ * (e.g. `refs/warp/writers/writers/alice`).
52
+ *
53
+ * @type {Set<string>}
54
+ */
55
+ export const RESERVED_GRAPH_NAME_SEGMENTS = new Set([
56
+ 'writers',
57
+ 'checkpoints',
58
+ 'coverage',
59
+ 'cursor',
60
+ 'audit',
61
+ 'trust',
62
+ 'seek-cache',
63
+ ]);
64
+
48
65
  // -----------------------------------------------------------------------------
49
66
  // Validators
50
67
  // -----------------------------------------------------------------------------
@@ -94,6 +111,15 @@ export function validateGraphName(name) {
94
111
  if (name.includes('\0')) {
95
112
  throw new Error(`Invalid graph name: contains null byte: ${name}`);
96
113
  }
114
+
115
+ const segments = name.split('/');
116
+ for (const seg of segments) {
117
+ if (RESERVED_GRAPH_NAME_SEGMENTS.has(seg)) {
118
+ throw new Error(
119
+ `Invalid graph name: segment '${seg}' is a reserved ref-layout keyword: ${name}`
120
+ );
121
+ }
122
+ }
97
123
  }
98
124
 
99
125
  /**
@@ -116,8 +116,7 @@ function crockfordBase32(bytes) {
116
116
  * Uses 128 bits of entropy (16 bytes) encoded as Crockford Base32.
117
117
  * The result is prefixed with `w_` for a total length of 28 characters.
118
118
  *
119
- * @param {Object} [options]
120
- * @param {(n: number) => Uint8Array} [options.randomBytes] - Custom RNG for testing
119
+ * @param {{ randomBytes?: (n: number) => Uint8Array }} [options] - Options with optional custom RNG for testing
121
120
  * @returns {string} A canonical writer ID (e.g., 'w_0123456789abcdefghjkmnpqrs')
122
121
  * @throws {WriterIdError} If RNG is unavailable or returns wrong shape
123
122
  *
@@ -148,11 +147,7 @@ export function generateWriterId({ randomBytes } = {}) {
148
147
  * 2. Load from git config key `warp.writerId.<graphName>`
149
148
  * 3. If missing or invalid, generate new canonical ID, persist, and return
150
149
  *
151
- * @param {Object} args
152
- * @param {string} args.graphName - The graph name
153
- * @param {string|undefined} args.explicitWriterId - Optional explicit writer ID
154
- * @param {(key: string) => Promise<string|null>} args.configGet - Function to read git config
155
- * @param {(key: string, value: string) => Promise<void>} args.configSet - Function to write git config
150
+ * @param {{ graphName: string, explicitWriterId: string|null|undefined, configGet: (key: string) => Promise<string|null>, configSet: (key: string, value: string) => Promise<void> }} args
156
151
  * @returns {Promise<string>} The resolved writer ID
157
152
  * @throws {WriterIdError} If config operations fail
158
153
  *
@@ -28,7 +28,7 @@ export function encodeCanonicalCbor(value) {
28
28
  /**
29
29
  * Decodes CBOR bytes to a value.
30
30
  *
31
- * @param {Buffer|Uint8Array} buffer - CBOR bytes
31
+ * @param {Uint8Array} buffer - CBOR bytes
32
32
  * @returns {unknown} Decoded value
33
33
  */
34
34
  export function decodeCanonicalCbor(buffer) {
@@ -13,6 +13,7 @@ const defaultClock = {
13
13
  return performance.now();
14
14
  },
15
15
  timestamp() {
16
+ // eslint-disable-next-line no-restricted-syntax -- ClockPort implementation
16
17
  return new Date().toISOString();
17
18
  },
18
19
  };
@@ -34,7 +34,7 @@ function sortKeys(value) {
34
34
  }
35
35
  return sorted;
36
36
  }
37
- if (typeof value === 'object' && (/** @type {Object} */ (value).constructor === Object || /** @type {Object} */ (value).constructor === undefined)) {
37
+ if (typeof value === 'object' && (/** @type {Record<string, unknown>} */ (value).constructor === Object || /** @type {Record<string, unknown>} */ (value).constructor === undefined)) {
38
38
  /** @type {Record<string, unknown>} */
39
39
  const sorted = {};
40
40
  for (const key of Object.keys(value).sort()) {