isomorphic-jj 1.4.3 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,8 +8,8 @@ import { JJError } from '../utils/errors.js';
8
8
 
9
9
  export class BackgroundOps {
10
10
  /**
11
- * @param {Object} jj - JJ instance
12
- * @param {Object} fs - Filesystem implementation
11
+ * @param {any} jj - JJ instance
12
+ * @param {any} fs - Filesystem implementation
13
13
  * @param {string} dir - Repository directory
14
14
  */
15
15
  constructor(jj, fs, dir) {
@@ -63,7 +63,7 @@ export class BackgroundOps {
63
63
  *
64
64
  * @param {string} path - Path to watch
65
65
  * @param {Function} callback - Callback function(event, filename)
66
- * @returns {string} Watcher ID
66
+ * @returns {Promise<string>} Watcher ID
67
67
  */
68
68
  async watch(path, callback) {
69
69
  if (!this.running) {
@@ -81,13 +81,15 @@ export class BackgroundOps {
81
81
  }
82
82
 
83
83
  // Browser: use FileSystem Access API observeDirectory if available
84
- if (typeof FileSystemObserver !== 'undefined') {
84
+ if (typeof (/** @type {any} */ (globalThis).FileSystemObserver) !== 'undefined') {
85
85
  const watcherId = `watcher-${this.watchers.size}`;
86
- const observer = new FileSystemObserver(async (records) => {
87
- for (const record of records) {
88
- callback(record.type, record.relativePathComponents.join('/'));
86
+ const observer = new /** @type {any} */ (globalThis).FileSystemObserver(
87
+ async (/** @type {any} */ records) => {
88
+ for (const record of records) {
89
+ callback(record.type, record.relativePathComponents.join('/'));
90
+ }
89
91
  }
90
- });
92
+ );
91
93
 
92
94
  // Note: This API is experimental and may not be available
93
95
  this.watchers.set(watcherId, observer);
@@ -130,11 +132,12 @@ export class BackgroundOps {
130
132
  * @param {Function} operation - Async operation to run
131
133
  * @param {Object} opts - Options
132
134
  * @param {string} [opts.description] - Operation description
133
- * @returns {Promise<Object>} Operation result
135
+ * @returns {Promise<{id: string, promise: Promise<any>}>} Operation handle
134
136
  */
135
137
  async queue(operation, opts = {}) {
136
138
  const operationId = `op-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
137
139
 
140
+ /** @type {Record<string, any>} */
138
141
  const op = {
139
142
  id: operationId,
140
143
  description: opts.description || 'background operation',
@@ -190,10 +193,10 @@ export class BackgroundOps {
190
193
  let ops = Array.from(this.operations.values());
191
194
 
192
195
  if (opts.status) {
193
- ops = ops.filter(op => op.status === opts.status);
196
+ ops = ops.filter((op) => op.status === opts.status);
194
197
  }
195
198
 
196
- return ops.map(op => ({
199
+ return ops.map((op) => ({
197
200
  id: op.id,
198
201
  description: op.description,
199
202
  status: op.status,
@@ -212,42 +215,56 @@ export class BackgroundOps {
212
215
  async enableAutoSnapshot(opts = {}) {
213
216
  const debounceMs = opts.debounceMs || 1000;
214
217
 
215
- const watcherId = await this.watch(this.dir, (event, filename) => {
216
- if (filename && filename.startsWith('.jj/')) {
217
- return; // Ignore .jj directory changes
218
- }
218
+ const watcherId = await this.watch(
219
+ this.dir,
220
+ (/** @type {any} */ event, /** @type {any} */ filename) => {
221
+ if (filename && filename.startsWith('.jj/')) {
222
+ return; // Ignore .jj directory changes
223
+ }
219
224
 
220
- if (filename && filename.startsWith('.git/')) {
221
- return; // Ignore .git directory changes
222
- }
225
+ if (filename && filename.startsWith('.git/')) {
226
+ return; // Ignore .git directory changes
227
+ }
223
228
 
224
- // Debounce: wait for changes to settle
225
- const existingTimer = this.timers.get(watcherId);
226
- if (existingTimer) {
227
- clearTimeout(existingTimer);
228
- }
229
+ // Debounce: wait for changes to settle
230
+ const existingTimer = this.timers.get(watcherId);
231
+ if (existingTimer) {
232
+ clearTimeout(existingTimer);
233
+ }
229
234
 
230
- const timer = setTimeout(async () => {
231
- try {
232
- // Create automatic snapshot
233
- await this.queue(async () => {
234
- await this.jj.describe({ message: `Auto-snapshot: ${filename || 'multiple files'} changed` });
235
- }, { description: 'auto-snapshot' });
236
- } catch (error) {
237
- console.error('Auto-snapshot failed:', error);
238
- } finally {
239
- // Clean up timer reference
240
- this.timers.delete(watcherId);
235
+ const timer = setTimeout(async () => {
236
+ try {
237
+ // Create automatic snapshot. queue() resolves as soon as the
238
+ // operation is *enqueued*, not once it finishes — it returns a
239
+ // `{ id, promise }` handle. Awaiting only queue() (and not the
240
+ // returned `promise`) would let a later describe() rejection
241
+ // become an unhandled promise rejection instead of landing in
242
+ // this catch block, so the handle's promise must be awaited too.
243
+ const { promise } = await this.queue(
244
+ async () => {
245
+ await this.jj.describe({
246
+ message: `Auto-snapshot: ${filename || 'multiple files'} changed`,
247
+ });
248
+ },
249
+ { description: 'auto-snapshot' }
250
+ );
251
+ await promise;
252
+ } catch (error) {
253
+ console.error('Auto-snapshot failed:', error);
254
+ } finally {
255
+ // Clean up timer reference
256
+ this.timers.delete(watcherId);
257
+ }
258
+ }, debounceMs);
259
+
260
+ // Use unref() to allow process to exit if this is the only pending timer
261
+ if (timer.unref) {
262
+ timer.unref();
241
263
  }
242
- }, debounceMs);
243
264
 
244
- // Use unref() to allow process to exit if this is the only pending timer
245
- if (timer.unref) {
246
- timer.unref();
265
+ this.timers.set(watcherId, timer);
247
266
  }
248
-
249
- this.timers.set(watcherId, timer);
250
- });
267
+ );
251
268
 
252
269
  return watcherId;
253
270
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * BookmarkStore - Manages local and remote bookmarks
3
- *
3
+ *
4
4
  * Bookmarks are named pointers to changes, similar to Git branches.
5
5
  */
6
6
 
@@ -15,6 +15,7 @@ export class BookmarkStore {
15
15
  this.storage = storage;
16
16
  this.local = new Map();
17
17
  this.remote = new Map(); // remote → (bookmark → changeId)
18
+ /** @type {Record<string, any>} */
18
19
  this.tracking = {}; // bookmark → { remote, ref } tracking info (v0.35.0)
19
20
  }
20
21
 
@@ -79,11 +80,13 @@ export class BookmarkStore {
79
80
  * Save bookmarks to storage
80
81
  */
81
82
  async save() {
83
+ /** @type {Record<string, any>} */
82
84
  const local = {};
83
85
  for (const [name, target] of this.local.entries()) {
84
86
  local[name] = target;
85
87
  }
86
88
 
89
+ /** @type {Record<string, any>} */
87
90
  const remote = {};
88
91
  for (const [remoteName, bookmarks] of this.remote.entries()) {
89
92
  remote[remoteName] = {};
@@ -102,7 +105,7 @@ export class BookmarkStore {
102
105
 
103
106
  /**
104
107
  * Set a local bookmark
105
- *
108
+ *
106
109
  * @param {string} name - Bookmark name
107
110
  * @param {string} target - Change ID to point to
108
111
  */
@@ -123,7 +126,7 @@ export class BookmarkStore {
123
126
 
124
127
  /**
125
128
  * Move an existing local bookmark
126
- *
129
+ *
127
130
  * @param {string} name - Bookmark name
128
131
  * @param {string} target - New change ID to point to
129
132
  */
@@ -144,7 +147,7 @@ export class BookmarkStore {
144
147
 
145
148
  /**
146
149
  * Delete a local bookmark
147
- *
150
+ *
148
151
  * @param {string} name - Bookmark name
149
152
  */
150
153
  async delete(name) {
@@ -162,7 +165,7 @@ export class BookmarkStore {
162
165
 
163
166
  /**
164
167
  * Get bookmark target
165
- *
168
+ *
166
169
  * @param {string} name - Bookmark name
167
170
  * @returns {Promise<string|null>} Change ID or null if not found
168
171
  */
@@ -172,7 +175,7 @@ export class BookmarkStore {
172
175
 
173
176
  /**
174
177
  * Set a remote bookmark
175
- *
178
+ *
176
179
  * @param {string} remoteName - Remote name
177
180
  * @param {string} name - Bookmark name
178
181
  * @param {string} target - Change ID to point to
@@ -191,7 +194,7 @@ export class BookmarkStore {
191
194
 
192
195
  /**
193
196
  * Get remote bookmark target
194
- *
197
+ *
195
198
  * @param {string} remoteName - Remote name
196
199
  * @param {string} name - Bookmark name
197
200
  * @returns {Promise<string|null>} Change ID or null if not found
@@ -204,13 +207,14 @@ export class BookmarkStore {
204
207
  /**
205
208
  * List all bookmarks (local and remote)
206
209
  *
207
- * @returns {Promise<Array>} Array of bookmark objects
210
+ * @returns {Promise<Array<any>>} Array of bookmark objects
208
211
  */
209
212
  async list() {
210
213
  const result = [];
211
214
 
212
215
  // Add local bookmarks with tracking info (v0.35.0)
213
216
  for (const [name, target] of this.local.entries()) {
217
+ /** @type {Record<string, any>} */
214
218
  const bookmark = {
215
219
  name,
216
220
  changeId: target,
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * ChangeGraph - Manages the change graph with stable change IDs
3
- *
3
+ *
4
4
  * Tracks changes, their relationships (parents/children), and evolution (amends/rewrites).
5
5
  */
6
6
 
@@ -62,6 +62,7 @@ export class ChangeGraph {
62
62
  * Save graph to storage
63
63
  */
64
64
  async save() {
65
+ /** @type {Record<string, any>} */
65
66
  const changes = {};
66
67
  for (const [changeId, change] of this.nodes.entries()) {
67
68
  changes[changeId] = change;
@@ -75,8 +76,8 @@ export class ChangeGraph {
75
76
 
76
77
  /**
77
78
  * Add a change to the graph
78
- *
79
- * @param {Object} change - Change object
79
+ *
80
+ * @param {Record<string, any>} change - Change object
80
81
  */
81
82
  async addChange(change) {
82
83
  validateChangeId(change.changeId);
@@ -96,9 +97,9 @@ export class ChangeGraph {
96
97
 
97
98
  /**
98
99
  * Get a change by ID
99
- *
100
+ *
100
101
  * @param {string} changeId - Change ID
101
- * @returns {Object|null} Change object or null if not found
102
+ * @returns {Promise<Record<string, any>|null>} Change object or null if not found
102
103
  */
103
104
  async getChange(changeId) {
104
105
  validateChangeId(changeId);
@@ -107,8 +108,8 @@ export class ChangeGraph {
107
108
 
108
109
  /**
109
110
  * Get all changes
110
- *
111
- * @returns {Array} Array of all changes
111
+ *
112
+ * @returns {Array<any>} Array of all changes
112
113
  */
113
114
  getAll() {
114
115
  return Array.from(this.nodes.values());
@@ -116,7 +117,7 @@ export class ChangeGraph {
116
117
 
117
118
  /**
118
119
  * Find change by commit ID
119
- *
120
+ *
120
121
  * @param {string} commitId - Git commit SHA-1
121
122
  * @returns {string|null} Change ID or null if not found
122
123
  */
@@ -126,8 +127,8 @@ export class ChangeGraph {
126
127
 
127
128
  /**
128
129
  * Get parent change IDs
129
- *
130
- * @param {string} changeId - Change ID
130
+ *
131
+ * @param {any} changeId - Change ID
131
132
  * @returns {Array<string>} Array of parent change IDs
132
133
  */
133
134
  getParents(changeId) {
@@ -137,27 +138,27 @@ export class ChangeGraph {
137
138
 
138
139
  /**
139
140
  * Get child change IDs
140
- *
141
+ *
141
142
  * @param {string} changeId - Change ID
142
143
  * @returns {Array<string>} Array of child change IDs
143
144
  */
144
145
  getChildren(changeId) {
145
146
  const children = [];
146
-
147
+
147
148
  for (const change of this.nodes.values()) {
148
149
  if (change.parents.includes(changeId)) {
149
150
  children.push(change.changeId);
150
151
  }
151
152
  }
152
-
153
+
153
154
  return children;
154
155
  }
155
156
 
156
157
  /**
157
158
  * Evolve a change (update commitId, track predecessor)
158
- *
159
+ *
159
160
  * Used when amending or rewriting a change.
160
- *
161
+ *
161
162
  * @param {string} changeId - Change ID
162
163
  * @param {string} newCommitId - New commit SHA-1
163
164
  */
@@ -188,8 +189,8 @@ export class ChangeGraph {
188
189
 
189
190
  /**
190
191
  * Update a change (modify in place)
191
- *
192
- * @param {Object} change - Updated change object
192
+ *
193
+ * @param {Record<string, any>} change - Updated change object
193
194
  */
194
195
  async updateChange(change) {
195
196
  validateChangeId(change.changeId);
@@ -213,20 +214,19 @@ export class ChangeGraph {
213
214
  */
214
215
  getAncestors(changeId) {
215
216
  const ancestors = [];
216
- const visited = new Set();
217
+ // `visited` also doubles as "already enqueued" — mark a node the moment
218
+ // it's scheduled (not when dequeued), so a change reachable via two paths
219
+ // in a diamond-shaped history (a<-b, a<-c, b&c<-d) is only added once.
220
+ const visited = new Set([changeId]);
217
221
  const queue = [changeId];
218
222
 
219
223
  while (queue.length > 0) {
220
224
  const current = queue.shift();
221
225
 
222
- if (visited.has(current)) {
223
- continue;
224
- }
225
- visited.add(current);
226
-
227
226
  const parents = this.getParents(current);
228
227
  for (const parent of parents) {
229
228
  if (!visited.has(parent)) {
229
+ visited.add(parent);
230
230
  ancestors.push(parent);
231
231
  queue.push(parent);
232
232
  }
@@ -239,12 +239,8 @@ export class ChangeGraph {
239
239
  /**
240
240
  * Create a change with default values (helper for tests)
241
241
  *
242
- * @param {Object} params - Change parameters
243
- * @param {string[]} [params.parents] - Parent change IDs
244
- * @param {string} [params.description] - Change description
245
- * @param {Object} [params.fileSnapshot] - File snapshot
246
- * @param {Object} [params.conflicts] - Conflict markers
247
- * @returns {Promise<Object>} Created change object
242
+ * @param {Record<string, any>} [params] - Change parameters
243
+ * @returns {Promise<Record<string, any>>} Created change object
248
244
  */
249
245
  async createChange(params = {}) {
250
246
  const crypto = await import('crypto');
@@ -253,6 +249,7 @@ export class ChangeGraph {
253
249
  const tree = params.tree || crypto.randomBytes(20).toString('hex');
254
250
  const timestamp = params.timestamp || new Date().toISOString();
255
251
 
252
+ /** @type {Record<string, any>} */
256
253
  const change = {
257
254
  changeId,
258
255
  parents: params.parents || [],
@@ -18,20 +18,26 @@ import { generateId } from '../utils/id-generation.js';
18
18
  * Conflict types supported by JJ
19
19
  */
20
20
  export const ConflictType = {
21
- CONTENT: 'content', // File content conflicts (3-way merge)
22
- PATH: 'path', // Path/rename conflicts
23
- DELETE_MODIFY: 'delete-modify', // File deleted in one side, modified in other
24
- MODIFY_DELETE: 'modify-delete', // File modified in one side, deleted in other
25
- ADD_ADD: 'add-add', // Same path added in both sides
21
+ CONTENT: 'content', // File content conflicts (3-way merge)
22
+ PATH: 'path', // Path/rename conflicts
23
+ DELETE_MODIFY: 'delete-modify', // File deleted in one side, modified in other
24
+ MODIFY_DELETE: 'modify-delete', // File modified in one side, deleted in other
25
+ ADD_ADD: 'add-add', // Same path added in both sides
26
26
  };
27
27
 
28
28
  /**
29
29
  * ConflictModel manages conflict detection, storage, and resolution
30
30
  */
31
31
  export class ConflictModel {
32
+ /**
33
+ * @param {any} storage
34
+ * @param {any} fs
35
+ * @param {any} [mergeDriverRegistry]
36
+ */
32
37
  constructor(storage, fs, mergeDriverRegistry = null) {
33
38
  this.storage = storage;
34
39
  this.fs = fs;
40
+ /** @type {any} */
35
41
  this.mergeDriverRegistry = mergeDriverRegistry; // v0.5: merge drivers
36
42
  this.conflicts = new Map(); // conflictId -> Conflict
37
43
  this.fileConflicts = new Map(); // path -> conflictId
@@ -81,23 +87,29 @@ export class ConflictModel {
81
87
  * @param {string} opts.baseChange - Common ancestor change
82
88
  * @param {string} opts.leftChange - Left side change
83
89
  * @param {string} opts.rightChange - Right side change
84
- * @param {Map} opts.baseFiles - Files in base
85
- * @param {Map} opts.leftFiles - Files in left
86
- * @param {Map} opts.rightFiles - Files in right
90
+ * @param {Map<any, any>} opts.baseFiles - Files in base
91
+ * @param {Map<any, any>} opts.leftFiles - Files in left
92
+ * @param {Map<any, any>} opts.rightFiles - Files in right
87
93
  * @param {Object} opts.drivers - Custom merge drivers (v0.5)
88
94
  * @param {string} opts.workingCopyDir - Working copy directory for file writes (v0.5)
89
- * @returns {Array<Conflict>} Detected conflicts
95
+ * @returns {Promise<any[]>} Detected conflicts
90
96
  */
91
97
  async detectConflicts(opts) {
92
- const { baseFiles, leftFiles, rightFiles, drivers = {}, workingCopyDir, baseChange, leftChange, rightChange } = opts;
98
+ const {
99
+ baseFiles,
100
+ leftFiles,
101
+ rightFiles,
102
+ drivers = {},
103
+ workingCopyDir,
104
+ baseChange,
105
+ leftChange,
106
+ rightChange,
107
+ } = opts;
108
+ /** @type {any[]} */
93
109
  const conflicts = [];
94
110
 
95
111
  // Get all unique paths across all versions
96
- const allPaths = new Set([
97
- ...baseFiles.keys(),
98
- ...leftFiles.keys(),
99
- ...rightFiles.keys(),
100
- ]);
112
+ const allPaths = new Set([...baseFiles.keys(), ...leftFiles.keys(), ...rightFiles.keys()]);
101
113
 
102
114
  for (const path of allPaths) {
103
115
  const baseContent = baseFiles.get(path);
@@ -108,7 +120,7 @@ export class ConflictModel {
108
120
  if (this.mergeDriverRegistry) {
109
121
  const mergeResult = await this._tryMergeDriver(
110
122
  path,
111
- {base: baseContent, ours: leftContent, theirs: rightContent},
123
+ { base: baseContent, ours: leftContent, theirs: rightContent },
112
124
  drivers,
113
125
  { baseChange, leftChange, rightChange }
114
126
  );
@@ -125,7 +137,10 @@ export class ConflictModel {
125
137
  type: driverConflict.type || 'driver-conflict',
126
138
  path,
127
139
  sides: { base: baseContent, left: leftContent, right: rightContent },
128
- message: driverConflict.message || mergeResult.message || `Merge driver detected conflicts`,
140
+ message:
141
+ driverConflict.message ||
142
+ mergeResult.message ||
143
+ `Merge driver detected conflicts`,
129
144
  driverResult: mergeResult,
130
145
  driverFailed: mergeResult.driverFailed,
131
146
  driverError: mergeResult.driverError,
@@ -167,6 +182,11 @@ export class ConflictModel {
167
182
 
168
183
  /**
169
184
  * Detect conflict for a single path
185
+ *
186
+ * @param {any} path
187
+ * @param {any} base
188
+ * @param {any} left
189
+ * @param {any} right
170
190
  */
171
191
  _detectPathConflict(path, base, left, right) {
172
192
  const baseExists = base !== undefined;
@@ -243,9 +263,12 @@ export class ConflictModel {
243
263
 
244
264
  /**
245
265
  * Create a conflict object
266
+ *
267
+ * @param {{ type: any, path: any, sides: any, message: any, driverFailed?: any, driverError?: any, driverResult?: any }} args
246
268
  */
247
269
  _createConflict({ type, path, sides, message, driverFailed, driverError }) {
248
270
  const conflictId = generateId('conflict');
271
+ /** @type {Record<string, any>} */
249
272
  const conflict = {
250
273
  conflictId,
251
274
  type,
@@ -269,6 +292,8 @@ export class ConflictModel {
269
292
 
270
293
  /**
271
294
  * Store a conflict
295
+ *
296
+ * @param {any} conflict
272
297
  */
273
298
  async addConflict(conflict) {
274
299
  this.conflicts.set(conflict.conflictId, conflict);
@@ -279,6 +304,8 @@ export class ConflictModel {
279
304
 
280
305
  /**
281
306
  * Get conflict by ID
307
+ *
308
+ * @param {any} conflictId
282
309
  */
283
310
  getConflict(conflictId) {
284
311
  return this.conflicts.get(conflictId);
@@ -286,6 +313,8 @@ export class ConflictModel {
286
313
 
287
314
  /**
288
315
  * Get conflict for a file path
316
+ *
317
+ * @param {any} path
289
318
  */
290
319
  getConflictForPath(path) {
291
320
  const conflictId = this.fileConflicts.get(path);
@@ -295,16 +324,18 @@ export class ConflictModel {
295
324
 
296
325
  /**
297
326
  * List all conflicts
327
+ *
328
+ * @param {Record<string, any>} [opts]
298
329
  */
299
330
  listConflicts(opts = {}) {
300
331
  let conflicts = Array.from(this.conflicts.values());
301
332
 
302
333
  if (opts.resolved !== undefined) {
303
- conflicts = conflicts.filter(c => c.resolved === opts.resolved);
334
+ conflicts = conflicts.filter((c) => c.resolved === opts.resolved);
304
335
  }
305
336
 
306
337
  if (opts.type) {
307
- conflicts = conflicts.filter(c => c.type === opts.type);
338
+ conflicts = conflicts.filter((c) => c.type === opts.type);
308
339
  }
309
340
 
310
341
  return conflicts;
@@ -312,6 +343,9 @@ export class ConflictModel {
312
343
 
313
344
  /**
314
345
  * Mark a conflict as resolved
346
+ *
347
+ * @param {any} conflictId
348
+ * @param {any} resolution
315
349
  */
316
350
  async resolveConflict(conflictId, resolution) {
317
351
  const conflict = this.conflicts.get(conflictId);
@@ -331,7 +365,7 @@ export class ConflictModel {
331
365
  // { side: 'ours' | 'theirs' | 'base' }
332
366
  if (!['ours', 'theirs', 'base'].includes(resolution.side)) {
333
367
  throw new JJError('INVALID_RESOLUTION', `Invalid side: ${resolution.side}`, {
334
- suggestion: 'Use "ours", "theirs", or "base"'
368
+ suggestion: 'Use "ours", "theirs", or "base"',
335
369
  });
336
370
  }
337
371
  resolvedContent = { type: 'side', side: resolution.side };
@@ -359,6 +393,8 @@ export class ConflictModel {
359
393
 
360
394
  /**
361
395
  * Remove a conflict (after resolution is committed)
396
+ *
397
+ * @param {any} conflictId
362
398
  */
363
399
  async removeConflict(conflictId) {
364
400
  const conflict = this.conflicts.get(conflictId);
@@ -378,11 +414,12 @@ export class ConflictModel {
378
414
 
379
415
  /**
380
416
  * Create conflict markers for a content conflict (like Git's <<<<<<<, =======, >>>>>>>)
417
+ *
418
+ * @param {any} conflict
381
419
  */
382
420
  generateConflictMarkers(conflict) {
383
421
  if (conflict.type !== ConflictType.CONTENT) {
384
- throw new JJError('INVALID_CONFLICT_TYPE',
385
- 'Can only generate markers for content conflicts');
422
+ throw new JJError('INVALID_CONFLICT_TYPE', 'Can only generate markers for content conflicts');
386
423
  }
387
424
 
388
425
  const { base, left, right } = conflict.sides;
@@ -400,9 +437,12 @@ export class ConflictModel {
400
437
 
401
438
  /**
402
439
  * Parse conflict markers from file content
440
+ *
441
+ * @param {any} content
403
442
  */
404
443
  parseConflictMarkers(content) {
405
- const markerRegex = /^<{7} Left\n([\s\S]*?)\n\|{7} Base\n([\s\S]*?)\n={7}\n([\s\S]*?)\n>{7} Right$/gm;
444
+ const markerRegex =
445
+ /^<{7} Left\n([\s\S]*?)\n\|{7} Base\n([\s\S]*?)\n={7}\n([\s\S]*?)\n>{7} Right$/gm;
406
446
  const matches = markerRegex.exec(content);
407
447
 
408
448
  if (!matches) {
@@ -429,10 +469,10 @@ export class ConflictModel {
429
469
  * Try to use a merge driver for a file (v0.5)
430
470
  *
431
471
  * @param {string} path - File path
432
- * @param {Object} content - Content versions { base, ours, theirs }
472
+ * @param {{ base?: string, ours?: string, theirs?: string }} content - Content versions { base, ours, theirs }
433
473
  * @param {Object} customDrivers - Per-merge custom drivers
434
474
  * @param {Object} metadata - Merge metadata
435
- * @returns {Promise<Object|null>} Merge result or null if no driver
475
+ * @returns {Promise<Record<string, any>|null>} Merge result or null if no driver
436
476
  */
437
477
  async _tryMergeDriver(path, content, customDrivers, metadata) {
438
478
  if (!this.mergeDriverRegistry) {
@@ -469,7 +509,7 @@ export class ConflictModel {
469
509
  *
470
510
  * @param {string} workingCopyDir - Working copy directory
471
511
  * @param {string} filePath - File path
472
- * @param {Object} result - Merge result
512
+ * @param {Record<string, any>} result - Merge result
473
513
  */
474
514
  async _writeDriverResult(workingCopyDir, filePath, result) {
475
515
  const fullPath = path.join(workingCopyDir, filePath);