@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
@@ -45,6 +45,7 @@
45
45
 
46
46
  import { Buffer } from 'node:buffer';
47
47
  import { retry } from '@git-stunts/alfred';
48
+ import PersistenceError from '../../domain/errors/PersistenceError.js';
48
49
  import GraphPersistencePort from '../../ports/GraphPersistencePort.js';
49
50
  import { validateOid, validateRef, validateLimit, validateConfigKey } from './adapterValidation.js';
50
51
 
@@ -79,6 +80,17 @@ const TRANSIENT_ERROR_PATTERNS = [
79
80
  * @typedef {Error & { details?: { stderr?: string, code?: number }, exitCode?: number, code?: number }} GitError
80
81
  */
81
82
 
83
+ /**
84
+ * @typedef {{ collect(opts?: { asString?: boolean }): Promise<Buffer | string> } & import('node:stream').Readable} CollectableStream
85
+ */
86
+
87
+ /**
88
+ * @typedef {object} GitPlumbingLike
89
+ * @property {string} emptyTree - The well-known SHA for Git's empty tree
90
+ * @property {(options: { args: string[], input?: string | Buffer }) => Promise<string>} execute - Execute a git command
91
+ * @property {(options: { args: string[] }) => Promise<CollectableStream>} executeStream - Execute a git command returning a stream
92
+ */
93
+
82
94
  /**
83
95
  * Determines if an error is transient and safe to retry.
84
96
  * @param {GitError} error - The error to check
@@ -91,10 +103,12 @@ function isTransientError(error) {
91
103
  return TRANSIENT_ERROR_PATTERNS.some(pattern => searchText.includes(pattern));
92
104
  }
93
105
 
106
+ /** @typedef {import('@git-stunts/alfred').RetryOptions} RetryOptions */
107
+
94
108
  /**
95
109
  * Default retry options for git operations.
96
110
  * Uses exponential backoff with decorrelated jitter.
97
- * @type {import('@git-stunts/alfred').RetryOptions}
111
+ * @type {RetryOptions}
98
112
  */
99
113
  const DEFAULT_RETRY_OPTIONS = {
100
114
  retries: 3,
@@ -135,6 +149,122 @@ function isDanglingObjectError(err) {
135
149
  );
136
150
  }
137
151
 
152
+ /** @type {string[]} Stderr/message patterns indicating a missing Git object. */
153
+ const MISSING_OBJECT_PATTERNS = [
154
+ 'bad object',
155
+ 'not a valid object name',
156
+ 'does not point to a valid object',
157
+ 'missing object',
158
+ 'not a commit',
159
+ 'could not read',
160
+ ];
161
+
162
+ /** @type {string[]} Stderr/message patterns indicating a ref was not found. */
163
+ const REF_NOT_FOUND_PATTERNS = [
164
+ 'not found',
165
+ 'does not exist',
166
+ 'unknown revision',
167
+ 'bad revision',
168
+ ];
169
+
170
+ /** @type {string[]} Stderr/message patterns indicating a ref I/O failure. */
171
+ const REF_IO_PATTERNS = [
172
+ 'cannot lock ref',
173
+ 'unable to create',
174
+ 'permission denied',
175
+ 'failed to lock',
176
+ ];
177
+
178
+ /**
179
+ * Builds a combined search string from an error's message and stderr.
180
+ * @param {GitError} err
181
+ * @returns {string}
182
+ */
183
+ function errorSearchText(err) {
184
+ const message = (err.message || '').toLowerCase();
185
+ const stderr = (err.details?.stderr || '').toLowerCase();
186
+ return `${message} ${stderr}`;
187
+ }
188
+
189
+ /**
190
+ * Checks if a Git error indicates a missing object (commit, blob, tree).
191
+ * Covers exit code 128 with object-related stderr patterns.
192
+ * @param {GitError} err
193
+ * @returns {boolean}
194
+ */
195
+ function isMissingObjectError(err) {
196
+ const code = getExitCode(err);
197
+ if (code !== 128 && code !== 1) {
198
+ return false;
199
+ }
200
+ const text = errorSearchText(err);
201
+ return MISSING_OBJECT_PATTERNS.some(p => text.includes(p));
202
+ }
203
+
204
+ /**
205
+ * Checks if a Git error indicates a ref not found condition.
206
+ * Covers patterns like "not found", "does not exist", "unknown revision".
207
+ * Gated on exit codes 1 (rev-parse --verify --quiet) and 128 (fatal).
208
+ * @param {GitError} err
209
+ * @returns {boolean}
210
+ */
211
+ function isRefNotFoundError(err) {
212
+ const code = getExitCode(err);
213
+ if (code !== 128 && code !== 1) {
214
+ return false;
215
+ }
216
+ const text = errorSearchText(err);
217
+ return REF_NOT_FOUND_PATTERNS.some(p => text.includes(p));
218
+ }
219
+
220
+ /**
221
+ * Checks if a Git error indicates a ref I/O failure
222
+ * (lock contention that exhausted retries, permission errors, etc.).
223
+ * Gated on exit code 128 (fatal).
224
+ * @param {GitError} err
225
+ * @returns {boolean}
226
+ */
227
+ function isRefIoError(err) {
228
+ if (getExitCode(err) !== 128) {
229
+ return false;
230
+ }
231
+ const text = errorSearchText(err);
232
+ return REF_IO_PATTERNS.some(p => text.includes(p));
233
+ }
234
+
235
+ /**
236
+ * Wraps a raw Git error in a typed PersistenceError when the failure
237
+ * matches a known pattern. Returns the original error unchanged if
238
+ * no pattern matches.
239
+ * @param {GitError} err - The raw error from Git plumbing
240
+ * @param {{ ref?: string, oid?: string }} [hint={}] - Optional context hints
241
+ * @returns {PersistenceError|GitError}
242
+ */
243
+ function wrapGitError(err, hint = {}) {
244
+ if (isMissingObjectError(err)) {
245
+ return new PersistenceError(
246
+ hint.oid ? `Missing Git object: ${hint.oid}` : err.message,
247
+ PersistenceError.E_MISSING_OBJECT,
248
+ { cause: /** @type {Error} */ (err), context: { ...hint } },
249
+ );
250
+ }
251
+ if (isRefNotFoundError(err)) {
252
+ return new PersistenceError(
253
+ hint.ref ? `Ref not found: ${hint.ref}` : err.message,
254
+ PersistenceError.E_REF_NOT_FOUND,
255
+ { cause: /** @type {Error} */ (err), context: { ...hint } },
256
+ );
257
+ }
258
+ if (isRefIoError(err)) {
259
+ return new PersistenceError(
260
+ hint.ref ? `Ref I/O error: ${hint.ref}` : err.message,
261
+ PersistenceError.E_REF_IO,
262
+ { cause: /** @type {Error} */ (err), context: { ...hint } },
263
+ );
264
+ }
265
+ return err;
266
+ }
267
+
138
268
  /**
139
269
  * Concrete implementation of {@link GraphPersistencePort} using Git plumbing commands.
140
270
  *
@@ -200,7 +330,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
200
330
  /**
201
331
  * Creates a new GitGraphAdapter instance.
202
332
  *
203
- * @param {{ plumbing: *, retryOptions?: Object }} options - Configuration options
333
+ * @param {{ plumbing: GitPlumbingLike, retryOptions?: Partial<RetryOptions> }} options - Configuration options
204
334
  *
205
335
  * @throws {Error} If plumbing is not provided
206
336
  *
@@ -221,7 +351,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
221
351
 
222
352
  /**
223
353
  * Executes a git command with retry logic.
224
- * @param {Object} options - Options to pass to plumbing.execute
354
+ * @param {{ args: string[], input?: string | Buffer }} options - Options to pass to plumbing.execute
225
355
  * @returns {Promise<string>} Command output
226
356
  * @private
227
357
  */
@@ -259,10 +389,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
259
389
 
260
390
  /**
261
391
  * Creates a commit pointing to the empty tree.
262
- * @param {Object} options
263
- * @param {string} options.message - The commit message (typically CBOR-encoded patch data)
264
- * @param {string[]} [options.parents=[]] - Parent commit SHAs
265
- * @param {boolean} [options.sign=false] - Whether to GPG-sign the commit
392
+ * @param {{ message: string, parents?: string[], sign?: boolean }} options
266
393
  * @returns {Promise<string>} The SHA of the created commit
267
394
  * @throws {Error} If any parent OID is invalid
268
395
  */
@@ -273,11 +400,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
273
400
  /**
274
401
  * Creates a commit pointing to a custom tree (not the empty tree).
275
402
  * Used for WARP patch commits that have attachment trees.
276
- * @param {Object} options
277
- * @param {string} options.treeOid - The tree OID to point to
278
- * @param {string[]} [options.parents=[]] - Parent commit SHAs
279
- * @param {string} options.message - Commit message
280
- * @param {boolean} [options.sign=false] - Whether to GPG sign
403
+ * @param {{ treeOid: string, parents?: string[], message: string, sign?: boolean }} options
281
404
  * @returns {Promise<string>} The created commit SHA
282
405
  */
283
406
  async commitNodeWithTree({ treeOid, parents = [], message, sign = false }) {
@@ -293,7 +416,11 @@ export default class GitGraphAdapter extends GraphPersistencePort {
293
416
  */
294
417
  async showNode(sha) {
295
418
  this._validateOid(sha);
296
- return await this._executeWithRetry({ args: ['show', '-s', '--format=%B', sha] });
419
+ try {
420
+ return await this._executeWithRetry({ args: ['show', '-s', '--format=%B', sha] });
421
+ } catch (err) {
422
+ throw wrapGitError(/** @type {GitError} */ (err), { oid: sha });
423
+ }
297
424
  }
298
425
 
299
426
  /**
@@ -308,13 +435,24 @@ export default class GitGraphAdapter extends GraphPersistencePort {
308
435
  // Format: SHA, author, date, parents (space-separated), then message
309
436
  // Using %x00 to separate fields for reliable parsing
310
437
  const format = '%H%x00%an <%ae>%x00%aI%x00%P%x00%B';
311
- const output = await this._executeWithRetry({
312
- args: ['show', '-s', `--format=${format}`, sha]
313
- });
438
+ let output;
439
+ try {
440
+ output = await this._executeWithRetry({
441
+ args: ['show', '-s', `--format=${format}`, sha]
442
+ });
443
+ } catch (err) {
444
+ throw wrapGitError(/** @type {GitError} */ (err), { oid: sha });
445
+ }
314
446
 
315
447
  const parts = output.split('\x00');
316
448
  if (parts.length < 5) {
317
- throw new Error(`Invalid commit format for SHA ${sha}`);
449
+ // Object exists but output is malformed — semantically closest to
450
+ // E_MISSING_OBJECT since the commit is unusable for data extraction.
451
+ throw new PersistenceError(
452
+ `Invalid commit format for SHA ${sha}`,
453
+ PersistenceError.E_MISSING_OBJECT,
454
+ { context: { oid: sha } },
455
+ );
318
456
  }
319
457
 
320
458
  const [commitSha, author, date, parentsStr, ...messageParts] = parts;
@@ -338,18 +476,19 @@ export default class GitGraphAdapter extends GraphPersistencePort {
338
476
  */
339
477
  async getCommitTree(sha) {
340
478
  this._validateOid(sha);
341
- const output = await this._executeWithRetry({
342
- args: ['rev-parse', `${sha}^{tree}`]
343
- });
344
- return output.trim();
479
+ try {
480
+ const output = await this._executeWithRetry({
481
+ args: ['rev-parse', `${sha}^{tree}`]
482
+ });
483
+ return output.trim();
484
+ } catch (err) {
485
+ throw wrapGitError(/** @type {GitError} */ (err), { oid: sha });
486
+ }
345
487
  }
346
488
 
347
489
  /**
348
490
  * Returns raw git log output for a ref.
349
- * @param {Object} options
350
- * @param {string} options.ref - The Git ref to log from
351
- * @param {number} [options.limit=50] - Maximum number of commits to return
352
- * @param {string} [options.format] - Custom format string for git log
491
+ * @param {{ ref: string, limit?: number, format?: string }} options
353
492
  * @returns {Promise<string>} The raw log output
354
493
  * @throws {Error} If the ref is invalid or the limit is out of range
355
494
  */
@@ -361,7 +500,11 @@ export default class GitGraphAdapter extends GraphPersistencePort {
361
500
  args.push(`--format=${format}`);
362
501
  }
363
502
  args.push(ref);
364
- return await this._executeWithRetry({ args });
503
+ try {
504
+ return await this._executeWithRetry({ args });
505
+ } catch (err) {
506
+ throw wrapGitError(/** @type {GitError} */ (err), { ref });
507
+ }
365
508
  }
366
509
 
367
510
  /**
@@ -369,10 +512,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
369
512
  * Uses the -z flag to produce NUL-terminated output, which:
370
513
  * - Ensures reliable parsing of commits with special characters in messages
371
514
  * - Ignores the i18n.logOutputEncoding config setting for consistent output
372
- * @param {Object} options
373
- * @param {string} options.ref - The ref to log from
374
- * @param {number} [options.limit=1000000] - Maximum number of commits to return
375
- * @param {string} [options.format] - Custom format string for git log
515
+ * @param {{ ref: string, limit?: number, format?: string }} options
376
516
  * @returns {Promise<import('node:stream').Readable>} A readable stream of git log output (NUL-terminated records)
377
517
  * @throws {Error} If the ref is invalid or the limit is out of range
378
518
  */
@@ -469,9 +609,14 @@ export default class GitGraphAdapter extends GraphPersistencePort {
469
609
  */
470
610
  async readTreeOids(treeOid) {
471
611
  this._validateOid(treeOid);
472
- const output = await this._executeWithRetry({
473
- args: ['ls-tree', '-r', '-z', treeOid]
474
- });
612
+ let output;
613
+ try {
614
+ output = await this._executeWithRetry({
615
+ args: ['ls-tree', '-r', '-z', treeOid]
616
+ });
617
+ } catch (err) {
618
+ throw wrapGitError(/** @type {GitError} */ (err), { oid: treeOid });
619
+ }
475
620
 
476
621
  /** @type {Record<string, string>} */
477
622
  const oids = {};
@@ -502,12 +647,16 @@ export default class GitGraphAdapter extends GraphPersistencePort {
502
647
  */
503
648
  async readBlob(oid) {
504
649
  this._validateOid(oid);
505
- const stream = await this.plumbing.executeStream({
506
- args: ['cat-file', 'blob', oid]
507
- });
508
- const raw = await stream.collect({ asString: false });
509
- // Ensure a real Node Buffer (plumbing may return Uint8Array)
510
- return Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
650
+ try {
651
+ const stream = await this.plumbing.executeStream({
652
+ args: ['cat-file', 'blob', oid]
653
+ });
654
+ const raw = await stream.collect({ asString: false });
655
+ // Ensure a real Node Buffer (plumbing may return Uint8Array)
656
+ return Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
657
+ } catch (err) {
658
+ throw wrapGitError(/** @type {GitError} */ (err), { oid });
659
+ }
511
660
  }
512
661
 
513
662
  /**
@@ -520,9 +669,13 @@ export default class GitGraphAdapter extends GraphPersistencePort {
520
669
  async updateRef(ref, oid) {
521
670
  this._validateRef(ref);
522
671
  this._validateOid(oid);
523
- await this._executeWithRetry({
524
- args: ['update-ref', ref, oid]
525
- });
672
+ try {
673
+ await this._executeWithRetry({
674
+ args: ['update-ref', ref, oid]
675
+ });
676
+ } catch (err) {
677
+ throw wrapGitError(/** @type {GitError} */ (err), { ref, oid });
678
+ }
526
679
  }
527
680
 
528
681
  /**
@@ -551,7 +704,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
551
704
  if (isDanglingObjectError(gitErr)) {
552
705
  return null;
553
706
  }
554
- throw err;
707
+ throw wrapGitError(gitErr, { ref });
555
708
  }
556
709
  }
557
710
 
@@ -576,9 +729,13 @@ export default class GitGraphAdapter extends GraphPersistencePort {
576
729
  this._validateOid(expectedOid);
577
730
  }
578
731
  // Direct call — CAS failures are semantically expected and must NOT be retried.
579
- await this.plumbing.execute({
580
- args: ['update-ref', ref, newOid, oldArg],
581
- });
732
+ try {
733
+ await this.plumbing.execute({
734
+ args: ['update-ref', ref, newOid, oldArg],
735
+ });
736
+ } catch (err) {
737
+ throw wrapGitError(/** @type {GitError} */ (err), { ref, oid: newOid });
738
+ }
582
739
  }
