@git-stunts/git-warp 10.8.0 → 11.3.3

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 (136) hide show
  1. package/README.md +53 -32
  2. package/SECURITY.md +64 -0
  3. package/bin/cli/commands/check.js +168 -0
  4. package/bin/cli/commands/doctor/checks.js +422 -0
  5. package/bin/cli/commands/doctor/codes.js +46 -0
  6. package/bin/cli/commands/doctor/index.js +239 -0
  7. package/bin/cli/commands/doctor/types.js +89 -0
  8. package/bin/cli/commands/history.js +80 -0
  9. package/bin/cli/commands/info.js +139 -0
  10. package/bin/cli/commands/install-hooks.js +128 -0
  11. package/bin/cli/commands/materialize.js +99 -0
  12. package/bin/cli/commands/patch.js +142 -0
  13. package/bin/cli/commands/path.js +88 -0
  14. package/bin/cli/commands/query.js +235 -0
  15. package/bin/cli/commands/registry.js +32 -0
  16. package/bin/cli/commands/seek.js +598 -0
  17. package/bin/cli/commands/tree.js +230 -0
  18. package/bin/cli/commands/trust.js +154 -0
  19. package/bin/cli/commands/verify-audit.js +114 -0
  20. package/bin/cli/commands/view.js +46 -0
  21. package/bin/cli/infrastructure.js +350 -0
  22. package/bin/cli/schemas.js +177 -0
  23. package/bin/cli/shared.js +244 -0
  24. package/bin/cli/types.js +96 -0
  25. package/bin/presenters/index.js +41 -9
  26. package/bin/presenters/json.js +14 -12
  27. package/bin/presenters/text.js +286 -28
  28. package/bin/warp-graph.js +5 -2346
  29. package/index.d.ts +111 -21
  30. package/index.js +2 -0
  31. package/package.json +10 -8
  32. package/src/domain/WarpGraph.js +109 -3252
  33. package/src/domain/crdt/ORSet.js +8 -8
  34. package/src/domain/errors/EmptyMessageError.js +2 -2
  35. package/src/domain/errors/ForkError.js +1 -1
  36. package/src/domain/errors/IndexError.js +1 -1
  37. package/src/domain/errors/OperationAbortedError.js +1 -1
  38. package/src/domain/errors/QueryError.js +3 -3
  39. package/src/domain/errors/SchemaUnsupportedError.js +1 -1
  40. package/src/domain/errors/ShardCorruptionError.js +2 -2
  41. package/src/domain/errors/ShardLoadError.js +2 -2
  42. package/src/domain/errors/ShardValidationError.js +4 -4
  43. package/src/domain/errors/StorageError.js +2 -2
  44. package/src/domain/errors/SyncError.js +1 -1
  45. package/src/domain/errors/TraversalError.js +1 -1
  46. package/src/domain/errors/TrustError.js +29 -0
  47. package/src/domain/errors/WarpError.js +2 -2
  48. package/src/domain/errors/WormholeError.js +1 -1
  49. package/src/domain/errors/index.js +1 -0
  50. package/src/domain/services/AuditMessageCodec.js +137 -0
  51. package/src/domain/services/AuditReceiptService.js +471 -0
  52. package/src/domain/services/AuditVerifierService.js +707 -0
  53. package/src/domain/services/BitmapIndexBuilder.js +3 -3
  54. package/src/domain/services/BitmapIndexReader.js +28 -19
  55. package/src/domain/services/BoundaryTransitionRecord.js +18 -17
  56. package/src/domain/services/CheckpointSerializerV5.js +17 -16
  57. package/src/domain/services/CheckpointService.js +2 -2
  58. package/src/domain/services/CommitDagTraversalService.js +13 -13
  59. package/src/domain/services/DagPathFinding.js +7 -7
  60. package/src/domain/services/DagTopology.js +1 -1
  61. package/src/domain/services/DagTraversal.js +1 -1
  62. package/src/domain/services/HealthCheckService.js +1 -1
  63. package/src/domain/services/HookInstaller.js +1 -1
  64. package/src/domain/services/HttpSyncServer.js +120 -55
  65. package/src/domain/services/IndexRebuildService.js +7 -7
  66. package/src/domain/services/IndexStalenessChecker.js +4 -3
  67. package/src/domain/services/JoinReducer.js +11 -11
  68. package/src/domain/services/LogicalTraversal.js +1 -1
  69. package/src/domain/services/MessageCodecInternal.js +4 -1
  70. package/src/domain/services/MessageSchemaDetector.js +2 -2
  71. package/src/domain/services/MigrationService.js +1 -1
  72. package/src/domain/services/ObserverView.js +8 -8
  73. package/src/domain/services/PatchBuilderV2.js +42 -26
  74. package/src/domain/services/ProvenanceIndex.js +1 -1
  75. package/src/domain/services/ProvenancePayload.js +1 -1
  76. package/src/domain/services/QueryBuilder.js +3 -3
  77. package/src/domain/services/StateDiff.js +14 -11
  78. package/src/domain/services/StateSerializerV5.js +2 -2
  79. package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
  80. package/src/domain/services/SyncAuthService.js +71 -4
  81. package/src/domain/services/SyncProtocol.js +25 -11
  82. package/src/domain/services/TemporalQuery.js +9 -6
  83. package/src/domain/services/TranslationCost.js +7 -5
  84. package/src/domain/services/WarpMessageCodec.js +4 -1
  85. package/src/domain/services/WormholeService.js +16 -7
  86. package/src/domain/trust/TrustCanonical.js +42 -0
  87. package/src/domain/trust/TrustCrypto.js +111 -0
  88. package/src/domain/trust/TrustEvaluator.js +195 -0
  89. package/src/domain/trust/TrustRecordService.js +281 -0
  90. package/src/domain/trust/TrustStateBuilder.js +222 -0
  91. package/src/domain/trust/canonical.js +68 -0
  92. package/src/domain/trust/reasonCodes.js +64 -0
  93. package/src/domain/trust/schemas.js +160 -0
  94. package/src/domain/trust/verdict.js +42 -0
  95. package/src/domain/types/TickReceipt.js +1 -1
  96. package/src/domain/types/WarpErrors.js +45 -0
  97. package/src/domain/types/WarpOptions.js +29 -0
  98. package/src/domain/types/WarpPersistence.js +41 -0
  99. package/src/domain/types/WarpTypes.js +2 -2
  100. package/src/domain/types/WarpTypesV2.js +2 -2
  101. package/src/domain/types/git-cas.d.ts +20 -0
  102. package/src/domain/utils/MinHeap.js +6 -5
  103. package/src/domain/utils/RefLayout.js +59 -0
  104. package/src/domain/utils/canonicalStringify.js +5 -4
  105. package/src/domain/utils/roaring.js +31 -5
  106. package/src/domain/warp/PatchSession.js +26 -17
  107. package/src/domain/warp/Writer.js +18 -3
  108. package/src/domain/warp/_internal.js +26 -0
  109. package/src/domain/warp/_wire.js +58 -0
  110. package/src/domain/warp/_wiredMethods.d.ts +254 -0
  111. package/src/domain/warp/checkpoint.methods.js +401 -0
  112. package/src/domain/warp/fork.methods.js +323 -0
  113. package/src/domain/warp/materialize.methods.js +238 -0
  114. package/src/domain/warp/materializeAdvanced.methods.js +350 -0
  115. package/src/domain/warp/patch.methods.js +554 -0
  116. package/src/domain/warp/provenance.methods.js +286 -0
  117. package/src/domain/warp/query.methods.js +280 -0
  118. package/src/domain/warp/subscribe.methods.js +272 -0
  119. package/src/domain/warp/sync.methods.js +554 -0
  120. package/src/globals.d.ts +64 -0
  121. package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
  122. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
  123. package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
  124. package/src/infrastructure/adapters/GitGraphAdapter.js +79 -11
  125. package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
  126. package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
  127. package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
  128. package/src/ports/CommitPort.js +10 -0
  129. package/src/ports/RefPort.js +17 -0
  130. package/src/visualization/layouts/converters.js +2 -2
  131. package/src/visualization/layouts/elkAdapter.js +1 -1
  132. package/src/visualization/layouts/elkLayout.js +10 -7
  133. package/src/visualization/layouts/index.js +1 -1
  134. package/src/visualization/renderers/ascii/seek.js +16 -6
  135. package/src/visualization/renderers/svg/index.js +1 -1
  136. package/src/hooks/post-merge.sh +0 -60
