@git-stunts/git-warp 11.2.1 → 11.5.0

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 (114) hide show
  1. package/README.md +24 -1
  2. package/bin/cli/commands/check.js +2 -2
  3. package/bin/cli/commands/doctor/checks.js +12 -12
  4. package/bin/cli/commands/doctor/index.js +2 -2
  5. package/bin/cli/commands/doctor/types.js +1 -1
  6. package/bin/cli/commands/history.js +12 -5
  7. package/bin/cli/commands/install-hooks.js +5 -5
  8. package/bin/cli/commands/materialize.js +2 -2
  9. package/bin/cli/commands/patch.js +142 -0
  10. package/bin/cli/commands/path.js +4 -4
  11. package/bin/cli/commands/query.js +54 -13
  12. package/bin/cli/commands/registry.js +4 -0
  13. package/bin/cli/commands/seek.js +17 -11
  14. package/bin/cli/commands/tree.js +230 -0
  15. package/bin/cli/commands/trust.js +3 -3
  16. package/bin/cli/commands/verify-audit.js +8 -7
  17. package/bin/cli/commands/view.js +6 -5
  18. package/bin/cli/infrastructure.js +26 -12
  19. package/bin/cli/shared.js +2 -2
  20. package/bin/cli/types.js +19 -8
  21. package/bin/presenters/index.js +35 -9
  22. package/bin/presenters/json.js +14 -12
  23. package/bin/presenters/text.js +155 -33
  24. package/index.d.ts +118 -22
  25. package/index.js +2 -0
  26. package/package.json +5 -3
  27. package/src/domain/WarpGraph.js +4 -1
  28. package/src/domain/crdt/ORSet.js +8 -8
  29. package/src/domain/errors/EmptyMessageError.js +2 -2
  30. package/src/domain/errors/ForkError.js +1 -1
  31. package/src/domain/errors/IndexError.js +1 -1
  32. package/src/domain/errors/OperationAbortedError.js +1 -1
  33. package/src/domain/errors/QueryError.js +1 -1
  34. package/src/domain/errors/SchemaUnsupportedError.js +1 -1
  35. package/src/domain/errors/ShardCorruptionError.js +2 -2
  36. package/src/domain/errors/ShardLoadError.js +2 -2
  37. package/src/domain/errors/ShardValidationError.js +4 -4
  38. package/src/domain/errors/StorageError.js +2 -2
  39. package/src/domain/errors/SyncError.js +1 -1
  40. package/src/domain/errors/TraversalError.js +1 -1
  41. package/src/domain/errors/TrustError.js +1 -1
  42. package/src/domain/errors/WarpError.js +2 -2
  43. package/src/domain/errors/WormholeError.js +1 -1
  44. package/src/domain/services/AuditReceiptService.js +6 -6
  45. package/src/domain/services/AuditVerifierService.js +52 -38
  46. package/src/domain/services/BitmapIndexBuilder.js +3 -3
  47. package/src/domain/services/BitmapIndexReader.js +28 -19
  48. package/src/domain/services/BoundaryTransitionRecord.js +18 -17
  49. package/src/domain/services/CheckpointSerializerV5.js +17 -16
  50. package/src/domain/services/CheckpointService.js +22 -3
  51. package/src/domain/services/CommitDagTraversalService.js +13 -13
  52. package/src/domain/services/DagPathFinding.js +7 -7
  53. package/src/domain/services/DagTopology.js +1 -1
  54. package/src/domain/services/DagTraversal.js +1 -1
  55. package/src/domain/services/HealthCheckService.js +1 -1
  56. package/src/domain/services/HookInstaller.js +1 -1
  57. package/src/domain/services/HttpSyncServer.js +92 -41
  58. package/src/domain/services/IndexRebuildService.js +7 -7
  59. package/src/domain/services/IndexStalenessChecker.js +4 -3
  60. package/src/domain/services/JoinReducer.js +26 -11
  61. package/src/domain/services/KeyCodec.js +7 -0
  62. package/src/domain/services/LogicalTraversal.js +1 -1
  63. package/src/domain/services/MessageCodecInternal.js +1 -1
  64. package/src/domain/services/MigrationService.js +1 -1
  65. package/src/domain/services/ObserverView.js +8 -8
  66. package/src/domain/services/PatchBuilderV2.js +96 -30
  67. package/src/domain/services/ProvenanceIndex.js +1 -1
  68. package/src/domain/services/ProvenancePayload.js +1 -1
  69. package/src/domain/services/QueryBuilder.js +3 -3
  70. package/src/domain/services/StateDiff.js +14 -11
  71. package/src/domain/services/StateSerializerV5.js +2 -2
  72. package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
  73. package/src/domain/services/SyncAuthService.js +3 -2
  74. package/src/domain/services/SyncProtocol.js +25 -11
  75. package/src/domain/services/TemporalQuery.js +9 -6
  76. package/src/domain/services/TranslationCost.js +7 -5
  77. package/src/domain/services/WormholeService.js +16 -7
  78. package/src/domain/trust/TrustCanonical.js +3 -3
  79. package/src/domain/trust/TrustEvaluator.js +18 -3
  80. package/src/domain/trust/TrustRecordService.js +30 -23
  81. package/src/domain/trust/TrustStateBuilder.js +21 -8
  82. package/src/domain/trust/canonical.js +6 -6
  83. package/src/domain/types/TickReceipt.js +1 -1
  84. package/src/domain/types/WarpErrors.js +45 -0
  85. package/src/domain/types/WarpOptions.js +29 -0
  86. package/src/domain/types/WarpPersistence.js +41 -0
  87. package/src/domain/types/WarpTypes.js +2 -2
  88. package/src/domain/types/WarpTypesV2.js +2 -2
  89. package/src/domain/utils/MinHeap.js +6 -5
  90. package/src/domain/utils/canonicalStringify.js +5 -4
  91. package/src/domain/utils/roaring.js +31 -5
  92. package/src/domain/warp/PatchSession.js +40 -18
  93. package/src/domain/warp/_wiredMethods.d.ts +199 -45
  94. package/src/domain/warp/checkpoint.methods.js +5 -1
  95. package/src/domain/warp/fork.methods.js +2 -2
  96. package/src/domain/warp/materialize.methods.js +55 -5
  97. package/src/domain/warp/materializeAdvanced.methods.js +15 -4
  98. package/src/domain/warp/patch.methods.js +54 -29
  99. package/src/domain/warp/provenance.methods.js +5 -3
  100. package/src/domain/warp/query.methods.js +89 -6
  101. package/src/domain/warp/sync.methods.js +16 -11
  102. package/src/globals.d.ts +64 -0
  103. package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
  104. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
  105. package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
  106. package/src/infrastructure/adapters/GitGraphAdapter.js +18 -13
  107. package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
  108. package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
  109. package/src/visualization/layouts/converters.js +2 -2
  110. package/src/visualization/layouts/elkAdapter.js +1 -1
  111. package/src/visualization/layouts/elkLayout.js +10 -7
  112. package/src/visualization/layouts/index.js +1 -1
  113. package/src/visualization/renderers/ascii/seek.js +16 -6
  114. package/src/visualization/renderers/svg/index.js +1 -1