583
740
 
584
741
  /**
@@ -589,9 +746,13 @@ export default class GitGraphAdapter extends GraphPersistencePort {
589
746
  */
590
747
  async deleteRef(ref) {
591
748
  this._validateRef(ref);
592
- await this._executeWithRetry({
593
- args: ['update-ref', '-d', ref]
594
- });
749
+ try {
750
+ await this._executeWithRetry({
751
+ args: ['update-ref', '-d', ref]
752
+ });
753
+ } catch (err) {
754
+ throw wrapGitError(/** @type {GitError} */ (err), { ref });
755
+ }
595
756
  }
596
757
 
597
758
  /**
@@ -645,14 +806,25 @@ export default class GitGraphAdapter extends GraphPersistencePort {
645
806
  /**
646
807
  * Lists refs matching a prefix.
647
808
  * @param {string} prefix - The ref prefix to match (e.g., 'refs/warp/events/writers/')
809
+ * @param {{ limit?: number }} [options] - Optional parameters. When `limit` is omitted or 0, all matching refs are returned.
648
810
  * @returns {Promise<string[]>} Array of matching ref paths
649
- * @throws {Error} If the prefix is invalid
811
+ * @throws {Error} If the prefix is invalid or the limit is out of range
650
812
  */
651
- async listRefs(prefix) {
813
+ async listRefs(prefix, options) {
652
814
  this._validateRef(prefix);
653
- const output = await this._executeWithRetry({
654
- args: ['for-each-ref', '--format=%(refname)', prefix]
655
- });
815
+ const limit = options?.limit;
816
+ const args = ['for-each-ref', '--format=%(refname)'];
817
+ if (limit) {
818
+ this._validateLimit(limit);
819
+ args.push(`--count=${limit}`);
820
+ }
821
+ args.push(prefix);
822
+ let output;
823
+ try {
824
+ output = await this._executeWithRetry({ args });
825
+ } catch (err) {
826
+ throw wrapGitError(/** @type {GitError} */ (err), { ref: prefix });
827
+ }
656
828
  // Parse output - one ref per line, filter empty lines
657
829
  return output.split('\n').filter(line => line.trim());
658
830
  }
@@ -687,10 +859,14 @@ export default class GitGraphAdapter extends GraphPersistencePort {
687
859
  */
688
860
  async countNodes(ref) {
689
861
  this._validateRef(ref);
690
- const output = await this._executeWithRetry({
691
- args: ['rev-list', '--count', ref]
692
- });
693
- return parseInt(output.trim(), 10);
862
+ try {
863
+ const output = await this._executeWithRetry({
864
+ args: ['rev-list', '--count', ref]
865
+ });
866
+ return parseInt(output.trim(), 10);
867
+ } catch (err) {
868
+ throw wrapGitError(/** @type {GitError} */ (err), { ref });
869
+ }
694
870
  }
695
871
 
696
872
  /**
@@ -394,9 +394,10 @@ export default class InMemoryGraphAdapter extends GraphPersistencePort {
394
394
 
395
395
  /**
396
396
  * @param {string} prefix
397
+ * @param {{ limit?: number }} [options]
397
398
  * @returns {Promise<string[]>}
398
399
  */
399
- async listRefs(prefix) {
400
+ async listRefs(prefix, options) {
400
401
  validateRef(prefix);
401
402
  const result = [];
402
403
  for (const key of this._refs.keys()) {
@@ -404,7 +405,13 @@ export default class InMemoryGraphAdapter extends GraphPersistencePort {
404
405
  result.push(key);
405
406
  }
406
407
  }
407
- return result.sort();
408
+ const sorted = result.sort();
409
+ const limit = options?.limit;
410
+ if (limit) {
411
+ validateLimit(limit);
412
+ return sorted.slice(0, limit);
413
+ }
414
+ return sorted;
408
415
  }
409
416
 
410
417
  // ── ConfigPort ──────────────────────────────────────────────────────
@@ -9,7 +9,7 @@ const MAX_BODY_BYTES = 10 * 1024 * 1024;
9
9
  * a 500 response if the handler throws.
10
10
  * @param {import('node:http').IncomingMessage} req
11
11
  * @param {import('node:http').ServerResponse} res
12
- * @param {{ handler: Function, logger: { error: Function } }} options
12
+ * @param {{ handler: (request: import('../../ports/HttpServerPort.js').HttpRequest) => Promise<import('../../ports/HttpServerPort.js').HttpResponse>, logger: { error: (...args: unknown[]) => void } }} options
13
13
  */
14
14
  async function dispatch(req, res, { handler, logger }) {
15
15
  try {
@@ -27,12 +27,12 @@ async function dispatch(req, res, { handler, logger }) {
27
27
  }
28
28
  const body = Buffer.concat(chunks);
29
29
 
30
- const response = await handler({
31
- method: req.method,
32
- url: req.url,
33
- headers: req.headers,
30
+ const response = await handler(/** @type {import('../../ports/HttpServerPort.js').HttpRequest} */ ({
31
+ method: req.method || 'GET',
32
+ url: req.url || '/',
33
+ headers: /** @type {Record<string, string>} */ (req.headers),
34
34
  body: body.length > 0 ? body : undefined,
35
- });
35
+ }));
36
36
 
37
37
  res.writeHead(response.status || 200, response.headers || {});
38
38
  res.end(response.body);
@@ -56,7 +56,7 @@ const noopLogger = { error() {} };
56
56
  */
57
57
  export default class NodeHttpAdapter extends HttpServerPort {
58
58
  /**
59
- * @param {{ logger?: { error: Function } }} [options]
59
+ * @param {{ logger?: { error: (...args: unknown[]) => void } }} [options]
60
60
  */
61
61
  constructor({ logger } = {}) {
62
62
  super();
@@ -64,8 +64,8 @@ export default class NodeHttpAdapter extends HttpServerPort {
64
64
  }
65
65
 
66
66
  /**
67
- * @param {Function} requestHandler
68
- * @returns {{ listen: Function, close: Function, address: Function }}
67
+ * @param {(request: import('../../ports/HttpServerPort.js').HttpRequest) => Promise<import('../../ports/HttpServerPort.js').HttpResponse>} requestHandler
68
+ * @returns {import('../../ports/HttpServerPort.js').HttpServerHandle}
69
69
  */
70
70
  createServer(requestHandler) {
71
71
  const logger = this._logger;
@@ -79,8 +79,8 @@ export default class NodeHttpAdapter extends HttpServerPort {
79
79
  return {
80
80
  /**
81
81
  * @param {number} port
82
- * @param {string|Function} [host]
83
- * @param {Function} [callback]
82
+ * @param {string|((err?: Error | null) => void)} [host]
83
+ * @param {(err?: Error | null) => void} [callback]
84
84
  */
85
85
  listen(port, host, callback) {
86
86
  const cb = typeof host === 'function' ? host : callback;
@@ -88,7 +88,7 @@ export default class NodeHttpAdapter extends HttpServerPort {
88
88
  /** @param {unknown} err */
89
89
  const onError = (err) => {
90
90
  if (cb) {
91
- cb(err);
91
+ cb(err instanceof Error ? err : new Error(String(err)));
92
92
  }
93
93
  };
94
94
  server.once('error', onError);
@@ -108,12 +108,12 @@ export default class NodeHttpAdapter extends HttpServerPort {
108
108
  });
109
109
  }
110
110
  },
111
- /** @param {((err?: Error) => void)} [callback] */
111
+ /** @param {(err?: Error) => void} [callback] */
112
112
  close(callback) {
113
113
  server.close(callback);
114
114
  },
115
115
  address() {
116
- return server.address();
116
+ return /** @type {{ address: string, port: number, family: string } | null} */ (server.address());
117
117
  },
118
118
  };
119
119
  }
@@ -70,8 +70,7 @@ function bufToHex(buf) {
70
70
  export default class WebCryptoAdapter extends CryptoPort {
71
71
  /**
72
72
  * Creates a new WebCryptoAdapter.
73
- * @param {Object} [options] - Configuration options
74
- * @param {SubtleCrypto} [options.subtle] - SubtleCrypto instance (defaults to globalThis.crypto.subtle)
73
+ * @param {{ subtle?: SubtleCrypto }} [options] - Configuration options
75
74
  */
76
75
  constructor({ subtle } = {}) {
77
76
  super();
@@ -10,7 +10,7 @@
10
10
  export default class BlobPort {
11
11
  /**
12
12
  * Writes content as a Git blob and returns its OID.
13
- * @param {Uint8Array|Buffer|string} _content - The blob content to write
13
+ * @param {Uint8Array | string} _content - The blob content to write
14
14
  * @returns {Promise<string>} The Git OID of the created blob
15
15
  * @throws {Error} If not implemented by a concrete adapter
16
16
  */
@@ -21,7 +21,7 @@ export default class BlobPort {
21
21
  /**
22
22
  * Reads the content of a Git blob.
23
23
  * @param {string} _oid - The blob OID to read
24
- * @returns {Promise<Buffer>} The blob content
24
+ * @returns {Promise<Uint8Array>} The blob content
25
25
  * @throws {Error} If not implemented by a concrete adapter
26
26
  */
27
27
  async readBlob(_oid) {
@@ -1,3 +1,25 @@
1
+ /**
2
+ * @typedef {Object} HttpRequest
3
+ * @property {string} method - HTTP method (GET, POST, etc.)
4
+ * @property {string} url - Request URL path + query string
5
+ * @property {Record<string, string>} headers - Lowercased header map
6
+ * @property {Buffer | Uint8Array | undefined} body - Raw body bytes (undefined for bodiless requests)
7
+ */
8
+
9
+ /**
10
+ * @typedef {Object} HttpResponse
11
+ * @property {number} [status] - HTTP status code (defaults to 200)
12
+ * @property {Record<string, string>} [headers] - Response headers
13
+ * @property {string | Buffer | Uint8Array | null} [body] - Response body
14
+ */
15
+
16
+ /**
17
+ * @typedef {Object} HttpServerHandle
18
+ * @property {(port: number, host?: string | ((err?: Error | null) => void), callback?: (err?: Error | null) => void) => void} listen
19
+ * @property {(callback?: (err?: Error) => void) => void} close
20
+ * @property {() => { address: string, port: number, family: string } | null} address
21
+ */
22
+
1
23
  /**
2
24
  * Port for HTTP server creation.
3
25
  *
@@ -12,8 +34,8 @@ export default class HttpServerPort {
12
34
  * and must return `{ status, headers, body }`. No raw req/res objects
13
35
  * are exposed to the domain.
14
36
  *
15
- * @param {(request: { method: string, url: string, headers: Object, body: Buffer|undefined }) => Promise<{ status: number, headers: Object, body: string|Buffer }>} _requestHandler - Async function (request) => response
16
- * @returns {{ listen: Function, close: Function, address: Function }} Server with listen(port, [host], cb(err)), close(cb), and address()
37
+ * @param {(request: HttpRequest) => Promise<HttpResponse>} _requestHandler - Async function (request) => response
38
+ * @returns {HttpServerHandle} Server with listen(port, [host], cb(err)), close(cb), and address()
17
39
  */
18
40
  createServer(_requestHandler) {
19
41
  throw new Error('HttpServerPort.createServer() not implemented');
@@ -42,10 +42,11 @@ export default class RefPort {
42
42
  /**
43
43
  * Lists refs matching a prefix.
44
44
  * @param {string} _prefix - The ref prefix to match (e.g., 'refs/warp/events/writers/')
45
+ * @param {{ limit?: number }} [_options] - Optional parameters. When `limit` is omitted or 0, all matching refs are returned.
45
46
  * @returns {Promise<string[]>} Array of matching ref names
46
47
  * @throws {Error} If not implemented by a concrete adapter
47
48
  */
48
- async listRefs(_prefix) {
49
+ async listRefs(_prefix, _options) {
49
50
  throw new Error('RefPort.listRefs() not implemented');
50
51
  }
51
52
 
@@ -4,7 +4,7 @@ import boxen from 'boxen';
4
4
  * Wraps content in a bordered box using boxen.
5
5
  *
6
6
  * @param {string} content - The text content to display inside the box
7
- * @param {Object} [options] - Options forwarded to boxen (e.g. title, borderColor)
7
+ * @param {Record<string, unknown>} [options] - Options forwarded to boxen (e.g. title, borderColor)
8
8
  * @returns {string} The boxed content string
9
9
  */
10
10
  export function createBox(content, options = {}) {
@@ -227,11 +227,7 @@ function buildStateLines(status, gc) {
227
227
 
228
228
  /**
229
229
  * Build the metadata section lines (writers, checkpoint, coverage, hooks).
230
- * @param {Object} opts - Metadata options
231
- * @param {WritersInfo} opts.writers - Writers info
232
- * @param {CheckpointInfo} opts.checkpoint - Checkpoint info
233
- * @param {CoverageInfo} opts.coverage - Coverage info
234
- * @param {HookInfo | null} opts.hook - Hook status
230
+ * @param {{ writers: WritersInfo, checkpoint: CheckpointInfo, coverage: CoverageInfo, hook: HookInfo | null }} opts - Metadata options
235
231
  * @returns {string[]}
236
232
  */
237
233
  function buildMetadataLines({ writers, checkpoint, coverage, hook }) {
@@ -67,12 +67,7 @@ function renderTruncationIndicator(truncated, hiddenCount) {
67
67
 
68
68
  /**
69
69
  * Renders a single patch entry line.
70
- * @param {Object} params - Entry parameters
71
- * @param {PatchEntry} params.entry - Patch entry
72
- * @param {boolean} params.isLast - Whether this is the last entry
73
- * @param {number} params.lamportWidth - Width for lamport timestamp padding
74
- * @param {string} [params.writerStr] - Writer string
75
- * @param {number} [params.maxWriterIdLen] - Max writer ID length for padding
70
+ * @param {{ entry: PatchEntry, isLast: boolean, lamportWidth: number, writerStr?: string, maxWriterIdLen?: number }} params - Entry parameters
76
71
  * @returns {string} Formatted entry line
77
72
  */
78
73
  function renderEntryLine({ entry, isLast, lamportWidth, writerStr, maxWriterIdLen }) {