@@ -16,6 +16,11 @@
16
16
  * @module infrastructure/adapters/CasSeekCacheAdapter
17
17
  */
18
18
 
19
+ /**
20
+ * Minimal interface for the ContentAddressableStore from @git-stunts/git-cas.
21
+ * @typedef {{ readManifest: Function, restore: Function, store: Function, createTree: Function }} CasStore
22
+ */
23
+
19
24
  import SeekCachePort from '../../ports/SeekCachePort.js';
20
25
  import { buildSeekCacheRef } from '../../domain/utils/RefLayout.js';
21
26
  import { Readable } from 'node:stream';
@@ -59,7 +64,7 @@ export default class CasSeekCacheAdapter extends SeekCachePort {
59
64
  /**
60
65
  * Lazily initializes the ContentAddressableStore.
61
66
  * @private
62
- * @returns {Promise<*>}
67
+ * @returns {Promise<CasStore>}
63
68
  */
64
69
  async _getCas() {
65
70
  if (!this._casPromise) {
@@ -73,7 +78,7 @@ export default class CasSeekCacheAdapter extends SeekCachePort {
73
78
 
74
79
  /**
75
80
  * @private
76
- * @returns {Promise<*>}
81
+ * @returns {Promise<CasStore>}
77
82
  */
78
83
  async _initCas() {
79
84
  const { default: ContentAddressableStore } = await import(
@@ -132,7 +137,7 @@ export default class CasSeekCacheAdapter extends SeekCachePort {
132
137
  * @returns {Promise<CacheIndex>} The mutated index
133
138
  */
134
139
  async _mutateIndex(mutate) {
135
- /** @type {*} */ // TODO(ts-cleanup): type CAS retry error
140
+ /** @type {unknown} */
136
141
  let lastErr;
137
142
  for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
138
143
  const index = await this._readIndex();
@@ -144,7 +149,7 @@ export default class CasSeekCacheAdapter extends SeekCachePort {
144
149
  lastErr = err;
145
150
  // Transient write failure — retry with fresh read
146
151
  if (attempt === MAX_CAS_RETRIES - 1) {
147
- throw new Error(`CasSeekCacheAdapter: index update failed after retries: ${lastErr.message}`);
152
+ throw new Error(`CasSeekCacheAdapter: index update failed after retries: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
148
153
  }
149
154
  }
150
155
  }
@@ -100,8 +100,8 @@ function createHandler(requestHandler, logger) {
100
100
  const plain = await toPlainRequest(request);
101
101
  const response = await requestHandler(plain);
102
102
  return toDenoResponse(response);
103
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
104
- if (err.status === 413) {
103
+ } catch (err) {
104
+ if (typeof err === 'object' && err !== null && /** @type {{status?: number}} */ (err).status === 413) {
105
105
  const msg = new TextEncoder().encode('Payload Too Large');
106
106
  return new Response(msg, {
107
107
  status: 413,
@@ -140,7 +140,7 @@ function closeImpl(state, callback) {
140
140
  callback();
141
141
  }
142
142
  },
143
- /** @param {*} err */ (err) => {
143
+ /** @param {unknown} err */ (err) => {
144
144
  state.server = null;
145
145
  if (callback) {
146
146
  callback(err);
@@ -210,7 +210,7 @@ export default class DenoHttpAdapter extends HttpServerPort {
210
210
  const hostname = typeof host === 'string' ? host : undefined;
211
211
 
212
212
  try {
213
- /** @type {*} */ // TODO(ts-cleanup): type Deno.serve options
213
+ /** @type {DenoServeOptions} */
214
214
  const serveOptions = {
215
215
  port,
216
216
  onListen() {
@@ -223,9 +223,8 @@ export default class DenoHttpAdapter extends HttpServerPort {
223
223
  serveOptions.hostname = hostname;
224
224
  }
225
225
 
226
- // @ts-expect-error — Deno global is only available in Deno runtime
227
226
  state.server = globalThis.Deno.serve(serveOptions, handler);
228
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
227
+ } catch (err) {
229
228
  if (cb) {
230
229
  cb(err);
231
230
  } else {
@@ -114,6 +114,26 @@ function getExitCode(err) {
114
114
  return err?.details?.code ?? err?.exitCode ?? err?.code;
115
115
  }
116
116
 
117
+ /**
118
+ * Checks if a Git error indicates a dangling or missing object.
119
+ * Exit code 128 with specific stderr patterns means the ref exists but
120
+ * points to a missing object. Other exit-128 failures (bad repo, corrupt
121
+ * index, permission errors) are NOT considered dangling and will re-throw.
122
+ * @param {GitError} err
123
+ * @returns {boolean}
124
+ */
125
+ function isDanglingObjectError(err) {
126
+ if (getExitCode(err) !== 128) {
127
+ return false;
128
+ }
129
+ const stderr = (err.details?.stderr || '').toLowerCase();
130
+ return (
131
+ stderr.includes('bad object') ||
132
+ stderr.includes('not a valid object name') ||
133
+ stderr.includes('does not point to a valid object')
134
+ );
135
+ }
136
+
117
137
  /**
118
138
  * Checks whether a Git ref exists without resolving it.
119
139
  * @param {function(Object): Promise<string>} execute - The git command executor function
@@ -125,8 +145,12 @@ async function refExists(execute, ref) {
125
145
  try {
126
146
  await execute({ args: ['show-ref', '--verify', '--quiet', ref] });
127
147
  return true;
128
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
129
- if (getExitCode(err) === 1) {
148
+ } catch (err) {
149
+ const gitErr = /** @type {GitError} */ (err);
150
+ if (getExitCode(gitErr) === 1) {
151
+ return false;
152
+ }
153
+ if (isDanglingObjectError(gitErr)) {
130
154
  return false;
131
155
  }
132
156
  throw err;
@@ -325,6 +349,20 @@ export default class GitGraphAdapter extends GraphPersistencePort {
325
349
  };
326
350
  }
327
351
 
352
+ /**
353
+ * Retrieves the tree OID for a given commit SHA.
354
+ * @param {string} sha - The commit SHA to query
355
+ * @returns {Promise<string>} The tree OID pointed to by the commit
356
+ * @throws {Error} If the SHA is invalid
357
+ */
358
+ async getCommitTree(sha) {
359
+ this._validateOid(sha);
360
+ const output = await this._executeWithRetry({
361
+ args: ['rev-parse', `${sha}^{tree}`]
362
+ });
363
+ return output.trim();
364
+ }
365
+
328
366
  /**
329
367
  * Returns raw git log output for a ref.
330
368
  * @param {Object} options
@@ -493,7 +531,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
493
531
  /**
494
532
  * Reads the OID a ref points to.
495
533
  * @param {string} ref - The ref name
496
- * @returns {Promise<string|null>} The OID, or null if the ref does not exist
534
+ * @returns {Promise<string|null>} The OID, or null if the ref does not exist or points to a dangling/missing object
497
535
  * @throws {Error} If the ref format is invalid
498
536
  */
499
537
  async readRef(ref) {
@@ -507,14 +545,44 @@ export default class GitGraphAdapter extends GraphPersistencePort {
507
545
  args: ['rev-parse', ref]
508
546
  });
509
547
  return oid.trim();
510
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
511
- if (getExitCode(err) === 1) {
548
+ } catch (err) {
549
+ const gitErr = /** @type {GitError} */ (err);
550
+ if (getExitCode(gitErr) === 1) {
551
+ return null;
552
+ }
553
+ if (isDanglingObjectError(gitErr)) {
512
554
  return null;
513
555
  }
514
556
  throw err;
515
557
  }
516
558
  }
517
559
 
560
+ /**
561
+ * Atomically updates a ref using compare-and-swap semantics.
562
+ *
563
+ * Uses `git update-ref ref newOid expectedOid` which is atomic CAS.
564
+ * Fails if the ref does not currently point to expectedOid.
565
+ *
566
+ * @param {string} ref - The ref name
567
+ * @param {string} newOid - The new OID to set
568
+ * @param {string|null} expectedOid - The expected current OID, or null if the ref must not exist
569
+ * @returns {Promise<void>}
570
+ * @throws {Error} If the ref does not match the expected value (CAS mismatch)
571
+ */
572
+ async compareAndSwapRef(ref, newOid, expectedOid) {
573
+ this._validateRef(ref);
574
+ this._validateOid(newOid);
575
+ // null means "ref must not exist" → use zero OID
576
+ const oldArg = expectedOid || '0'.repeat(newOid.length);
577
+ if (expectedOid) {
578
+ this._validateOid(expectedOid);
579
+ }
580
+ // Direct call — CAS failures are semantically expected and must NOT be retried.
581
+ await this.plumbing.execute({
582
+ args: ['update-ref', ref, newOid, oldArg],
583
+ });
584
+ }
585
+
518
586
  /**
519
587
  * Deletes a ref.
520
588
  * @param {string} ref - The ref name to delete
@@ -562,8 +630,8 @@ export default class GitGraphAdapter extends GraphPersistencePort {
562
630
  try {
563
631
  await this._executeWithRetry({ args: ['cat-file', '-e', sha] });
564
632
  return true;
565
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
566
- if (getExitCode(err) === 1) {
633
+ } catch (err) {
634
+ if (getExitCode(/** @type {GitError} */ (err)) === 1) {
567
635
  return false;
568
636
  }
569
637
  throw err;
@@ -638,8 +706,8 @@ export default class GitGraphAdapter extends GraphPersistencePort {
638
706
  args: ['merge-base', '--is-ancestor', potentialAncestor, descendant]
639
707
  });
640
708
  return true; // Exit code 0 means it IS an ancestor
641
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
642
- if (this._getExitCode(err) === 1) {
709
+ } catch (err) {
710
+ if (this._getExitCode(/** @type {GitError} */ (err)) === 1) {
643
711
  return false; // Exit code 1 means it is NOT an ancestor
644
712
  }
645
713
  throw err; // Re-throw unexpected errors
@@ -660,8 +728,8 @@ export default class GitGraphAdapter extends GraphPersistencePort {
660
728
  });
661
729
  // Preserve empty-string values; only drop trailing newline
662
730
  return value.replace(/\n$/, '');
663
- } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
664
- if (this._isConfigKeyNotFound(err)) {
731
+ } catch (err) {
732
+ if (this._isConfigKeyNotFound(/** @type {GitError} */ (err))) {
665
733
  return null;
666
734
  }
667
735
  throw err;
@@ -253,6 +253,19 @@ export default class InMemoryGraphAdapter extends GraphPersistencePort {
253
253
  };
254
254
  }
255
255
 
256
+ /**
257
+ * @param {string} sha
258
+ * @returns {Promise<string>}
259
+ */
260
+ async getCommitTree(sha) {
261
+ validateOid(sha);
262
+ const commit = this._commits.get(sha);
263
+ if (!commit) {
264
+ throw new Error(`Commit not found: ${sha}`);
265
+ }
266
+ return commit.treeOid;
267
+ }
268
+
256
269
  /**
257
270
  * @param {string} sha
258
271
  * @returns {Promise<boolean>}
@@ -356,6 +369,29 @@ export default class InMemoryGraphAdapter extends GraphPersistencePort {
356
369
  this._refs.delete(ref);
357
370
  }
358
371
 
372
+ /**
373
+ * Atomically updates a ref using compare-and-swap semantics.
374
+ * @param {string} ref - The ref name
375
+ * @param {string} newOid - The new OID to set
376
+ * @param {string|null} expectedOid - Expected current OID, or null if ref must not exist
377
+ * @returns {Promise<void>}
378
+ * @throws {Error} If the ref does not match the expected value (CAS mismatch)
379
+ */
380
+ async compareAndSwapRef(ref, newOid, expectedOid) {
381
+ validateRef(ref);
382
+ validateOid(newOid);
383
+ if (expectedOid) {
384
+ validateOid(expectedOid);
385
+ }
386
+ const current = this._refs.get(ref) || null;
387
+ if (current !== expectedOid) {
388
+ throw new Error(
389
+ `CAS mismatch on ${ref}: expected ${expectedOid || '(none)'}, got ${current || '(none)'}`,
390
+ );
391
+ }
392
+ this._refs.set(ref, newOid);
393
+ }
394
+
359
395
  /**
360
396
  * @param {string} prefix
361
397
  * @returns {Promise<string[]>}
@@ -71,7 +71,7 @@ export default class NodeHttpAdapter extends HttpServerPort {
71
71
  const logger = this._logger;
72
72
  const server = createServer((req, res) => {
73
73
  dispatch(req, res, { handler: requestHandler, logger }).catch(
74
- /** @param {*} err */ (err) => {
74
+ /** @param {unknown} err */ (err) => {
75
75
  logger.error('[NodeHttpAdapter] unhandled dispatch error:', err);
76
76
  });
77
77
  });
@@ -85,7 +85,7 @@ export default class NodeHttpAdapter extends HttpServerPort {
85
85
  listen(port, host, callback) {
86
86
  const cb = typeof host === 'function' ? host : callback;
87
87
  const bindHost = typeof host === 'string' ? host : undefined;
88
- /** @param {*} err */
88
+ /** @param {unknown} err */
89
89
  const onError = (err) => {
90
90
  if (cb) {
91
91
  cb(err);
@@ -38,8 +38,8 @@ function toWebCryptoAlgo(algorithm) {
38
38
  function toUint8Array(data) {
39
39
  if (data instanceof Uint8Array) { return data; }
40
40
  if (typeof data === 'string') { return new TextEncoder().encode(data); }
41
- if (typeof Buffer !== 'undefined' && Buffer.isBuffer(/** @type {*} */ (data))) { // TODO(ts-cleanup): narrow port type
42
- const buf = /** @type {Buffer} */ (/** @type {*} */ (data)); // TODO(ts-cleanup): narrow port type
41
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(data)) {
42
+ const buf = /** @type {Buffer} */ (data);
43
43
  return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
44
44
  }
45
45
  throw new Error('WebCryptoAdapter: data must be string, Buffer, or Uint8Array');
@@ -90,6 +90,16 @@ export default class CommitPort {
90
90
  throw new Error('CommitPort.nodeExists() not implemented');
91
91
  }
92
92
 
93
+ /**
94
+ * Retrieves the tree OID for a given commit SHA.
95
+ * @param {string} _sha - The commit SHA to query
96
+ * @returns {Promise<string>} The tree OID pointed to by the commit
97
+ * @throws {Error} If not implemented by a concrete adapter
98
+ */
99
+ async getCommitTree(_sha) {
100
+ throw new Error('CommitPort.getCommitTree() not implemented');
101
+ }
102
+
93
103
  /**
94
104
  * Pings the repository to verify accessibility.
95
105
  * @returns {Promise<{ok: boolean, latencyMs: number}>} Health check result with latency
@@ -48,4 +48,21 @@ export default class RefPort {
48
48
  async listRefs(_prefix) {
49
49
  throw new Error('RefPort.listRefs() not implemented');
50
50
  }
51
+
52
+ /**
53
+ * Atomically updates a ref using compare-and-swap semantics.
54
+ *
55
+ * The ref is updated to `_newOid` only if it currently points to `_expectedOid`.
56
+ * If `_expectedOid` is `null`, the ref must not exist (genesis CAS).
57
+ *
58
+ * @param {string} _ref - The ref name
59
+ * @param {string} _newOid - The new OID to set
60
+ * @param {string|null} _expectedOid - The expected current OID, or null if the ref must not exist
61
+ * @returns {Promise<void>}
62
+ * @throws {Error} If the ref does not match the expected value (CAS mismatch)
63
+ * @throws {Error} If not implemented by a concrete adapter
64
+ */
65
+ async compareAndSwapRef(_ref, _newOid, _expectedOid) {
66
+ throw new Error('RefPort.compareAndSwapRef() not implemented');
67
+ }
51
68
  }
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  /**
10
- * @typedef {{ id: string, label: string, props?: Record<string, any> }} GraphDataNode
10
+ * @typedef {{ id: string, label: string, props?: Record<string, unknown> }} GraphDataNode
11
11
  * @typedef {{ from: string, to: string, label?: string }} GraphDataEdge
12
12
  * @typedef {{ nodes: GraphDataNode[], edges: GraphDataEdge[] }} GraphData
13
13
  */
@@ -16,7 +16,7 @@
16
16
  * Converts a query result payload + edge array into graph data.
17
17
  * Edges are filtered to only those connecting matched nodes.
18
18
  *
19
- * @param {{ nodes?: Array<{ id: string, props?: Record<string, any> }> } | null} payload - Query result
19
+ * @param {{ nodes?: Array<{ id: string, props?: Record<string, unknown> }> } | null} payload - Query result
20
20
  * @param {Array<{ from: string, to: string, label?: string }>} edges - Edge array from graph.getEdges()
21
21
  * @returns {GraphData}
22
22
  */
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  /**
6
- * @typedef {{ id: string, label: string, props?: Record<string, any> }} GraphDataNode
6
+ * @typedef {{ id: string, label: string, props?: Record<string, unknown> }} GraphDataNode
7
7
  * @typedef {{ from: string, to: string, label?: string }} GraphDataEdge
8
8
  * @typedef {{ nodes: GraphDataNode[], edges: GraphDataEdge[] }} GraphData
9
9
  * @typedef {{ text: string }} ElkLabel
@@ -7,20 +7,23 @@
7
7
 
8
8
  /**
9
9
  * @typedef {{ id: string, x?: number, y?: number, width?: number, height?: number, labels?: Array<{ text: string }> }} ElkResultChild
10
- * @typedef {{ id: string, sources?: string[], targets?: string[], labels?: Array<{ text: string }>, sections?: any[] }} ElkResultEdge
10
+ * @typedef {{ id: string, sources?: string[], targets?: string[], labels?: Array<{ text: string }>, sections?: unknown[] }} ElkResultEdge
11
11
  * @typedef {{ children?: ElkResultChild[], edges?: ElkResultEdge[], width?: number, height?: number }} ElkResult
12
- * @typedef {{ id: string, x: number, y: number, width: number, height: number, label: string }} PosNode
13
- * @typedef {{ id: string, source: string, target: string, label?: string, sections: any[] }} PosEdge
12
+ * @typedef {{ id: string, x: number, y: number, width: number, height: number, label?: string }} PosNode
13
+ * @typedef {{ x: number, y: number }} LayoutPoint
14
+ * @typedef {{ startPoint?: LayoutPoint, endPoint?: LayoutPoint, bendPoints?: LayoutPoint[] }} LayoutSection
15
+ * @typedef {{ id: string, source: string, target: string, label?: string, sections?: LayoutSection[] }} PosEdge
14
16
  * @typedef {{ nodes: PosNode[], edges: PosEdge[], width: number, height: number }} PositionedGraph
15
17
  * @typedef {{ id: string, children?: Array<{ id: string, width?: number, height?: number, labels?: Array<{ text: string }> }>, edges?: Array<{ id: string, sources?: string[], targets?: string[], labels?: Array<{ text: string }> }>, layoutOptions?: Record<string, string> }} ElkGraphInput
18
+ * @typedef {{ layout: (graph: ElkGraphInput) => Promise<ElkResult> }} ElkEngine
16
19
  */
17
20
 
18
- /** @type {Promise<any> | null} */
21
+ /** @type {Promise<unknown> | null} */
19
22
  let elkPromise = null;
20
23
 
21
24
  /**
22
25
  * Returns (or creates) a singleton ELK instance.
23
- * @returns {Promise<any>} ELK instance
26
+ * @returns {Promise<unknown>} ELK instance
24
27
  */
25
28
  function getElk() {
26
29
  if (!elkPromise) {
@@ -39,7 +42,7 @@ export async function runLayout(elkGraph) {
39
42
  /** @type {ElkResult | undefined} */
40
43
  let result;
41
44
  try {
42
- const elk = await getElk();
45
+ const elk = /** @type {ElkEngine} */ (await getElk());
43
46
  result = await elk.layout(elkGraph);
44
47
  } catch {
45
48
  return fallbackLayout(elkGraph);
@@ -67,7 +70,7 @@ function toPositionedGraph(result) {
67
70
  source: e.sources?.[0] ?? '',
68
71
  target: e.targets?.[0] ?? '',
69
72
  label: e.labels?.[0]?.text,
70
- sections: e.sections ?? [],
73
+ sections: /** @type {LayoutSection[]} */ (e.sections ?? []),
71
74
  }));
72
75
 
73
76
  return {
@@ -21,7 +21,7 @@ import { runLayout } from './elkLayout.js';
21
21
  *
22
22
  * @param {{ nodes: Array<{ id: string, label: string }>, edges: Array<{ from: string, to: string, label?: string }> }} graphData - Normalised graph data
23
23
  * @param {{ type?: 'query'|'path'|'slice', layoutOptions?: Record<string, string> }} [options]
24
- * @returns {Promise<{ nodes: any[], edges: any[], width: number, height: number }>} PositionedGraph
24
+ * @returns {Promise<import('./elkLayout.js').PositionedGraph>} PositionedGraph
25
25
  */
26
26
  export async function layoutGraph(graphData, options = {}) {
27
27
  const elkGraph = toElkGraph(graphData, options);
@@ -30,7 +30,7 @@ import { formatOpSummary } from './opSummary.js';
30
30
  * @property {number} patchCount
31
31
  * @property {Map<string, WriterInfo> | Record<string, WriterInfo>} perWriter
32
32
  * @property {{ nodes?: number, edges?: number }} [diff]
33
- * @property {Record<string, any>} [tickReceipt]
33
+ * @property {Record<string, unknown>} [tickReceipt]
34
34
  * @property {import('../../../domain/services/StateDiff.js').StateDiffResult | null} [structuralDiff]
35
35
  * @property {string} [diffBaseline]
36
36
  * @property {number | null} [baselineTick]
@@ -73,7 +73,7 @@ function pluralize(n, singular, plural) {
73
73
  return n === 1 ? singular : plural;
74
74
  }
75
75
 
76
- /** @param {Record<string, any> | undefined} tickReceipt @returns {string[]} */
76
+ /** @param {Record<string, unknown> | undefined} tickReceipt @returns {string[]} */
77
77
  function buildReceiptLines(tickReceipt) {
78
78
  if (!tickReceipt || typeof tickReceipt !== 'object') {
79
79
  return [];
@@ -85,8 +85,12 @@ function buildReceiptLines(tickReceipt) {
85
85
 
86
86
  const lines = [];
87
87
  for (const [writerId, entry] of entries) {
88
- const sha = typeof entry.sha === 'string' ? entry.sha : null;
89
- const opSummary = entry.opSummary && typeof entry.opSummary === 'object' ? entry.opSummary : entry;
88
+ /** @type {Record<string, unknown>} */
89
+ const rec = /** @type {Record<string, unknown>} */ (entry);
90
+ const sha = typeof rec.sha === 'string' ? rec.sha : null;
91
+ const opSummary = rec.opSummary && typeof rec.opSummary === 'object'
92
+ ? /** @type {Record<string, number>} */ (rec.opSummary)
93
+ : /** @type {Record<string, number>} */ (rec);
90
94
  const name = padRight(formatWriterName(writerId, NAME_W), NAME_W);
91
95
  const shaStr = sha ? ` ${formatSha(sha)}` : '';
92
96
  lines.push(` ${name}${shaStr} ${formatOpSummary(opSummary, 40)}`);
@@ -443,14 +447,20 @@ function collectDiffEntries(diff) {
443
447
 
444
448
  /**
445
449
  * Formats a property value for display (truncated if too long).
446
- * @param {*} value
450
+ * @param {unknown} value
447
451
  * @returns {string}
448
452
  */
449
453
  function formatPropValue(value) {
450
454
  if (value === undefined) {
451
455
  return 'undefined';
452
456
  }
453
- const s = typeof value === 'string' ? `"${value}"` : String(value);
457
+ if (typeof value === 'string') {
458
+ const s = `"${value}"`;
459
+ return s.length > 40 ? `${s.slice(0, 37)}...` : s;
460
+ }
461
+ const s = typeof value === 'number' || typeof value === 'boolean'
462
+ ? String(value)
463
+ : JSON.stringify(value) ?? '';
454
464
  return s.length > 40 ? `${s.slice(0, 37)}...` : s;
455
465
  }
456
466
 
@@ -119,7 +119,7 @@ function renderEdge(edge) {
119
119
  /**
120
120
  * Renders a PositionedGraph as an SVG string.
121
121
  *
122
- * @param {{ nodes?: Array<{ id: string, x: number, y: number, width: number, height: number, label?: string }>, edges?: Array<{ sections?: Array<any>, label?: string }>, width?: number, height?: number }} positionedGraph - PositionedGraph from runLayout()
122
+ * @param {{ nodes?: Array<{ id: string, x: number, y: number, width: number, height: number, label?: string }>, edges?: Array<{ sections?: Array<{ startPoint?: { x: number, y: number }, endPoint?: { x: number, y: number }, bendPoints?: Array<{ x: number, y: number }> }>, label?: string }>, width?: number, height?: number }} positionedGraph - PositionedGraph from runLayout()
123
123
  * @param {{ title?: string }} [options]
124
124
  * @returns {string} Complete SVG markup
125
125
  */
@@ -1,60 +0,0 @@
1
- #!/bin/sh
2
- # --- @git-stunts/git-warp post-merge hook __WARP_HOOK_VERSION__ ---
3
- # warp-hook-version: __WARP_HOOK_VERSION__
4
- #
5
- # Post-merge hook: notify (or auto-materialize) when warp refs changed.
6
- #
7
- # Compares refs/warp/ before and after merge by maintaining
8
- # a snapshot file at .git/warp-ref-snapshot. If any warp writer refs
9
- # changed and warp.autoMaterialize is true, runs `git warp materialize`.
10
- # Otherwise prints an informational message advising re-materialization.
11
- # Always exits 0 — never blocks a merge.
12
-
13
- GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) || exit 0
14
- SNAPSHOT="${GIT_DIR}/warp-ref-snapshot"
15
-
16
- # Capture current warp refs (sorted for stable comparison)
17
- CURRENT=$(git for-each-ref --format='%(refname) %(objectname)' --sort=refname refs/warp/ 2>/dev/null) || true
18
-
19
- if [ -z "$CURRENT" ]; then
20
- # No warp refs exist — clean up any stale snapshot and exit
21
- rm -f "$SNAPSHOT"
22
- exit 0
23
- fi
24
-
25
- CHANGED=0
26
-
27
- if [ -f "$SNAPSHOT" ]; then
28
- PREVIOUS=$(cat "$SNAPSHOT")
29
- if [ "$CURRENT" != "$PREVIOUS" ]; then
30
- CHANGED=1
31
- fi
32
- else
33
- # First encounter — refs exist but no snapshot yet
34
- CHANGED=1
35
- fi
36
-
37
- # Save current state for next comparison
38
- printf '%s\n' "$CURRENT" > "$SNAPSHOT"
39
-
40
- if [ "$CHANGED" -eq 0 ]; then
41
- exit 0
42
- fi
43
-
44
- AUTO_MAT=$(git config --bool warp.autoMaterialize 2>/dev/null) || true
45
-
46
- if [ "$AUTO_MAT" = "true" ]; then
47
- echo "[warp] Refs changed — auto-materializing..."
48
- if command -v git-warp >/dev/null 2>&1; then
49
- git-warp materialize || echo "[warp] Warning: auto-materialize failed."
50
- elif command -v warp-graph >/dev/null 2>&1; then
51
- warp-graph materialize || echo "[warp] Warning: auto-materialize failed."
52
- else
53
- echo "[warp] Warning: neither git-warp nor warp-graph found in PATH."
54
- fi
55
- else
56
- echo "[warp] Writer refs changed during merge. Call materialize() to see updates."
57
- fi
58
-
59
- exit 0
60
- # --- end @git-stunts/git-warp ---