@@ -29,7 +29,7 @@ export {
29
29
  * @typedef {Object} WarpStateV5
30
30
  * @property {import('../crdt/ORSet.js').ORSet} nodeAlive - ORSet of alive nodes
31
31
  * @property {import('../crdt/ORSet.js').ORSet} edgeAlive - ORSet of alive edges
32
- * @property {Map<string, import('../crdt/LWW.js').LWWRegister<*>>} prop - Properties with LWW
32
+ * @property {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} prop - Properties with LWW
33
33
  * @property {import('../crdt/VersionVector.js').VersionVector} observedFrontier - Observed version vector
34
34
  * @property {Map<string, import('../utils/EventId.js').EventId>} edgeBirthEvent - EdgeKey → EventId of most recent EdgeAdd (for clean-slate prop visibility)
35
35
  */
@@ -81,7 +81,7 @@ export function createEmptyStateV5() {
81
81
  * @param {string} [op.to] - Target node ID (for EdgeAdd, EdgeRemove)
82
82
  * @param {string} [op.label] - Edge label (for EdgeAdd, EdgeRemove)
83
83
  * @param {string} [op.key] - Property key (for PropSet)
84
- * @param {*} [op.value] - Property value (for PropSet)
84
+ * @param {unknown} [op.value] - Property value (for PropSet)
85
85
  * @param {import('../utils/EventId.js').EventId} eventId - Event ID for causality tracking
86
86
  * @returns {void}
87
87
  */
@@ -114,7 +114,7 @@ export function applyOpV2(state, op, eventId) {
114
114
  // Uses EventId-based LWW, same as v4
115
115
  const key = encodePropKey(/** @type {string} */ (op.node), /** @type {string} */ (op.key));
116
116
  const current = state.prop.get(key);
117
- state.prop.set(key, /** @type {import('../crdt/LWW.js').LWWRegister<*>} */ (lwwMax(current, lwwSet(eventId, op.value))));
117
+ state.prop.set(key, /** @type {import('../crdt/LWW.js').LWWRegister<unknown>} */ (lwwMax(current, lwwSet(eventId, op.value))));
118
118
  break;
119
119
  }
120
120
  default:
@@ -290,11 +290,11 @@ function edgeRemoveOutcome(orset, op) {
290
290
  * - `superseded`: An existing value with higher EventId wins
291
291
  * - `redundant`: Exact same write (identical EventId)
292
292
  *
293
- * @param {Map<string, import('../crdt/LWW.js').LWWRegister<*>>} propMap - The properties map keyed by encoded prop keys
293
+ * @param {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} propMap - The properties map keyed by encoded prop keys
294
294
  * @param {Object} op - The PropSet operation
295
295
  * @param {string} op.node - Node ID owning the property
296
296
  * @param {string} op.key - Property key/name
297
- * @param {*} op.value - Property value to set
297
+ * @param {unknown} op.value - Property value to set
298
298
  * @param {import('../utils/EventId.js').EventId} eventId - The event ID for this operation, used for LWW comparison
299
299
  * @returns {{target: string, result: 'applied'|'superseded'|'redundant', reason?: string}}
300
300
  * Outcome with encoded prop key as target; includes reason when superseded
@@ -328,6 +328,19 @@ function propSetOutcome(propMap, op, eventId) {
328
328
  return { target, result: 'redundant' };
329
329
  }
330
330
 
331
+ /**
332
+ * Folds a patch's own dot into the observed frontier.
333
+ * @param {Map<string, number>} frontier
334
+ * @param {string} writer
335
+ * @param {number} lamport
336
+ */
337
+ function foldPatchDot(frontier, writer, lamport) {
338
+ const current = frontier.get(writer) || 0;
339
+ if (lamport > current) {
340
+ frontier.set(writer, lamport);
341
+ }
342
+ }
343
+
331
344
  /**
332
345
  * Joins a patch into state, applying all operations in order.
333
346
  *
@@ -347,7 +360,7 @@ function propSetOutcome(propMap, op, eventId) {
347
360
  * @param {Object} patch - The patch to apply
348
361
  * @param {string} patch.writer - Writer ID who created this patch
349
362
  * @param {number} patch.lamport - Lamport timestamp of this patch
350
- * @param {Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?: *, oid?: string}>} patch.ops - Array of operations to apply
363
+ * @param {Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?: unknown, oid?: string}>} patch.ops - Array of operations to apply
351
364
  * @param {Map<string, number>|{[x: string]: number}} patch.context - Version vector context (Map or serialized form)
352
365
  * @param {string} patchSha - The Git SHA of the patch commit (used for EventId creation)
353
366
  * @param {boolean} [collectReceipts=false] - When true, computes and returns receipt data
@@ -366,6 +379,7 @@ export function join(state, patch, patchSha, collectReceipts) {
366
379
  ? patch.context
367
380
  : vvDeserialize(patch.context);
368
381
  state.observedFrontier = vvMerge(state.observedFrontier, contextVV);
382
+ foldPatchDot(state.observedFrontier, patch.writer, patch.lamport);
369
383
  return state;
370
384
  }
371
385
 
@@ -423,6 +437,7 @@ export function join(state, patch, patchSha, collectReceipts) {
423
437
  ? patch.context
424
438
  : vvDeserialize(patch.context);
425
439
  state.observedFrontier = vvMerge(state.observedFrontier, contextVV);
440
+ foldPatchDot(state.observedFrontier, patch.writer, patch.lamport);
426
441
 
427
442
  const receipt = createTickReceipt({
428
443
  patchSha,
@@ -470,16 +485,16 @@ export function joinStates(a, b) {
470
485
  *
471
486
  * This is a pure function that does not mutate its inputs.
472
487
  *
473
- * @param {Map<string, import('../crdt/LWW.js').LWWRegister<*>>} a - First property map
474
- * @param {Map<string, import('../crdt/LWW.js').LWWRegister<*>>} b - Second property map
475
- * @returns {Map<string, import('../crdt/LWW.js').LWWRegister<*>>} New map containing merged properties
488
+ * @param {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} a - First property map
489
+ * @param {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} b - Second property map
490
+ * @returns {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} New map containing merged properties
476
491
  */
477
492
  function mergeProps(a, b) {
478
493
  const result = new Map(a);
479
494
 
480
495
  for (const [key, regB] of b) {
481
496
  const regA = result.get(key);
482
- result.set(key, /** @type {import('../crdt/LWW.js').LWWRegister<*>} */ (lwwMax(regA, regB)));
497
+ result.set(key, /** @type {import('../crdt/LWW.js').LWWRegister<unknown>} */ (lwwMax(regA, regB)));
483
498
  }
484
499
 
485
500
  return result;
@@ -530,7 +545,7 @@ function mergeEdgeBirthEvent(a, b) {
530
545
  * - When `options.receipts` is true, returns a TickReceipt per patch for
531
546
  * provenance tracking and debugging.
532
547
  *
533
- * @param {Array<{patch: {writer: string, lamport: number, ops: Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?: *, oid?: string}>, context: Map<string, number>|{[x: string]: number}}, sha: string}>} patches - Array of patch objects with their Git SHAs
548
+ * @param {Array<{patch: {writer: string, lamport: number, ops: Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?: unknown, oid?: string}>, context: Map<string, number>|{[x: string]: number}}, sha: string}>} patches - Array of patch objects with their Git SHAs
534
549
  * @param {WarpStateV5} [initialState] - Optional starting state (for incremental materialization from checkpoint)
535
550
  * @param {Object} [options] - Optional configuration
536
551
  * @param {boolean} [options.receipts=false] - When true, collect and return TickReceipts
@@ -18,6 +18,13 @@ export const FIELD_SEPARATOR = '\0';
18
18
  */
19
19
  export const EDGE_PROP_PREFIX = '\x01';
20
20
 
21
+ /**
22
+ * Well-known property key for content attachment.
23
+ * Stores a content-addressed blob OID as the property value.
24
+ * @const {string}
25
+ */
26
+ export const CONTENT_PROPERTY_KEY = '_content';
27
+
21
28
  /**
22
29
  * Encodes an edge key to a string for Map storage.
23
30
  *
@@ -152,7 +152,7 @@ export default class LogicalTraversal {
152
152
  * @throws {TraversalError} If the labelFilter is invalid (INVALID_LABEL_FILTER)
153
153
  */
154
154
  async _prepare(start, { dir, labelFilter, maxDepth }) {
155
- const materialized = await /** @type {any} */ (this._graph)._materializeGraph(); // TODO(ts-cleanup): narrow port type
155
+ const materialized = await /** @type {{ _materializeGraph: () => Promise<{adjacency: {outgoing: Map<string, Array<{neighborId: string, label: string}>>, incoming: Map<string, Array<{neighborId: string, label: string}>>}}> }} */ (this._graph)._materializeGraph();
156
156
 
157
157
  if (!(await this._graph.hasNode(start))) {
158
158
  throw new TraversalError(`Start node not found: ${start}`, {
@@ -66,7 +66,7 @@ const SHA256_PATTERN = /^[0-9a-f]{64}$/;
66
66
  // -----------------------------------------------------------------------------
67
67
 
68
68
  // Lazy singleton codec instance
69
- /** @type {*} */ // TODO(ts-cleanup): type lazy singleton
69
+ /** @type {TrailerCodec|null} */
70
70
  let _codec = null;
71
71
 
72
72
  /**
@@ -16,7 +16,7 @@ import { createVersionVector, vvIncrement } from '../crdt/VersionVector.js';
16
16
  * @param {Object} v4State - The V4 materialized state (visible projection)
17
17
  * @param {Map<string, {value: boolean}>} v4State.nodeAlive - V4 node alive map
18
18
  * @param {Map<string, {value: boolean}>} v4State.edgeAlive - V4 edge alive map
19
- * @param {Map<string, *>} v4State.prop - V4 property map
19
+ * @param {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>} v4State.prop - V4 property map
20
20
  * @param {string} migrationWriterId - Writer ID to use for synthetic dots
21
21
  * @returns {import('./JoinReducer.js').WarpStateV5} The migrated V5 state
22
22
  */
@@ -43,10 +43,10 @@ function matchGlob(pattern, str) {
43
43
  * - If `expose` is provided and non-empty, only keys in `expose` are included.
44
44
  * - If `expose` is absent/empty, all non-redacted keys are included.
45
45
  *
46
- * @param {Map<string, *>} propsMap - The full properties Map
46
+ * @param {Map<string, unknown>} propsMap - The full properties Map
47
47
  * @param {string[]|undefined} expose - Whitelist of property keys to include
48
48
  * @param {string[]|undefined} redact - Blacklist of property keys to exclude
49
- * @returns {Map<string, *>} Filtered properties Map
49
+ * @returns {Map<string, unknown>} Filtered properties Map
50
50
  */
51
51
  function filterProps(propsMap, expose, redact) {
52
52
  const redactSet = redact && redact.length > 0 ? new Set(redact) : null;
@@ -102,7 +102,7 @@ export default class ObserverView {
102
102
  this._graph = graph;
103
103
 
104
104
  /** @type {LogicalTraversal} */
105
- this.traverse = new LogicalTraversal(/** @type {*} */ (this)); // TODO(ts-cleanup): type observer cast
105
+ this.traverse = new LogicalTraversal(/** @type {import('../WarpGraph.js').default} */ (/** @type {unknown} */ (this)));
106
106
  }
107
107
 
108
108
  /**
@@ -124,11 +124,11 @@ export default class ObserverView {
124
124
  * Builds a filtered adjacency structure that only includes edges
125
125
  * where both endpoints pass the match filter.
126
126
  *
127
- * @returns {Promise<{state: *, stateHash: string, adjacency: {outgoing: Map<string, *[]>, incoming: Map<string, *[]>}}>}
127
+ * @returns {Promise<{state: unknown, stateHash: string, adjacency: {outgoing: Map<string, unknown[]>, incoming: Map<string, unknown[]>}}>}
128
128
  * @private
129
129
  */
130
130
  async _materializeGraph() {
131
- const materialized = await /** @type {*} */ (this._graph)._materializeGraph(); // TODO(ts-cleanup): narrow port type
131
+ const materialized = await /** @type {{ _materializeGraph: () => Promise<{state: import('./JoinReducer.js').WarpStateV5, stateHash: string, adjacency: {outgoing: Map<string, Array<{neighborId: string, label: string}>>, incoming: Map<string, Array<{neighborId: string, label: string}>>}}> }} */ (this._graph)._materializeGraph();
132
132
  const { state, stateHash } = materialized;
133
133
 
134
134
  // Build filtered adjacency: only edges where both endpoints match
@@ -212,7 +212,7 @@ export default class ObserverView {
212
212
  * the observer pattern.
213
213
  *
214
214
  * @param {string} nodeId - The node ID to get properties for
215
- * @returns {Promise<Map<string, *>|null>} Filtered properties Map, or null
215
+ * @returns {Promise<Map<string, unknown>|null>} Filtered properties Map, or null
216
216
  */
217
217
  async getNodeProps(nodeId) {
218
218
  if (!matchGlob(this._matchPattern, nodeId)) {
@@ -234,7 +234,7 @@ export default class ObserverView {
234
234
  *
235
235
  * An edge is visible only when both endpoints match the observer pattern.
236
236
  *
237
- * @returns {Promise<Array<{from: string, to: string, label: string, props: Record<string, *>}>>}
237
+ * @returns {Promise<Array<{from: string, to: string, label: string, props: Record<string, unknown>}>>}
238
238
  */
239
239
  async getEdges() {
240
240
  const allEdges = await this._graph.getEdges();
@@ -260,6 +260,6 @@ export default class ObserverView {
260
260
  * @returns {QueryBuilder} A query builder scoped to this observer
261
261
  */
262
262
  query() {
263
- return new QueryBuilder(/** @type {*} */ (this)); // TODO(ts-cleanup): type observer cast
263
+ return new QueryBuilder(/** @type {import('../WarpGraph.js').default} */ (/** @type {unknown} */ (this)));
264
264
  }
265
265
  }
@@ -23,8 +23,8 @@ import {
23
23
  createPropSetV2,
24
24
  createPatchV2,
25
25
  } from '../types/WarpTypesV2.js';
26
- import { encodeEdgeKey, EDGE_PROP_PREFIX } from './KeyCodec.js';
27
- import { encodePatchMessage, decodePatchMessage } from './WarpMessageCodec.js';
26
+ import { encodeEdgeKey, EDGE_PROP_PREFIX, CONTENT_PROPERTY_KEY } from './KeyCodec.js';
27
+ import { encodePatchMessage, decodePatchMessage, detectMessageKind } from './WarpMessageCodec.js';
28
28
  import { buildWriterRef } from '../utils/RefLayout.js';
29
29
  import WriterError from '../errors/WriterError.js';
30
30
 
@@ -86,7 +86,7 @@ export class PatchBuilderV2 {
86
86
  */
87
87
  constructor({ persistence, graphName, writerId, lamport, versionVector, getCurrentState, expectedParentSha = null, onCommitSuccess = null, onDeleteWithData = 'warn', codec, logger }) {
88
88
  /** @type {import('../../ports/GraphPersistencePort.js').default & import('../../ports/RefPort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default & import('../../ports/TreePort.js').default} */
89
- this._persistence = /** @type {*} */ (persistence); // TODO(ts-cleanup): narrow port type
89
+ this._persistence = /** @type {import('../../ports/GraphPersistencePort.js').default & import('../../ports/RefPort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default & import('../../ports/TreePort.js').default} */ (persistence);
90
90
 
91
91
  /** @type {string} */
92
92
  this._graphName = graphName;
@@ -124,6 +124,13 @@ export class PatchBuilderV2 {
124
124
  /** @type {{ warn: Function }} */
125
125
  this._logger = logger || nullLogger;
126
126
 
127
+ /**
128
+ * Content blob OIDs written during this patch via attachContent/attachEdgeContent.
129
+ * These are embedded in the commit tree for GC protection.
130
+ * @type {string[]}
131
+ */
132
+ this._contentBlobs = [];
133
+
127
134
  /**
128
135
  * Nodes/edges read by this patch (for provenance tracking).
129
136
  *
@@ -346,7 +353,7 @@ export class PatchBuilderV2 {
346
353
  *
347
354
  * @param {string} nodeId - The node ID to set the property on
348
355
  * @param {string} key - Property key (should not contain null bytes)
349
- * @param {*} value - Property value. Must be JSON-serializable (strings,
356
+ * @param {unknown} value - Property value. Must be JSON-serializable (strings,
350
357
  * numbers, booleans, arrays, plain objects, or null). Use `null` to
351
358
  * effectively delete a property (LWW semantics).
352
359
  * @returns {PatchBuilderV2} This builder instance for method chaining
@@ -389,7 +396,7 @@ export class PatchBuilderV2 {
389
396
  * @param {string} to - Target node ID (edge destination)
390
397
  * @param {string} label - Edge label/type identifying which edge to modify
391
398
  * @param {string} key - Property key (should not contain null bytes)
392
- * @param {*} value - Property value. Must be JSON-serializable (strings,
399
+ * @param {unknown} value - Property value. Must be JSON-serializable (strings,
393
400
  * numbers, booleans, arrays, plain objects, or null). Use `null` to
394
401
  * effectively delete a property (LWW semantics).
395
402
  * @returns {PatchBuilderV2} This builder instance for method chaining
@@ -430,6 +437,45 @@ export class PatchBuilderV2 {
430
437
  return this;
431
438
  }
432
439
 
440
+ /**
441
+ * Attaches content to a node by writing the blob to the Git object store
442
+ * and storing the blob OID as the `_content` property.
443
+ *
444
+ * The blob OID is also tracked for embedding in the commit tree, which
445
+ * ensures content blobs survive `git gc` (GC protection via reachability).
446
+ *
447
+ * Note: The node must exist in the materialized state (or be added in
448
+ * this patch) for `getContent()` to find it later. `attachContent()`
449
+ * only sets the `_content` property — it does not create the node.
450
+ *
451
+ * @param {string} nodeId - The node ID to attach content to
452
+ * @param {Buffer|string} content - The content to attach
453
+ * @returns {Promise<PatchBuilderV2>} This builder instance for method chaining
454
+ */
455
+ async attachContent(nodeId, content) {
456
+ const oid = await this._persistence.writeBlob(content);
457
+ this.setProperty(nodeId, CONTENT_PROPERTY_KEY, oid);
458
+ this._contentBlobs.push(oid);
459
+ return this;
460
+ }
461
+
462
+ /**
463
+ * Attaches content to an edge by writing the blob to the Git object store
464
+ * and storing the blob OID as the `_content` edge property.
465
+ *
466
+ * @param {string} from - Source node ID
467
+ * @param {string} to - Target node ID
468
+ * @param {string} label - Edge label
469
+ * @param {Buffer|string} content - The content to attach
470
+ * @returns {Promise<PatchBuilderV2>} This builder instance for method chaining
471
+ */
472
+ async attachEdgeContent(from, to, label, content) {
473
+ const oid = await this._persistence.writeBlob(content);
474
+ this.setEdgeProperty(from, to, label, CONTENT_PROPERTY_KEY, oid);
475
+ this._contentBlobs.push(oid);
476
+ return this;
477
+ }
478
+
433
479
  /**
434
480
  * Builds the PatchV2 object without committing.
435
481
  *
@@ -454,7 +500,7 @@ export class PatchBuilderV2 {
454
500
  schema,
455
501
  writer: this._writerId,
456
502
  lamport: this._lamport,
457
- context: /** @type {*} */ (this._vv), // TODO(ts-cleanup): narrow port type
503
+ context: vvSerialize(this._vv),
458
504
  ops: this._ops,
459
505
  reads: [...this._reads].sort(),
460
506
  writes: [...this._writes].sort(),
@@ -468,11 +514,12 @@ export class PatchBuilderV2 {
468
514
  * 1. Validates the patch is non-empty
469
515
  * 2. Checks for concurrent modifications (compare-and-swap on writer ref)
470
516
  * 3. Calculates the next lamport timestamp from the parent commit
471
- * 4. Encodes the patch as CBOR and writes it as a Git blob
472
- * 5. Creates a Git tree containing the patch blob
473
- * 6. Creates a commit with proper trailers linking to the parent
474
- * 7. Updates the writer ref to point to the new commit
475
- * 8. Invokes the success callback if provided (for eager re-materialization)
517
+ * 4. Builds the PatchV2 structure with the resolved lamport
518
+ * 5. Encodes the patch as CBOR and writes it as a Git blob
519
+ * 6. Creates a Git tree containing the patch blob
520
+ * 7. Creates a commit with proper trailers linking to the parent
521
+ * 8. Updates the writer ref to point to the new commit
522
+ * 9. Invokes the success callback if provided (for eager re-materialization)
476
523
  *
477
524
  * The commit is written to the writer's patch chain at:
478
525
  * `refs/warp/<graphName>/writers/<writerId>`
@@ -524,19 +571,39 @@ export class PatchBuilderV2 {
524
571
  throw err;
525
572
  }
526
573
 
527
- // 3. Calculate lamport and parent from current ref state
528
- let lamport = 1;
574
+ // 3. Calculate lamport and parent from current ref state.
575
+ // Start from this._lamport (set by _nextLamport() in createPatch()), which already
576
+ // incorporates the globally-observed max Lamport tick via _maxObservedLamport.
577
+ // This ensures a first-time writer whose own chain is empty still commits at a tick
578
+ // above any previously-observed writer, winning LWW tiebreakers correctly.
579
+ let lamport = this._lamport;
529
580
  let parentCommit = null;
530
581
 
531
582
  if (currentRefSha) {
532
- // Read the current patch commit to get its lamport timestamp
533
- const commitMessage = await this._persistence.showNode(currentRefSha);
534
- const patchInfo = decodePatchMessage(commitMessage);
535
- lamport = patchInfo.lamport + 1;
536
583
  parentCommit = currentRefSha;
584
+ // Read the current patch commit to get its lamport timestamp and take the max,
585
+ // so the chain stays monotonic even if the ref advanced since createPatch().
586
+ const commitMessage = await this._persistence.showNode(currentRefSha);
587
+ const kind = detectMessageKind(commitMessage);
588
+
589
+ if (kind === 'patch') {
590
+ let patchInfo;
591
+ try {
592
+ patchInfo = decodePatchMessage(commitMessage);
593
+ } catch (err) {
594
+ throw new Error(
595
+ `Failed to parse lamport from writer ref ${writerRef}: ` +
596
+ `commit ${currentRefSha} has invalid patch message format`,
597
+ { cause: err }
598
+ );
599
+ }
600
+ lamport = Math.max(this._lamport, patchInfo.lamport + 1);
601
+ }
602
+ // Non-patch ref (checkpoint, etc.): keep lamport from this._lamport
603
+ // (already incorporates _maxObservedLamport), matching _nextLamport() behavior.
537
604
  }
538
605
 
539
- // 3. Build PatchV2 structure with correct lamport
606
+ // 4. Build PatchV2 structure with correct lamport
540
607
  // Note: Dots were assigned using constructor lamport, but commit lamport may differ.
541
608
  // For now, we use the calculated lamport for the patch metadata.
542
609
  // The dots themselves are independent of patch lamport (they use VV counters).
@@ -552,18 +619,20 @@ export class PatchBuilderV2 {
552
619
  writes: [...this._writes].sort(),
553
620
  });
554
621
 
555
- // 4. Encode patch as CBOR
622
+ // 5. Encode patch as CBOR and write as a Git blob
556
623
  const patchCbor = this._codec.encode(patch);
557
-
558
- // 5. Write patch.cbor blob
559
624
  const patchBlobOid = await this._persistence.writeBlob(/** @type {Buffer} */ (patchCbor));
560
625
 
561
- // 6. Create tree with the blob
626
+ // 6. Create tree with the patch blob + any content blobs (deduplicated)
562
627
  // Format for mktree: "mode type oid\tpath"
563
- const treeEntry = `100644 blob ${patchBlobOid}\tpatch.cbor`;
564
- const treeOid = await this._persistence.writeTree([treeEntry]);
628
+ const treeEntries = [`100644 blob ${patchBlobOid}\tpatch.cbor`];
629
+ const uniqueBlobs = [...new Set(this._contentBlobs)];
630
+ for (const blobOid of uniqueBlobs) {
631
+ treeEntries.push(`100644 blob ${blobOid}\t_content_${blobOid}`);
632
+ }
633
+ const treeOid = await this._persistence.writeTree(treeEntries);
565
634
 
566
- // 7. Create patch commit message with trailers (schema:2)
635
+ // 7. Create commit with proper trailers linking to the parent
567
636
  const commitMessage = encodePatchMessage({
568
637
  graph: this._graphName,
569
638
  writer: this._writerId,
@@ -571,8 +640,6 @@ export class PatchBuilderV2 {
571
640
  patchOid: patchBlobOid,
572
641
  schema,
573
642
  });
574
-
575
- // 8. Create commit with tree, linking to previous patch as parent if exists
576
643
  const parents = parentCommit ? [parentCommit] : [];
577
644
  const newCommitSha = await this._persistence.commitNodeWithTree({
578
645
  treeOid,
@@ -580,10 +647,10 @@ export class PatchBuilderV2 {
580
647
  message: commitMessage,
581
648
  });
582
649
 
583
- // 9. Update writer ref to point to new commit
650
+ // 8. Update writer ref to point to new commit
584
651
  await this._persistence.updateRef(writerRef, newCommitSha);
585
652
 
586
- // 10. Notify success callback (updates graph's version vector + eager re-materialize)
653
+ // 9. Notify success callback (updates graph's version vector + eager re-materialize)
587
654
  if (this._onCommitSuccess) {
588
655
  try {
589
656
  await this._onCommitSuccess({ patch, sha: newCommitSha });
@@ -593,7 +660,6 @@ export class PatchBuilderV2 {
593
660
  }
594
661
  }
595
662
 
596
- // 11. Return the new commit SHA
597
663
  return newCommitSha;
598
664
  }
599
665
 
@@ -277,7 +277,7 @@ class ProvenanceIndex {
277
277
  static deserialize(buffer, { codec } = {}) {
278
278
  const c = codec || defaultCodec;
279
279
  /** @type {{ version?: number, entries?: Array<[string, string[]]> }} */
280
- const obj = /** @type {any} */ (c.decode(buffer)); // TODO(ts-cleanup): narrow port type
280
+ const obj = /** @type {{ version?: number, entries?: Array<[string, string[]]> }} */ (c.decode(buffer));
281
281
 
282
282
  if (obj.version !== 1) {
283
283
  throw new Error(`Unsupported ProvenanceIndex version: ${obj.version}`);
@@ -172,7 +172,7 @@ class ProvenancePayload {
172
172
  // Use JoinReducer's reduceV5 for deterministic materialization.
173
173
  // Note: reduceV5 returns { state, receipts } when options.receipts is truthy,
174
174
  // but returns bare WarpStateV5 when no options passed (as here).
175
- return /** @type {import('./JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {*} */ (this.#patches), initialState)); // TODO(ts-cleanup): type patch array
175
+ return /** @type {import('./JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {Parameters<typeof reduceV5>[0]} */ ([...this.#patches]), initialState));
176
176
  }
177
177
 
178
178
  /**
@@ -675,7 +675,7 @@ export default class QueryBuilder {
675
675
  * @throws {QueryError} If an unknown select field is specified (code: E_QUERY_SELECT_FIELD)
676
676
  */
677
677
  async run() {
678
- const materialized = await /** @type {any} */ (this._graph)._materializeGraph(); // TODO(ts-cleanup): narrow port type
678
+ const materialized = await /** @type {{ _materializeGraph: () => Promise<{adjacency: AdjacencyMaps, stateHash: string}> }} */ (this._graph)._materializeGraph();
679
679
  const { adjacency, stateHash } = materialized;
680
680
  const allNodes = sortIds(await this._graph.getNodes());
681
681
 
@@ -805,11 +805,11 @@ export default class QueryBuilder {
805
805
  for (const nodeId of workingSet) {
806
806
  const propsMap = (await this._graph.getNodeProps(nodeId)) || new Map();
807
807
  for (const { segments, values } of propsByAgg.values()) {
808
- /** @type {*} */ // TODO(ts-cleanup): type deep property traversal
808
+ /** @type {unknown} */
809
809
  let value = propsMap.get(segments[0]);
810
810
  for (let i = 1; i < segments.length; i++) {
811
811
  if (value && typeof value === 'object') {
812
- value = value[segments[i]];
812
+ value = /** @type {Record<string, unknown>} */ (value)[segments[i]];
813
813
  } else {
814
814
  value = undefined;
815
815
  break;
@@ -24,8 +24,8 @@ import { decodeEdgeKey, decodePropKey, isEdgePropKey } from './KeyCodec.js';
24
24
  * @property {string} key - Encoded property key
25
25
  * @property {string} nodeId - Node ID (for node props)
26
26
  * @property {string} propKey - Property name
27
- * @property {*} oldValue - Previous value (undefined if new)
28
- * @property {*} newValue - New value
27
+ * @property {unknown} oldValue - Previous value (undefined if new)
28
+ * @property {unknown} newValue - New value
29
29
  */
30
30
 
31
31
  /**
@@ -33,7 +33,7 @@ import { decodeEdgeKey, decodePropKey, isEdgePropKey } from './KeyCodec.js';
33
33
  * @property {string} key - Encoded property key
34
34
  * @property {string} nodeId - Node ID (for node props)
35
35
  * @property {string} propKey - Property name
36
- * @property {*} oldValue - Previous value
36
+ * @property {unknown} oldValue - Previous value
37
37
  */
38
38
 
39
39
  /**
@@ -86,8 +86,8 @@ function compareProps(a, b) {
86
86
 
87
87
  /**
88
88
  * Checks if two arrays are deeply equal.
89
- * @param {Array<*>} a
90
- * @param {Array<*>} b
89
+ * @param {Array<unknown>} a
90
+ * @param {Array<unknown>} b
91
91
  * @returns {boolean}
92
92
  */
93
93
  function arraysEqual(a, b) {
@@ -104,8 +104,8 @@ function arraysEqual(a, b) {
104
104
 
105
105
  /**
106
106
  * Checks if two objects are deeply equal.
107
- * @param {Record<string, *>} a
108
- * @param {Record<string, *>} b
107
+ * @param {Record<string, unknown>} a
108
+ * @param {Record<string, unknown>} b
109
109
  * @returns {boolean}
110
110
  */
111
111
  function objectsEqual(a, b) {
@@ -127,8 +127,8 @@ function objectsEqual(a, b) {
127
127
 
128
128
  /**
129
129
  * Checks if two values are deeply equal (for property comparison).
130
- * @param {*} a
131
- * @param {*} b
130
+ * @param {unknown} a
131
+ * @param {unknown} b
132
132
  * @returns {boolean}
133
133
  */
134
134
  function deepEqual(a, b) {
@@ -148,9 +148,12 @@ function deepEqual(a, b) {
148
148
  return false;
149
149
  }
150
150
  if (Array.isArray(a)) {
151
- return arraysEqual(a, b);
151
+ return arraysEqual(a, /** @type {unknown[]} */ (b));
152
152
  }
153
- return objectsEqual(a, b);
153
+ return objectsEqual(
154
+ /** @type {Record<string, unknown>} */ (a),
155
+ /** @type {Record<string, unknown>} */ (b),
156
+ );
154
157
  }
155
158
 
156
159
  /**
@@ -139,11 +139,11 @@ export async function computeStateHashV5(state, { crypto, codec } = /** @type {{
139
139
  * @param {Buffer} buffer
140
140
  * @param {Object} [options]
141
141
  * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
142
- * @returns {{nodes: string[], edges: Array<{from: string, to: string, label: string}>, props: Array<{node: string, key: string, value: *}>}}
142
+ * @returns {{nodes: string[], edges: Array<{from: string, to: string, label: string}>, props: Array<{node: string, key: string, value: unknown}>}}
143
143
  */
144
144
  export function deserializeStateV5(buffer, { codec } = {}) {
145
145
  const c = codec || defaultCodec;
146
- return /** @type {{nodes: string[], edges: Array<{from: string, to: string, label: string}>, props: Array<{node: string, key: string, value: *}>}} */ (c.decode(buffer));
146
+ return /** @type {{nodes: string[], edges: Array<{from: string, to: string, label: string}>, props: Array<{node: string, key: string, value: unknown}>}} */ (c.decode(buffer));
147
147
  }
148
148
 
149
149
  // ============================================================================