@git-stunts/git-warp 10.1.2 → 10.4.2

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 (106) hide show
  1. package/README.md +31 -4
  2. package/bin/warp-graph.js +1242 -59
  3. package/index.d.ts +31 -0
  4. package/index.js +4 -0
  5. package/package.json +13 -3
  6. package/src/domain/WarpGraph.js +487 -140
  7. package/src/domain/crdt/LWW.js +1 -1
  8. package/src/domain/crdt/ORSet.js +10 -6
  9. package/src/domain/crdt/VersionVector.js +5 -1
  10. package/src/domain/errors/EmptyMessageError.js +2 -4
  11. package/src/domain/errors/ForkError.js +4 -0
  12. package/src/domain/errors/IndexError.js +4 -0
  13. package/src/domain/errors/OperationAbortedError.js +4 -0
  14. package/src/domain/errors/QueryError.js +4 -0
  15. package/src/domain/errors/SchemaUnsupportedError.js +4 -0
  16. package/src/domain/errors/ShardCorruptionError.js +2 -6
  17. package/src/domain/errors/ShardLoadError.js +2 -6
  18. package/src/domain/errors/ShardValidationError.js +2 -7
  19. package/src/domain/errors/StorageError.js +2 -6
  20. package/src/domain/errors/SyncError.js +4 -0
  21. package/src/domain/errors/TraversalError.js +4 -0
  22. package/src/domain/errors/WarpError.js +2 -4
  23. package/src/domain/errors/WormholeError.js +4 -0
  24. package/src/domain/services/AnchorMessageCodec.js +1 -4
  25. package/src/domain/services/BitmapIndexBuilder.js +10 -6
  26. package/src/domain/services/BitmapIndexReader.js +27 -21
  27. package/src/domain/services/BoundaryTransitionRecord.js +22 -15
  28. package/src/domain/services/CheckpointMessageCodec.js +1 -7
  29. package/src/domain/services/CheckpointSerializerV5.js +20 -19
  30. package/src/domain/services/CheckpointService.js +18 -18
  31. package/src/domain/services/CommitDagTraversalService.js +13 -1
  32. package/src/domain/services/DagPathFinding.js +40 -18
  33. package/src/domain/services/DagTopology.js +7 -6
  34. package/src/domain/services/DagTraversal.js +5 -3
  35. package/src/domain/services/Frontier.js +7 -6
  36. package/src/domain/services/HealthCheckService.js +15 -14
  37. package/src/domain/services/HookInstaller.js +64 -13
  38. package/src/domain/services/HttpSyncServer.js +15 -14
  39. package/src/domain/services/IndexRebuildService.js +12 -12
  40. package/src/domain/services/IndexStalenessChecker.js +13 -6
  41. package/src/domain/services/JoinReducer.js +28 -27
  42. package/src/domain/services/LogicalTraversal.js +7 -6
  43. package/src/domain/services/MessageCodecInternal.js +2 -0
  44. package/src/domain/services/ObserverView.js +6 -6
  45. package/src/domain/services/PatchBuilderV2.js +9 -9
  46. package/src/domain/services/PatchMessageCodec.js +1 -7
  47. package/src/domain/services/ProvenanceIndex.js +6 -8
  48. package/src/domain/services/ProvenancePayload.js +1 -2
  49. package/src/domain/services/QueryBuilder.js +29 -23
  50. package/src/domain/services/StateDiff.js +7 -7
  51. package/src/domain/services/StateSerializerV5.js +8 -6
  52. package/src/domain/services/StreamingBitmapIndexBuilder.js +29 -23
  53. package/src/domain/services/SyncProtocol.js +23 -26
  54. package/src/domain/services/TemporalQuery.js +4 -3
  55. package/src/domain/services/TranslationCost.js +4 -4
  56. package/src/domain/services/WormholeService.js +19 -15
  57. package/src/domain/types/TickReceipt.js +10 -6
  58. package/src/domain/types/WarpTypesV2.js +2 -3
  59. package/src/domain/utils/CachedValue.js +1 -1
  60. package/src/domain/utils/LRUCache.js +3 -3
  61. package/src/domain/utils/MinHeap.js +2 -2
  62. package/src/domain/utils/RefLayout.js +106 -15
  63. package/src/domain/utils/WriterId.js +2 -2
  64. package/src/domain/utils/defaultCodec.js +9 -2
  65. package/src/domain/utils/defaultCrypto.js +36 -0
  66. package/src/domain/utils/parseCursorBlob.js +51 -0
  67. package/src/domain/utils/roaring.js +5 -5
  68. package/src/domain/utils/seekCacheKey.js +32 -0
  69. package/src/domain/warp/PatchSession.js +3 -3
  70. package/src/domain/warp/Writer.js +2 -2
  71. package/src/infrastructure/adapters/BunHttpAdapter.js +21 -8
  72. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +311 -0
  73. package/src/infrastructure/adapters/ClockAdapter.js +2 -2
  74. package/src/infrastructure/adapters/DenoHttpAdapter.js +22 -9
  75. package/src/infrastructure/adapters/GitGraphAdapter.js +16 -27
  76. package/src/infrastructure/adapters/NodeCryptoAdapter.js +16 -3
  77. package/src/infrastructure/adapters/NodeHttpAdapter.js +33 -11
  78. package/src/infrastructure/adapters/WebCryptoAdapter.js +21 -11
  79. package/src/infrastructure/codecs/CborCodec.js +16 -8
  80. package/src/ports/BlobPort.js +2 -2
  81. package/src/ports/CodecPort.js +2 -2
  82. package/src/ports/CommitPort.js +8 -21
  83. package/src/ports/ConfigPort.js +3 -3
  84. package/src/ports/CryptoPort.js +7 -7
  85. package/src/ports/GraphPersistencePort.js +12 -14
  86. package/src/ports/HttpServerPort.js +1 -5
  87. package/src/ports/IndexStoragePort.js +1 -0
  88. package/src/ports/LoggerPort.js +9 -9
  89. package/src/ports/RefPort.js +5 -5
  90. package/src/ports/SeekCachePort.js +73 -0
  91. package/src/ports/TreePort.js +3 -3
  92. package/src/visualization/layouts/converters.js +14 -7
  93. package/src/visualization/layouts/elkAdapter.js +24 -11
  94. package/src/visualization/layouts/elkLayout.js +23 -7
  95. package/src/visualization/layouts/index.js +3 -3
  96. package/src/visualization/renderers/ascii/check.js +30 -17
  97. package/src/visualization/renderers/ascii/graph.js +122 -16
  98. package/src/visualization/renderers/ascii/history.js +29 -90
  99. package/src/visualization/renderers/ascii/index.js +1 -1
  100. package/src/visualization/renderers/ascii/info.js +9 -7
  101. package/src/visualization/renderers/ascii/materialize.js +20 -16
  102. package/src/visualization/renderers/ascii/opSummary.js +81 -0
  103. package/src/visualization/renderers/ascii/path.js +1 -1
  104. package/src/visualization/renderers/ascii/seek.js +344 -0
  105. package/src/visualization/renderers/ascii/table.js +1 -1
  106. package/src/visualization/renderers/svg/index.js +5 -1
@@ -37,7 +37,7 @@ export default class DagTopology {
37
37
  * @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger instance
38
38
  * @param {import('./DagTraversal.js').default} [options.traversal] - Traversal service for ancestor enumeration
39
39
  */
40
- constructor({ indexReader, logger = nullLogger, traversal } = {}) {
40
+ constructor(/** @type {{ indexReader: import('./BitmapIndexReader.js').default, logger?: import('../../ports/LoggerPort.js').default, traversal?: import('./DagTraversal.js').default }} */ { indexReader, logger = nullLogger, traversal } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
41
41
  if (!indexReader) {
42
42
  throw new Error('DagTopology requires an indexReader');
43
43
  }
@@ -76,9 +76,10 @@ export default class DagTopology {
76
76
  */
77
77
  async commonAncestors({ shas, maxResults = 100, maxDepth = DEFAULT_MAX_DEPTH, signal }) {
78
78
  if (shas.length === 0) { return []; }
79
+ const traversal = /** @type {import('./DagTraversal.js').default} */ (this._traversal);
79
80
  if (shas.length === 1) {
80
81
  const ancestors = [];
81
- for await (const node of this._traversal.ancestors({ sha: shas[0], maxNodes: maxResults, maxDepth, signal })) {
82
+ for await (const node of traversal.ancestors({ sha: shas[0], maxNodes: maxResults, maxDepth, signal })) {
82
83
  ancestors.push(node.sha);
83
84
  }
84
85
  return ancestors;
@@ -92,7 +93,7 @@ export default class DagTopology {
92
93
  for (const sha of shas) {
93
94
  checkAborted(signal, 'commonAncestors');
94
95
  const visited = new Set();
95
- for await (const node of this._traversal.ancestors({ sha, maxDepth, signal })) {
96
+ for await (const node of traversal.ancestors({ sha, maxDepth, signal })) {
96
97
  if (!visited.has(node.sha)) {
97
98
  visited.add(node.sha);
98
99
  ancestorCounts.set(node.sha, (ancestorCounts.get(node.sha) || 0) + 1);
@@ -149,7 +150,7 @@ export default class DagTopology {
149
150
  checkAborted(signal, 'topologicalSort');
150
151
  }
151
152
 
152
- const sha = queue.shift();
153
+ const sha = /** @type {string} */ (queue.shift());
153
154
  const neighbors = await this._getNeighbors(sha, direction);
154
155
  edges.set(sha, neighbors);
155
156
 
@@ -182,7 +183,7 @@ export default class DagTopology {
182
183
  checkAborted(signal, 'topologicalSort');
183
184
  }
184
185
 
185
- const sha = ready.shift();
186
+ const sha = /** @type {string} */ (ready.shift());
186
187
  const depth = depthMap.get(sha) || 0;
187
188
 
188
189
  nodesYielded++;
@@ -190,7 +191,7 @@ export default class DagTopology {
190
191
 
191
192
  const neighbors = edges.get(sha) || [];
192
193
  for (const neighbor of neighbors) {
193
- const newDegree = inDegree.get(neighbor) - 1;
194
+ const newDegree = /** @type {number} */ (inDegree.get(neighbor)) - 1;
194
195
  inDegree.set(neighbor, newDegree);
195
196
 
196
197
  if (!depthMap.has(neighbor)) {
@@ -43,7 +43,7 @@ export default class DagTraversal {
43
43
  * @param {import('./BitmapIndexReader.js').default} options.indexReader - Index reader for O(1) lookups
44
44
  * @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger instance
45
45
  */
46
- constructor({ indexReader, logger = nullLogger } = {}) {
46
+ constructor(/** @type {{ indexReader: import('./BitmapIndexReader.js').default, logger?: import('../../ports/LoggerPort.js').default }} */ { indexReader, logger = nullLogger } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
47
47
  if (!indexReader) {
48
48
  throw new Error('DagTraversal requires an indexReader');
49
49
  }
@@ -89,6 +89,7 @@ export default class DagTraversal {
89
89
  signal,
90
90
  }) {
91
91
  const visited = new Set();
92
+ /** @type {TraversalNode[]} */
92
93
  const queue = [{ sha: start, depth: 0, parent: null }];
93
94
  let nodesYielded = 0;
94
95
 
@@ -99,7 +100,7 @@ export default class DagTraversal {
99
100
  checkAborted(signal, 'bfs');
100
101
  }
101
102
 
102
- const current = queue.shift();
103
+ const current = /** @type {TraversalNode} */ (queue.shift());
103
104
 
104
105
  if (visited.has(current.sha)) { continue; }
105
106
  if (current.depth > maxDepth) { continue; }
@@ -142,6 +143,7 @@ export default class DagTraversal {
142
143
  signal,
143
144
  }) {
144
145
  const visited = new Set();
146
+ /** @type {TraversalNode[]} */
145
147
  const stack = [{ sha: start, depth: 0, parent: null }];
146
148
  let nodesYielded = 0;
147
149
 
@@ -152,7 +154,7 @@ export default class DagTraversal {
152
154
  checkAborted(signal, 'dfs');
153
155
  }
154
156
 
155
- const current = stack.pop();
157
+ const current = /** @type {TraversalNode} */ (stack.pop());
156
158
 
157
159
  if (visited.has(current.sha)) { continue; }
158
160
  if (current.depth > maxDepth) { continue; }
@@ -50,12 +50,13 @@ export function getWriters(frontier) {
50
50
  * Keys are sorted for determinism.
51
51
  * @param {Frontier} frontier
52
52
  * @param {Object} [options]
53
- * @param {import('../../ports/CodecPort.js').default} options.codec - Codec for serialization
54
- * @returns {Buffer}
53
+ * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
54
+ * @returns {Buffer|Uint8Array}
55
55
  */
56
- export function serializeFrontier(frontier, { codec } = {}) {
56
+ export function serializeFrontier(frontier, { codec } = /** @type {{codec?: import('../../ports/CodecPort.js').default}} */ ({})) {
57
57
  const c = codec || defaultCodec;
58
58
  // Convert Map to sorted object for deterministic encoding
59
+ /** @type {Record<string, string|undefined>} */
59
60
  const obj = {};
60
61
  const sortedKeys = Array.from(frontier.keys()).sort();
61
62
  for (const key of sortedKeys) {
@@ -68,12 +69,12 @@ export function serializeFrontier(frontier, { codec } = {}) {
68
69
  * Deserializes frontier from CBOR bytes.
69
70
  * @param {Buffer} buffer
70
71
  * @param {Object} [options]
71
- * @param {import('../../ports/CodecPort.js').default} options.codec - Codec for deserialization
72
+ * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
72
73
  * @returns {Frontier}
73
74
  */
74
- export function deserializeFrontier(buffer, { codec } = {}) {
75
+ export function deserializeFrontier(buffer, { codec } = /** @type {{codec?: import('../../ports/CodecPort.js').default}} */ ({})) {
75
76
  const c = codec || defaultCodec;
76
- const obj = c.decode(buffer);
77
+ const obj = /** @type {Record<string, string>} */ (c.decode(buffer));
77
78
  const frontier = new Map();
78
79
  for (const [writerId, patchSha] of Object.entries(obj)) {
79
80
  frontier.set(writerId, patchSha);
@@ -46,7 +46,7 @@ export default class HealthCheckService {
46
46
  /**
47
47
  * Creates a HealthCheckService instance.
48
48
  * @param {Object} options
49
- * @param {import('../../ports/GraphPersistencePort.js').default} options.persistence - Persistence port for repository checks
49
+ * @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default} options.persistence - Persistence port for repository checks
50
50
  * @param {import('../../ports/ClockPort.js').default} options.clock - Clock port for timing operations
51
51
  * @param {number} [options.cacheTtlMs=5000] - How long to cache health results in milliseconds
52
52
  * @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger for structured logging
@@ -132,22 +132,23 @@ export default class HealthCheckService {
132
132
  */
133
133
  async getHealth() {
134
134
  const { value, cachedAt, fromCache } = await this._healthCache.getWithMetadata();
135
+ const result = /** @type {HealthResult} */ (value);
135
136
 
136
137
  if (cachedAt) {
137
- return { ...value, cachedAt };
138
+ return { ...result, cachedAt };
138
139
  }
139
140
 
140
141
  // Log only for fresh computations
141
142
  if (!fromCache) {
142
143
  this._logger.debug('Health check completed', {
143
144
  operation: 'getHealth',
144
- status: value.status,
145
- repositoryStatus: value.components.repository.status,
146
- indexStatus: value.components.index.status,
145
+ status: result.status,
146
+ repositoryStatus: result.components.repository.status,
147
+ indexStatus: result.components.index.status,
147
148
  });
148
149
  }
149
150
 
150
- return value;
151
+ return result;
151
152
  }
152
153
 
153
154
  /**
@@ -184,16 +185,16 @@ export default class HealthCheckService {
184
185
  try {
185
186
  const pingResult = await this._persistence.ping();
186
187
  return {
187
- status: pingResult.ok ? HealthStatus.HEALTHY : HealthStatus.UNHEALTHY,
188
+ status: /** @type {'healthy'|'unhealthy'} */ (pingResult.ok ? HealthStatus.HEALTHY : HealthStatus.UNHEALTHY),
188
189
  latencyMs: Math.round(pingResult.latencyMs * 100) / 100, // Round to 2 decimal places
189
190
  };
190
191
  } catch (err) {
191
192
  this._logger.warn('Repository ping failed', {
192
193
  operation: 'checkRepository',
193
- error: err.message,
194
+ error: /** @type {any} */ (err).message, // TODO(ts-cleanup): type error
194
195
  });
195
196
  return {
196
- status: HealthStatus.UNHEALTHY,
197
+ status: /** @type {'healthy'|'unhealthy'} */ (HealthStatus.UNHEALTHY),
197
198
  latencyMs: 0,
198
199
  };
199
200
  }
@@ -207,7 +208,7 @@ export default class HealthCheckService {
207
208
  _checkIndex() {
208
209
  if (!this._indexReader) {
209
210
  return {
210
- status: HealthStatus.DEGRADED,
211
+ status: /** @type {'healthy'|'degraded'|'unhealthy'} */ (HealthStatus.DEGRADED),
211
212
  loaded: false,
212
213
  };
213
214
  }
@@ -216,7 +217,7 @@ export default class HealthCheckService {
216
217
  const shardCount = this._indexReader.shardOids?.size ?? 0;
217
218
 
218
219
  return {
219
- status: HealthStatus.HEALTHY,
220
+ status: /** @type {'healthy'|'degraded'|'unhealthy'} */ (HealthStatus.HEALTHY),
220
221
  loaded: true,
221
222
  shardCount,
222
223
  };
@@ -232,15 +233,15 @@ export default class HealthCheckService {
232
233
  _computeOverallStatus(repositoryHealth, indexHealth) {
233
234
  // If repository is unhealthy, overall is unhealthy
234
235
  if (repositoryHealth.status === HealthStatus.UNHEALTHY) {
235
- return HealthStatus.UNHEALTHY;
236
+ return /** @type {'healthy'|'degraded'|'unhealthy'} */ (HealthStatus.UNHEALTHY);
236
237
  }
237
238
 
238
239
  // If index is degraded (not loaded), overall is degraded
239
240
  if (indexHealth.status === HealthStatus.DEGRADED) {
240
- return HealthStatus.DEGRADED;
241
+ return /** @type {'healthy'|'degraded'|'unhealthy'} */ (HealthStatus.DEGRADED);
241
242
  }
242
243
 
243
244
  // All components healthy
244
- return HealthStatus.HEALTHY;
245
+ return /** @type {'healthy'|'degraded'|'unhealthy'} */ (HealthStatus.HEALTHY);
245
246
  }
246
247
  }
@@ -7,6 +7,21 @@
7
7
  * @module domain/services/HookInstaller
8
8
  */
9
9
 
10
+ /**
11
+ * @typedef {Object} FsAdapter
12
+ * @property {(path: string, content: string | Buffer, options?: Object) => void} writeFileSync
13
+ * @property {(path: string, mode: number) => void} chmodSync
14
+ * @property {(path: string, encoding?: string) => string} readFileSync
15
+ * @property {(path: string) => boolean} existsSync
16
+ * @property {(path: string, options?: Object) => void} mkdirSync
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} PathUtils
21
+ * @property {(...segments: string[]) => string} join
22
+ * @property {(...segments: string[]) => string} resolve
23
+ */
24
+
10
25
  const DELIMITER_START_PREFIX = '# --- @git-stunts/git-warp post-merge hook';
11
26
  const DELIMITER_END = '# --- end @git-stunts/git-warp ---';
12
27
  const VERSION_MARKER_PREFIX = '# warp-hook-version:';
@@ -59,17 +74,22 @@ export class HookInstaller {
59
74
  * Creates a new HookInstaller.
60
75
  *
61
76
  * @param {Object} deps - Injected dependencies
62
- * @param {Object} deps.fs - Filesystem adapter with methods: readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync
77
+ * @param {FsAdapter} deps.fs - Filesystem adapter with methods: readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync
63
78
  * @param {(repoPath: string, key: string) => string|null} deps.execGitConfig - Function to read git config values
64
79
  * @param {string} deps.version - Package version
65
80
  * @param {string} deps.templateDir - Directory containing hook templates
66
- * @param {{ join: (...segments: string[]) => string, resolve: (...segments: string[]) => string }} deps.path - Path utilities (join and resolve)
81
+ * @param {PathUtils} deps.path - Path utilities (join and resolve)
67
82
  */
68
- constructor({ fs, execGitConfig, version, templateDir, path } = {}) {
83
+ constructor({ fs, execGitConfig, version, templateDir, path } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
84
+ /** @type {FsAdapter} */
69
85
  this._fs = fs;
86
+ /** @type {(repoPath: string, key: string) => string|null} */
70
87
  this._execGitConfig = execGitConfig;
88
+ /** @type {string} */
71
89
  this._templateDir = templateDir;
90
+ /** @type {string} */
72
91
  this._version = version;
92
+ /** @type {PathUtils} */
73
93
  this._path = path;
74
94
  }
75
95
 
@@ -134,7 +154,11 @@ export class HookInstaller {
134
154
  throw new Error(`Unknown install strategy: ${strategy}`);
135
155
  }
136
156
 
137
- /** @private */
157
+ /**
158
+ * @param {string} hookPath
159
+ * @param {string} content
160
+ * @private
161
+ */
138
162
  _freshInstall(hookPath, content) {
139
163
  this._fs.writeFileSync(hookPath, content, { mode: 0o755 });
140
164
  this._fs.chmodSync(hookPath, 0o755);
@@ -145,13 +169,17 @@ export class HookInstaller {
145
169
  };
146
170
  }
147
171
 
148
- /** @private */
172
+ /**
173
+ * @param {string} hookPath
174
+ * @param {string} stamped
175
+ * @private
176
+ */
149
177
  _upgradeInstall(hookPath, stamped) {
150
178
  const existing = this._readFile(hookPath);
151
179
  const classification = classifyExistingHook(existing);
152
180
 
153
181
  if (classification.appended) {
154
- const updated = replaceDelimitedSection(existing, stamped);
182
+ const updated = replaceDelimitedSection(/** @type {string} */ (existing), stamped);
155
183
  // If delimiters were corrupted, replaceDelimitedSection returns unchanged content — fall back to overwrite
156
184
  if (updated === existing) {
157
185
  this._fs.writeFileSync(hookPath, stamped, { mode: 0o755 });
@@ -170,7 +198,11 @@ export class HookInstaller {
170
198
  };
171
199
  }
172
200
 
173
- /** @private */
201
+ /**
202
+ * @param {string} hookPath
203
+ * @param {string} stamped
204
+ * @private
205
+ */
174
206
  _appendInstall(hookPath, stamped) {
175
207
  const existing = this._readFile(hookPath) || '';
176
208
  const body = stripShebang(stamped);
@@ -184,7 +216,11 @@ export class HookInstaller {
184
216
  };
185
217
  }
186
218
 
187
- /** @private */
219
+ /**
220
+ * @param {string} hookPath
221
+ * @param {string} stamped
222
+ * @private
223
+ */
188
224
  _replaceInstall(hookPath, stamped) {
189
225
  const existing = this._readFile(hookPath);
190
226
  let backupPath;
@@ -210,12 +246,18 @@ export class HookInstaller {
210
246
  return this._fs.readFileSync(templatePath, 'utf8');
211
247
  }
212
248
 
213
- /** @private */
249
+ /**
250
+ * @param {string} template
251
+ * @private
252
+ */
214
253
  _stampVersion(template) {
215
254
  return template.replaceAll(VERSION_PLACEHOLDER, this._version);
216
255
  }
217
256
 
218
- /** @private */
257
+ /**
258
+ * @param {string} repoPath
259
+ * @private
260
+ */
219
261
  _resolveHooksDir(repoPath) {
220
262
  const customPath = this._execGitConfig(repoPath, 'core.hooksPath');
221
263
  if (customPath) {
@@ -230,12 +272,18 @@ export class HookInstaller {
230
272
  return this._path.join(repoPath, '.git', 'hooks');
231
273
  }
232
274
 
233
- /** @private */
275
+ /**
276
+ * @param {string} repoPath
277
+ * @private
278
+ */
234
279
  _resolveHookPath(repoPath) {
235
280
  return this._path.join(this._resolveHooksDir(repoPath), 'post-merge');
236
281
  }
237
282
 
238
- /** @private */
283
+ /**
284
+ * @param {string} filePath
285
+ * @private
286
+ */
239
287
  _readFile(filePath) {
240
288
  try {
241
289
  return this._fs.readFileSync(filePath, 'utf8');
@@ -244,7 +292,10 @@ export class HookInstaller {
244
292
  }
245
293
  }
246
294
 
247
- /** @private */
295
+ /**
296
+ * @param {string} dirPath
297
+ * @private
298
+ */
248
299
  _ensureDir(dirPath) {
249
300
  if (!this._fs.existsSync(dirPath)) {
250
301
  this._fs.mkdirSync(dirPath, { recursive: true });
@@ -22,9 +22,10 @@ function canonicalizeJson(value) {
22
22
  return value.map(canonicalizeJson);
23
23
  }
24
24
  if (value && typeof value === 'object') {
25
+ /** @type {{ [x: string]: * }} */
25
26
  const sorted = {};
26
27
  for (const key of Object.keys(value).sort()) {
27
- sorted[key] = canonicalizeJson(value[key]);
28
+ sorted[key] = canonicalizeJson(/** @type {{ [x: string]: * }} */ (value)[key]);
28
29
  }
29
30
  return sorted;
30
31
  }
@@ -97,7 +98,7 @@ function isValidSyncRequest(parsed) {
97
98
  * Checks the content-type header. Returns an error response if the
98
99
  * content type is present but not application/json, otherwise null.
99
100
  *
100
- * @param {Object} headers - Request headers
101
+ * @param {{ [x: string]: string }} headers - Request headers
101
102
  * @returns {{ status: number, headers: Object, body: string }|null}
102
103
  * @private
103
104
  */
@@ -113,7 +114,7 @@ function checkContentType(headers) {
113
114
  * Parses the request URL and validates the path and method.
114
115
  * Returns an error response on failure, or null if valid.
115
116
  *
116
- * @param {{ method: string, url: string, headers: Object }} request
117
+ * @param {{ method: string, url: string, headers: { [x: string]: string } }} request
117
118
  * @param {string} expectedPath
118
119
  * @param {string} defaultHost
119
120
  * @returns {{ status: number, headers: Object, body: string }|null}
@@ -171,12 +172,12 @@ export default class HttpSyncServer {
171
172
  /**
172
173
  * @param {Object} options
173
174
  * @param {import('../../ports/HttpServerPort.js').default} options.httpPort - HTTP server port abstraction
174
- * @param {Object} options.graph - WarpGraph instance (must expose processSyncRequest)
175
+ * @param {{ processSyncRequest: (request: *) => Promise<*> }} options.graph - WarpGraph instance (must expose processSyncRequest)
175
176
  * @param {string} [options.path='/sync'] - URL path to handle sync requests on
176
177
  * @param {string} [options.host='127.0.0.1'] - Host to bind
177
178
  * @param {number} [options.maxRequestBytes=4194304] - Maximum request body size in bytes
178
179
  */
179
- constructor({ httpPort, graph, path = '/sync', host = '127.0.0.1', maxRequestBytes = DEFAULT_MAX_REQUEST_BYTES } = {}) {
180
+ constructor({ httpPort, graph, path = '/sync', host = '127.0.0.1', maxRequestBytes = DEFAULT_MAX_REQUEST_BYTES } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
180
181
  this._httpPort = httpPort;
181
182
  this._graph = graph;
182
183
  this._path = path && path.startsWith('/') ? path : `/${path || 'sync'}`;
@@ -188,7 +189,7 @@ export default class HttpSyncServer {
188
189
  /**
189
190
  * Handles an incoming HTTP request through the port abstraction.
190
191
  *
191
- * @param {{ method: string, url: string, headers: Object, body: Buffer|undefined }} request
192
+ * @param {{ method: string, url: string, headers: { [x: string]: string }, body: Buffer|undefined }} request
192
193
  * @returns {Promise<{ status: number, headers: Object, body: string }>}
193
194
  * @private
194
195
  */
@@ -212,7 +213,7 @@ export default class HttpSyncServer {
212
213
  const response = await this._graph.processSyncRequest(parsed);
213
214
  return jsonResponse(response);
214
215
  } catch (err) {
215
- return errorResponse(500, err?.message || 'Sync failed');
216
+ return errorResponse(500, /** @type {any} */ (err)?.message || 'Sync failed'); // TODO(ts-cleanup): type error
216
217
  }
217
218
  }
218
219
 
@@ -228,18 +229,18 @@ export default class HttpSyncServer {
228
229
  throw new Error('listen() requires a numeric port');
229
230
  }
230
231
 
231
- const server = this._httpPort.createServer((request) => this._handleRequest(request));
232
+ const server = this._httpPort.createServer((/** @type {*} */ request) => this._handleRequest(request)); // TODO(ts-cleanup): type http callback
232
233
  this._server = server;
233
234
 
234
- await new Promise((resolve, reject) => {
235
- server.listen(port, this._host, (err) => {
235
+ await /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
236
+ server.listen(port, this._host, (/** @type {*} */ err) => { // TODO(ts-cleanup): type http callback
236
237
  if (err) {
237
238
  reject(err);
238
239
  } else {
239
240
  resolve();
240
241
  }
241
242
  });
242
- });
243
+ }));
243
244
 
244
245
  const address = server.address();
245
246
  const actualPort = typeof address === 'object' && address ? address.port : port;
@@ -248,15 +249,15 @@ export default class HttpSyncServer {
248
249
  return {
249
250
  url,
250
251
  close: () =>
251
- new Promise((resolve, reject) => {
252
- server.close((err) => {
252
+ /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
253
+ server.close((/** @type {*} */ err) => { // TODO(ts-cleanup): type http callback
253
254
  if (err) {
254
255
  reject(err);
255
256
  } else {
256
257
  resolve();
257
258
  }
258
259
  });
259
- }),
260
+ })),
260
261
  };
261
262
  }
262
263
  }
@@ -40,17 +40,17 @@ export default class IndexRebuildService {
40
40
  * Creates an IndexRebuildService instance.
41
41
  *
42
42
  * @param {Object} options - Configuration options
43
- * @param {Object} options.graphService - Graph service providing node iteration.
44
- * Must implement `iterateNodes({ ref, limit }) => AsyncGenerator<GraphNode>`.
43
+ * @param {{ iterateNodes: (opts: { ref: string, limit: number }) => AsyncIterable<{ sha: string, parents: string[] }> }} options.graphService - Graph service providing node iteration.
45
44
  * @param {import('../../ports/IndexStoragePort.js').default} options.storage - Storage adapter
46
45
  * for persisting index blobs and trees. Typically GitGraphAdapter.
47
46
  * @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger for
48
47
  * structured logging. Defaults to null logger (no logging).
48
+ * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
49
49
  * @param {import('../../ports/CryptoPort.js').default} [options.crypto] - Crypto adapter for checksums
50
50
  * @throws {Error} If graphService is not provided
51
51
  * @throws {Error} If storage adapter is not provided
52
52
  */
53
- constructor({ graphService, storage, logger = nullLogger, codec, crypto }) {
53
+ constructor({ graphService, storage, logger = nullLogger, codec, crypto } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
54
54
  if (!graphService) {
55
55
  throw new Error('IndexRebuildService requires a graphService');
56
56
  }
@@ -156,7 +156,7 @@ export default class IndexRebuildService {
156
156
  operation: 'rebuild',
157
157
  ref,
158
158
  mode,
159
- error: err.message,
159
+ error: /** @type {any} */ (err).message, // TODO(ts-cleanup): type error
160
160
  durationMs,
161
161
  });
162
162
  throw err;
@@ -247,12 +247,12 @@ export default class IndexRebuildService {
247
247
  * @private
248
248
  */
249
249
  async _rebuildStreaming(ref, { limit, maxMemoryBytes, onFlush, onProgress, signal, frontier }) {
250
- const builder = new StreamingBitmapIndexBuilder({
250
+ const builder = new StreamingBitmapIndexBuilder(/** @type {*} */ ({ // TODO(ts-cleanup): narrow port type
251
251
  storage: this.storage,
252
252
  maxMemoryBytes,
253
253
  onFlush,
254
254
  crypto: this._crypto,
255
- });
255
+ }));
256
256
 
257
257
  let processedNodes = 0;
258
258
 
@@ -266,7 +266,7 @@ export default class IndexRebuildService {
266
266
  if (processedNodes % 10000 === 0) {
267
267
  checkAborted(signal, 'rebuild');
268
268
  if (onProgress) {
269
- const stats = builder.getMemoryStats();
269
+ const stats = /** @type {any} */ (builder).getMemoryStats(); // TODO(ts-cleanup): narrow port type
270
270
  onProgress({
271
271
  processedNodes,
272
272
  currentMemoryBytes: stats.estimatedBitmapBytes,
@@ -275,7 +275,7 @@ export default class IndexRebuildService {
275
275
  }
276
276
  }
277
277
 
278
- return await builder.finalize({ signal, frontier });
278
+ return await /** @type {any} */ (builder).finalize({ signal, frontier }); // TODO(ts-cleanup): narrow port type
279
279
  }
280
280
 
281
281
  /**
@@ -302,10 +302,10 @@ export default class IndexRebuildService {
302
302
  const treeStructure = await builder.serialize({ frontier });
303
303
  const flatEntries = [];
304
304
  for (const [path, buffer] of Object.entries(treeStructure)) {
305
- const oid = await this.storage.writeBlob(buffer);
305
+ const oid = await /** @type {import('../../ports/BlobPort.js').default} */ (/** @type {unknown} */ (this.storage)).writeBlob(buffer);
306
306
  flatEntries.push(`100644 blob ${oid}\t${path}`);
307
307
  }
308
- return await this.storage.writeTree(flatEntries);
308
+ return await /** @type {import('../../ports/TreePort.js').default} */ (/** @type {unknown} */ (this.storage)).writeTree(flatEntries);
309
309
  }
310
310
 
311
311
  /**
@@ -384,12 +384,12 @@ export default class IndexRebuildService {
384
384
  }
385
385
 
386
386
  const startTime = performance.now();
387
- const shardOids = await this.storage.readTreeOids(treeOid);
387
+ const shardOids = await /** @type {import('../../ports/TreePort.js').default} */ (/** @type {unknown} */ (this.storage)).readTreeOids(treeOid);
388
388
  const shardCount = Object.keys(shardOids).length;
389
389
 
390
390
  // Staleness check
391
391
  if (currentFrontier) {
392
- const indexFrontier = await loadIndexFrontier(shardOids, this.storage, { codec: this._codec });
392
+ const indexFrontier = await loadIndexFrontier(shardOids, /** @type {*} */ (this.storage), { codec: this._codec }); // TODO(ts-cleanup): narrow port type
393
393
  if (indexFrontier) {
394
394
  const result = checkStaleness(indexFrontier, currentFrontier);
395
395
  if (result.stale) {
@@ -5,7 +5,11 @@
5
5
 
6
6
  import defaultCodec from '../utils/defaultCodec.js';
7
7
 
8
- /** @private */
8
+ /**
9
+ * @param {*} envelope
10
+ * @param {string} label
11
+ * @private
12
+ */
9
13
  function validateEnvelope(envelope, label) {
10
14
  if (!envelope || typeof envelope !== 'object' || !envelope.frontier || typeof envelope.frontier !== 'object') {
11
15
  throw new Error(`invalid frontier envelope for ${label}`);
@@ -16,17 +20,17 @@ function validateEnvelope(envelope, label) {
16
20
  * Loads the frontier from an index tree's shard OIDs.
17
21
  *
18
22
  * @param {Record<string, string>} shardOids - Map of path → blob OID from readTreeOids
19
- * @param {import('../../ports/IndexStoragePort.js').default} storage - Storage adapter
23
+ * @param {import('../../ports/IndexStoragePort.js').default & import('../../ports/BlobPort.js').default} storage - Storage adapter
20
24
  * @param {Object} [options]
21
25
  * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
22
26
  * @returns {Promise<Map<string, string>|null>} Frontier map, or null if not present (legacy index)
23
27
  */
24
- export async function loadIndexFrontier(shardOids, storage, { codec } = {}) {
28
+ export async function loadIndexFrontier(shardOids, storage, { codec } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
25
29
  const c = codec || defaultCodec;
26
30
  const cborOid = shardOids['frontier.cbor'];
27
31
  if (cborOid) {
28
32
  const buffer = await storage.readBlob(cborOid);
29
- const envelope = c.decode(buffer);
33
+ const envelope = /** @type {{ frontier: Record<string, string> }} */ (c.decode(buffer));
30
34
  validateEnvelope(envelope, 'frontier.cbor');
31
35
  return new Map(Object.entries(envelope.frontier));
32
36
  }
@@ -34,7 +38,7 @@ export async function loadIndexFrontier(shardOids, storage, { codec } = {}) {
34
38
  const jsonOid = shardOids['frontier.json'];
35
39
  if (jsonOid) {
36
40
  const buffer = await storage.readBlob(jsonOid);
37
- const envelope = JSON.parse(buffer.toString('utf-8'));
41
+ const envelope = /** @type {{ frontier: Record<string, string> }} */ (JSON.parse(buffer.toString('utf-8')));
38
42
  validateEnvelope(envelope, 'frontier.json');
39
43
  return new Map(Object.entries(envelope.frontier));
40
44
  }
@@ -51,7 +55,10 @@ export async function loadIndexFrontier(shardOids, storage, { codec } = {}) {
51
55
  * @property {string[]} removedWriters - Writers in index but not current
52
56
  */
53
57
 
54
- /** @private */
58
+ /**
59
+ * @param {{ stale: boolean, advancedWriters: string[], newWriters: string[], removedWriters: string[] }} opts
60
+ * @private
61
+ */
55
62
  function buildReason({ stale, advancedWriters, newWriters, removedWriters }) {
56
63
  if (!stale) {
57
64
  return 'index is current';