isomorphic-jj 1.4.2 → 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.
@@ -19,6 +19,10 @@ const protoDir = inDist
19
19
  : path.join(__dirname, '..', 'protos');
20
20
 
21
21
  export class JJCheckout {
22
+ /**
23
+ * @param {any} fs - File system module (isomorphic-git compatible)
24
+ * @param {any} dir - Repository directory path
25
+ */
22
26
  constructor(fs, dir) {
23
27
  this.fs = fs;
24
28
  this.dir = dir;
@@ -42,7 +46,7 @@ export class JJCheckout {
42
46
  // Create message (use camelCase for protobufjs)
43
47
  const message = Checkout.create({
44
48
  operationId: opIdBuffer,
45
- workspaceName: workspaceName
49
+ workspaceName: workspaceName,
46
50
  });
47
51
 
48
52
  // Verify the message
@@ -62,7 +66,7 @@ export class JJCheckout {
62
66
  /**
63
67
  * Read checkout file
64
68
  *
65
- * @returns {Object} Decoded checkout data
69
+ * @returns {Promise<Record<string, any>>} Decoded checkout data
66
70
  */
67
71
  async readCheckout() {
68
72
  const checkoutPath = `${this.dir}/.jj/working_copy/checkout`;
@@ -73,12 +77,12 @@ export class JJCheckout {
73
77
  const Checkout = root.lookupType('local_working_copy.Checkout');
74
78
 
75
79
  // Decode
76
- const message = Checkout.decode(buffer);
80
+ const message = /** @type {any} */ (Checkout.decode(buffer));
77
81
 
78
82
  // Return the decoded message with proper field access
79
83
  return {
80
84
  operation_id: message.operationId,
81
- workspace_name: message.workspaceName
85
+ workspace_name: message.workspaceName,
82
86
  };
83
87
  }
84
88
  }
@@ -19,6 +19,10 @@ const protoDir = inDist
19
19
  : path.join(__dirname, '..', 'protos');
20
20
 
21
21
  export class JJOperationStore {
22
+ /**
23
+ * @param {any} fs - File system module (isomorphic-git compatible)
24
+ * @param {any} dir - Repository directory path
25
+ */
22
26
  constructor(fs, dir) {
23
27
  this.fs = fs;
24
28
  this.dir = dir;
@@ -31,7 +35,7 @@ export class JJOperationStore {
31
35
  * @param {string} operationId - Operation ID as hex string (128 characters)
32
36
  * @param {string} viewId - View ID as hex string (128 characters)
33
37
  * @param {Array<string>} parentIds - Parent operation IDs as hex strings
34
- * @param {Object} metadata - Operation metadata
38
+ * @param {Record<string, any>} metadata - Operation metadata
35
39
  */
36
40
  async writeOperation(operationId, viewId, parentIds, metadata) {
37
41
  // Load protobuf schema
@@ -40,23 +44,23 @@ export class JJOperationStore {
40
44
 
41
45
  // Convert hex IDs to bytes
42
46
  const viewIdBuffer = Buffer.from(viewId, 'hex');
43
- const parentBuffers = parentIds.map(id => Buffer.from(id, 'hex'));
47
+ const parentBuffers = parentIds.map((id) => Buffer.from(id, 'hex'));
44
48
 
45
49
  // Create metadata message (use camelCase for protobufjs)
46
50
  const metadataMsg = {
47
51
  startTime: {
48
52
  millisSinceEpoch: metadata.start_time.millis_since_epoch,
49
- tzOffset: metadata.start_time.tz_offset
53
+ tzOffset: metadata.start_time.tz_offset,
50
54
  },
51
55
  endTime: {
52
56
  millisSinceEpoch: metadata.end_time.millis_since_epoch,
53
- tzOffset: metadata.end_time.tz_offset
57
+ tzOffset: metadata.end_time.tz_offset,
54
58
  },
55
59
  description: metadata.description,
56
60
  hostname: metadata.hostname,
57
61
  username: metadata.username,
58
62
  isSnapshot: metadata.is_snapshot,
59
- tags: metadata.tags
63
+ tags: metadata.tags,
60
64
  };
61
65
 
62
66
  // Create operation message (use camelCase for protobufjs)
@@ -65,7 +69,7 @@ export class JJOperationStore {
65
69
  parents: parentBuffers,
66
70
  metadata: metadataMsg,
67
71
  commitPredecessors: [], // Empty for now
68
- storesCommitPredecessors: false
72
+ storesCommitPredecessors: false,
69
73
  });
70
74
 
71
75
  // Verify the message
@@ -86,7 +90,7 @@ export class JJOperationStore {
86
90
  * Read operation file
87
91
  *
88
92
  * @param {string} operationId - Operation ID as hex string (128 characters)
89
- * @returns {Object} Decoded operation data
93
+ * @returns {Promise<Record<string, any>>} Decoded operation data
90
94
  */
91
95
  async readOperation(operationId) {
92
96
  // Read from .jj/repo/op_store/operations/OPERATIONID
@@ -98,7 +102,7 @@ export class JJOperationStore {
98
102
  const Operation = root.lookupType('simple_op_store.Operation');
99
103
 
100
104
  // Decode
101
- const message = Operation.decode(buffer);
105
+ const message = /** @type {any} */ (Operation.decode(buffer));
102
106
 
103
107
  // Return the decoded message with proper field access
104
108
  return {
@@ -107,20 +111,20 @@ export class JJOperationStore {
107
111
  metadata: {
108
112
  start_time: {
109
113
  millis_since_epoch: message.metadata.startTime.millisSinceEpoch,
110
- tz_offset: message.metadata.startTime.tzOffset
114
+ tz_offset: message.metadata.startTime.tzOffset,
111
115
  },
112
116
  end_time: {
113
117
  millis_since_epoch: message.metadata.endTime.millisSinceEpoch,
114
- tz_offset: message.metadata.endTime.tzOffset
118
+ tz_offset: message.metadata.endTime.tzOffset,
115
119
  },
116
120
  description: message.metadata.description,
117
121
  hostname: message.metadata.hostname,
118
122
  username: message.metadata.username,
119
123
  is_snapshot: message.metadata.isSnapshot,
120
- tags: message.metadata.tags
124
+ tags: message.metadata.tags,
121
125
  },
122
126
  commit_predecessors: message.commitPredecessors,
123
- stores_commit_predecessors: message.storesCommitPredecessors
127
+ stores_commit_predecessors: message.storesCommitPredecessors,
124
128
  };
125
129
  }
126
130
  }
@@ -19,6 +19,10 @@ const protoDir = inDist
19
19
  : path.join(__dirname, '..', 'protos');
20
20
 
21
21
  export class JJTreeState {
22
+ /**
23
+ * @param {any} fs - File system module (isomorphic-git compatible)
24
+ * @param {any} dir - Repository directory path
25
+ */
22
26
  constructor(fs, dir) {
23
27
  this.fs = fs;
24
28
  this.dir = dir;
@@ -29,7 +33,7 @@ export class JJTreeState {
29
33
  * Write tree_state file
30
34
  *
31
35
  * @param {string} treeId - Tree ID as hex string (40 characters for SHA-1)
32
- * @param {Array} fileStates - Array of file state objects
36
+ * @param {Array<any>} fileStates - Array of file state objects
33
37
  */
34
38
  async writeTreeState(treeId, fileStates = []) {
35
39
  // Load protobuf schema
@@ -40,13 +44,13 @@ export class JJTreeState {
40
44
  const treeIdBuffer = Buffer.from(treeId, 'hex');
41
45
 
42
46
  // Convert file states to protobuf format
43
- const fileStateEntries = fileStates.map(fs => ({
47
+ const fileStateEntries = fileStates.map((fs) => ({
44
48
  path: fs.path,
45
49
  state: {
46
50
  mtimeMillisSinceEpoch: fs.mtime, // Use camelCase for protobufjs
47
51
  size: fs.size,
48
- fileType: fs.fileType || 0 // 0 = Normal
49
- }
52
+ fileType: fs.fileType || 0, // 0 = Normal
53
+ },
50
54
  }));
51
55
 
52
56
  // Create message (use camelCase for protobufjs)
@@ -55,8 +59,8 @@ export class JJTreeState {
55
59
  fileStates: fileStateEntries,
56
60
  isFileStatesSorted: false, // We don't sort them
57
61
  sparsePatterns: {
58
- prefixes: [] // No sparse patterns
59
- }
62
+ prefixes: [], // No sparse patterns
63
+ },
60
64
  });
61
65
 
62
66
  // Verify the message
@@ -76,7 +80,7 @@ export class JJTreeState {
76
80
  /**
77
81
  * Read tree_state file
78
82
  *
79
- * @returns {Object} Decoded tree state data
83
+ * @returns {Promise<Record<string, any>>} Decoded tree state data
80
84
  */
81
85
  async readTreeState() {
82
86
  const treeStatePath = `${this.dir}/.jj/working_copy/tree_state`;
@@ -87,14 +91,14 @@ export class JJTreeState {
87
91
  const TreeState = root.lookupType('local_working_copy.TreeState');
88
92
 
89
93
  // Decode
90
- const message = TreeState.decode(buffer);
94
+ const message = /** @type {any} */ (TreeState.decode(buffer));
91
95
 
92
96
  // Return the decoded message with proper field access
93
97
  return {
94
98
  tree_ids: message.treeIds,
95
99
  file_states: message.fileStates,
96
100
  is_file_states_sorted: message.isFileStatesSorted,
97
- sparse_patterns: message.sparsePatterns
101
+ sparse_patterns: message.sparsePatterns,
98
102
  };
99
103
  }
100
104
  }
@@ -19,6 +19,10 @@ const protoDir = inDist
19
19
  : path.join(__dirname, '..', 'protos');
20
20
 
21
21
  export class JJViewStore {
22
+ /**
23
+ * @param {any} fs - File system module (isomorphic-git compatible)
24
+ * @param {any} dir - Repository directory path
25
+ */
22
26
  constructor(fs, dir) {
23
27
  this.fs = fs;
24
28
  this.dir = dir;
@@ -38,9 +42,10 @@ export class JJViewStore {
38
42
  const View = root.lookupType('simple_op_store.View');
39
43
 
40
44
  // Convert hex IDs to bytes
41
- const headBuffers = headIds.map(id => Buffer.from(id, 'hex'));
45
+ const headBuffers = headIds.map((id) => Buffer.from(id, 'hex'));
42
46
 
43
47
  // Convert wcCommitIds object to map
48
+ /** @type {Record<string, any>} */
44
49
  const wcCommitIdsMap = {};
45
50
  for (const [workspace, commitId] of Object.entries(wcCommitIds)) {
46
51
  wcCommitIdsMap[workspace] = Buffer.from(commitId, 'hex');
@@ -55,7 +60,7 @@ export class JJViewStore {
55
60
  remoteViews: [],
56
61
  gitRefs: [],
57
62
  gitHead: null,
58
- hasGitRefsMigratedToRemoteTags: false
63
+ hasGitRefsMigratedToRemoteTags: false,
59
64
  });
60
65
 
61
66
  // Verify the message
@@ -76,7 +81,7 @@ export class JJViewStore {
76
81
  * Read view file
77
82
  *
78
83
  * @param {string} viewId - View ID as hex string (128 characters)
79
- * @returns {Object} Decoded view data
84
+ * @returns {Promise<Record<string, any>>} Decoded view data
80
85
  */
81
86
  async readView(viewId) {
82
87
  // Read from .jj/repo/op_store/views/VIEWID
@@ -88,9 +93,10 @@ export class JJViewStore {
88
93
  const View = root.lookupType('simple_op_store.View');
89
94
 
90
95
  // Decode
91
- const message = View.decode(buffer);
96
+ const message = /** @type {any} */ (View.decode(buffer));
92
97
 
93
98
  // Convert wcCommitIds map back to object
99
+ /** @type {Record<string, any>} */
94
100
  const wcCommitIds = {};
95
101
  for (const [workspace, commitIdBuffer] of Object.entries(message.wcCommitIds || {})) {
96
102
  wcCommitIds[workspace] = commitIdBuffer;
@@ -105,7 +111,7 @@ export class JJViewStore {
105
111
  remote_views: message.remoteViews,
106
112
  git_refs: message.gitRefs,
107
113
  git_head: message.gitHead,
108
- has_git_refs_migrated_to_remote_tags: message.hasGitRefsMigratedToRemoteTags
114
+ has_git_refs_migrated_to_remote_tags: message.hasGitRefsMigratedToRemoteTags,
109
115
  };
110
116
  }
111
117
  }
@@ -21,12 +21,15 @@ function matchesPattern(path, pattern) {
21
21
 
22
22
  // Convert glob pattern to regex
23
23
  // Support: *, **, ?, [abc]
24
+ // Literal dots must be escaped BEFORE the ** -> '.*' substitution, otherwise
25
+ // the dot-escaping pass corrupts the inserted '.*' into the literal '\.*'
26
+ // (zero-or-more dots) instead of "any characters".
24
27
  const regexPattern = pattern
28
+ .replace(/\./g, '\\.')
25
29
  .replace(/\*\*/g, '__DOUBLESTAR__')
26
30
  .replace(/\*/g, '[^/]*')
27
31
  .replace(/__DOUBLESTAR__/g, '.*')
28
- .replace(/\?/g, '[^/]')
29
- .replace(/\./g, '\\.');
32
+ .replace(/\?/g, '[^/]');
30
33
 
31
34
  const regex = new RegExp(`^${regexPattern}$`);
32
35
  return regex.test(path);
@@ -46,12 +49,10 @@ function isBinaryContent(content) {
46
49
  if (content.indexOf('\0') !== -1) return true;
47
50
 
48
51
  // Check for high ratio of non-printable characters
49
- const nonPrintable = content.split('').filter(
50
- (char) => {
51
- const code = char.charCodeAt(0);
52
- return code < 32 && code !== 9 && code !== 10 && code !== 13;
53
- }
54
- ).length;
52
+ const nonPrintable = content.split('').filter((char) => {
53
+ const code = char.charCodeAt(0);
54
+ return code < 32 && code !== 9 && code !== 10 && code !== 13;
55
+ }).length;
55
56
 
56
57
  const ratio = nonPrintable / content.length;
57
58
  return ratio > 0.3;
@@ -60,8 +61,8 @@ function isBinaryContent(content) {
60
61
  /**
61
62
  * Default merge driver - standard three-way text merge
62
63
  *
63
- * @param {MergeContext} context
64
- * @returns {Promise<MergeResult>}
64
+ * @param {{ content: { base?: string, ours?: string, theirs?: string } }} context
65
+ * @returns {Promise<Record<string, any>>}
65
66
  */
66
67
  async function defaultMergeDriver(context) {
67
68
  const { content } = context;
@@ -88,10 +89,12 @@ async function defaultMergeDriver(context) {
88
89
  return {
89
90
  content: conflictMarkers,
90
91
  hasConflict: true,
91
- conflicts: [{
92
- type: 'content',
93
- sides: ['ours', 'theirs'],
94
- }],
92
+ conflicts: [
93
+ {
94
+ type: 'content',
95
+ sides: ['ours', 'theirs'],
96
+ },
97
+ ],
95
98
  };
96
99
  }
97
100
 
@@ -102,14 +105,14 @@ async function defaultMergeDriver(context) {
102
105
  * @param {Object} options - Wrapper options
103
106
  * @param {number} [options.timeout=5000] - Timeout in ms
104
107
  * @param {boolean} [options.strict=false] - Throw on error instead of falling back
105
- * @param {Object} [options.jj] - JJ instance for event emission
108
+ * @param {any} [options.jj] - JJ instance for event emission
106
109
  * @param {string} [options.pattern] - Pattern that matched this driver
107
110
  * @returns {Function} Wrapped driver
108
111
  */
109
112
  function wrapDriver(driver, options = {}) {
110
113
  const { timeout = 5000, strict = false, jj = null, pattern = null } = options;
111
114
 
112
- return async (context) => {
115
+ return async (/** @type {any} */ context) => {
113
116
  let timeoutId;
114
117
  const timeoutPromise = new Promise((_, reject) => {
115
118
  timeoutId = setTimeout(() => reject(new Error('Driver timeout')), timeout);
@@ -120,10 +123,7 @@ function wrapDriver(driver, options = {}) {
120
123
  });
121
124
 
122
125
  try {
123
- const result = await Promise.race([
124
- driver(context),
125
- timeoutPromise,
126
- ]);
126
+ const result = await Promise.race([driver(context), timeoutPromise]);
127
127
 
128
128
  // Clear timeout if driver completed successfully
129
129
  clearTimeout(timeoutId);
@@ -140,14 +140,16 @@ function wrapDriver(driver, options = {}) {
140
140
 
141
141
  // Emit driver:failed event if jj instance available
142
142
  if (jj && jj.dispatchEvent) {
143
- jj.dispatchEvent(new CustomEvent('driver:failed', {
144
- detail: {
145
- path: context.path,
146
- pattern: pattern,
147
- error: error.message,
148
- timestamp: new Date().toISOString(),
149
- },
150
- }));
143
+ jj.dispatchEvent(
144
+ new CustomEvent('driver:failed', {
145
+ detail: {
146
+ path: context.path,
147
+ pattern: pattern,
148
+ error: error.message,
149
+ timestamp: new Date().toISOString(),
150
+ },
151
+ })
152
+ );
151
153
  }
152
154
 
153
155
  // In strict mode, throw the error
@@ -170,16 +172,24 @@ function wrapDriver(driver, options = {}) {
170
172
  * MergeDriverRegistry manages custom merge drivers
171
173
  */
172
174
  export class MergeDriverRegistry {
175
+ /**
176
+ * @param {any} [jj]
177
+ */
173
178
  constructor(jj = null) {
174
- /** @type {Array<{pattern: string, driver: Function, accepts: Object, strict: boolean}>} */
179
+ /** @type {Array<{pattern: string, driver: Function, accepts: any, strict: boolean}>} */
175
180
  this.drivers = [];
181
+ /** @type {any} */
176
182
  this.jj = jj; // JJ instance for event emission
183
+ // Expose the built-in fallback driver so callers (e.g. ConflictModel) can
184
+ // detect when findDriver() fell through to it rather than resolving a
185
+ // real custom driver, and skip driver execution in that case.
186
+ this.defaultDriver = defaultMergeDriver;
177
187
  }
178
188
 
179
189
  /**
180
190
  * Register one or more merge drivers
181
191
  *
182
- * @param {Object<string, Function|Object>} drivers - Pattern -> driver map
192
+ * @param {Record<string, any>} drivers - Pattern -> driver map
183
193
  *
184
194
  * @example
185
195
  * registry.register({
@@ -206,11 +216,9 @@ export class MergeDriverRegistry {
206
216
  }
207
217
 
208
218
  if (typeof driver !== 'function') {
209
- throw new JJError(
210
- 'INVALID_DRIVER',
211
- `Driver for pattern "${pattern}" must be a function`,
212
- { pattern }
213
- );
219
+ throw new JJError('INVALID_DRIVER', `Driver for pattern "${pattern}" must be a function`, {
220
+ pattern,
221
+ });
214
222
  }
215
223
 
216
224
  // Wrap driver with error handling
@@ -325,7 +333,7 @@ export class MergeDriverRegistry {
325
333
  * @param {string|Buffer|null} context.theirs - Their content
326
334
  * @param {Object} context.metadata - Metadata
327
335
  * @param {boolean} isBinary - Whether content is binary
328
- * @returns {Promise<MergeResult>}
336
+ * @returns {Promise<Record<string, any>>}
329
337
  */
330
338
  async executeDriver(driver, context, isBinary) {
331
339
  const { base, ours, theirs, ...rest } = context;
@@ -381,11 +389,30 @@ export class MergeDriverRegistry {
381
389
  isBinaryFile(path, content) {
382
390
  // Check by extension first
383
391
  const binaryExtensions = [
384
- '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico',
385
- '.zip', '.tar', '.gz', '.bz2', '.7z',
386
- '.pdf', '.doc', '.docx', '.xls', '.xlsx',
387
- '.exe', '.dll', '.so', '.dylib',
388
- '.mp3', '.mp4', '.avi', '.mov',
392
+ '.png',
393
+ '.jpg',
394
+ '.jpeg',
395
+ '.gif',
396
+ '.bmp',
397
+ '.ico',
398
+ '.zip',
399
+ '.tar',
400
+ '.gz',
401
+ '.bz2',
402
+ '.7z',
403
+ '.pdf',
404
+ '.doc',
405
+ '.docx',
406
+ '.xls',
407
+ '.xlsx',
408
+ '.exe',
409
+ '.dll',
410
+ '.so',
411
+ '.dylib',
412
+ '.mp3',
413
+ '.mp4',
414
+ '.avi',
415
+ '.mov',
389
416
  ];
390
417
 
391
418
  const ext = path.substring(path.lastIndexOf('.')).toLowerCase();
@@ -412,11 +439,30 @@ export class MergeDriverRegistry {
412
439
  */
413
440
  export function isBinaryFile(path, content) {
414
441
  const binaryExtensions = [
415
- '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico',
416
- '.zip', '.tar', '.gz', '.bz2', '.7z',
417
- '.pdf', '.doc', '.docx', '.xls', '.xlsx',
418
- '.exe', '.dll', '.so', '.dylib',
419
- '.mp3', '.mp4', '.avi', '.mov',
442
+ '.png',
443
+ '.jpg',
444
+ '.jpeg',
445
+ '.gif',
446
+ '.bmp',
447
+ '.ico',
448
+ '.zip',
449
+ '.tar',
450
+ '.gz',
451
+ '.bz2',
452
+ '.7z',
453
+ '.pdf',
454
+ '.doc',
455
+ '.docx',
456
+ '.xls',
457
+ '.xlsx',
458
+ '.exe',
459
+ '.dll',
460
+ '.so',
461
+ '.dylib',
462
+ '.mp3',
463
+ '.mp4',
464
+ '.avi',
465
+ '.mov',
420
466
  ];
421
467
 
422
468
  const ext = path.substring(path.lastIndexOf('.')).toLowerCase();
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * OperationLog - Manages the append-only operation log
3
- *
3
+ *
4
4
  * Records all repository mutations for complete undo/redo and time-travel.
5
5
  */
6
6
 
@@ -13,6 +13,7 @@ export class OperationLog {
13
13
  */
14
14
  constructor(storage) {
15
15
  this.storage = storage;
16
+ /** @type {any[]} */
16
17
  this.operations = [];
17
18
  this.headOperationId = null;
18
19
  }
@@ -39,9 +40,9 @@ export class OperationLog {
39
40
 
40
41
  /**
41
42
  * Record a new operation
42
- *
43
- * @param {Object} operation - Operation to record (without id)
44
- * @returns {Promise<Object>} Recorded operation with id
43
+ *
44
+ * @param {Record<string, any>} operation - Operation to record (without id)
45
+ * @returns {Promise<Record<string, any>>} Recorded operation with id
45
46
  */
46
47
  async recordOperation(operation) {
47
48
  // Ensure operations are loaded
@@ -76,20 +77,20 @@ export class OperationLog {
76
77
 
77
78
  /**
78
79
  * Get all operations
79
- *
80
- * @returns {Promise<Array>} Array of all operations (chronological order)
80
+ *
81
+ * @returns {Promise<Array<any>>} Array of all operations (chronological order)
81
82
  */
82
83
  async list() {
83
84
  if (this.operations.length === 0 && this.headOperationId === null) {
84
85
  await this.load();
85
86
  }
86
-
87
+
87
88
  return this.operations;
88
89
  }
89
90
 
90
91
  /**
91
92
  * Get the head (latest) operation
92
- *
93
+ *
93
94
  * @returns {Promise<Object|null>} Latest operation or null if log is empty
94
95
  */
95
96
  async getHeadOperation() {
@@ -102,10 +103,10 @@ export class OperationLog {
102
103
 
103
104
  /**
104
105
  * Undo the last operation
105
- *
106
+ *
106
107
  * Returns the view from the previous operation, allowing the caller
107
108
  * to restore repository state.
108
- *
109
+ *
109
110
  * @returns {Promise<Object>} View from previous operation
110
111
  */
111
112
  async undo() {
@@ -128,10 +129,10 @@ export class OperationLog {
128
129
  // Return view from parent operation
129
130
  const currentHead = this.operations[this.operations.length - 1];
130
131
  const parentId = currentHead.parents[0];
131
-
132
+
132
133
  // Find parent operation
133
- const parentOp = this.operations.find(op => op.id === parentId);
134
-
134
+ const parentOp = this.operations.find((op) => op.id === parentId);
135
+
135
136
  if (!parentOp) {
136
137
  // If parent not found, return view from operation before current
137
138
  return this.operations[this.operations.length - 2].view;
@@ -142,7 +143,7 @@ export class OperationLog {
142
143
 
143
144
  /**
144
145
  * Get snapshot (view) at a specific operation
145
- *
146
+ *
146
147
  * @param {string} operationId - Operation ID
147
148
  * @returns {Promise<Object>} View from that operation
148
149
  */
@@ -151,8 +152,8 @@ export class OperationLog {
151
152
  await this.load();
152
153
  }
153
154
 
154
- const operation = this.operations.find(op => op.id === operationId);
155
-
155
+ const operation = this.operations.find((op) => op.id === operationId);
156
+
156
157
  if (!operation) {
157
158
  throw new JJError('OPERATION_NOT_FOUND', `Operation ${operationId} not found`, {
158
159
  operationId,
@@ -174,7 +175,7 @@ export class OperationLog {
174
175
  await this.load();
175
176
  }
176
177
 
177
- return this.operations.find(op => op.id === operationId) || null;
178
+ return this.operations.find((op) => op.id === operationId) || null;
178
179
  }
179
180
 
180
181
  /**
@@ -199,7 +200,7 @@ export class OperationLog {
199
200
  }
200
201
 
201
202
  // Find the operation
202
- const opIndex = this.operations.findIndex(op => op.id === operationId);
203
+ const opIndex = this.operations.findIndex((op) => op.id === operationId);
203
204
  if (opIndex === -1) {
204
205
  throw new JJError('OPERATION_NOT_FOUND', `Operation ${operationId} not found`, {
205
206
  operationId,
@@ -217,23 +218,20 @@ export class OperationLog {
217
218
  }
218
219
 
219
220
  // Find children (operations that reference this as a parent)
220
- const children = this.operations.filter(op =>
221
- op.parents && op.parents.includes(operationId)
222
- );
221
+ const children = this.operations.filter((op) => op.parents && op.parents.includes(operationId));
223
222
 
224
223
  // Get the parent of the operation being abandoned (may be undefined for root)
225
- const grandparentId = operation.parents && operation.parents.length > 0
226
- ? operation.parents[0]
227
- : undefined;
224
+ const grandparentId =
225
+ operation.parents && operation.parents.length > 0 ? operation.parents[0] : undefined;
228
226
 
229
227
  // Relink children to grandparent
230
228
  const relinkedChildren = [];
231
229
  for (const child of children) {
232
230
  // Replace references to abandoned operation with grandparent
233
231
  const oldParents = [...child.parents];
234
- child.parents = child.parents.map(p =>
235
- p === operationId ? grandparentId : p
236
- ).filter(Boolean); // Remove undefined if there was no grandparent
232
+ child.parents = child.parents
233
+ .map((/** @type {any} */ p) => (p === operationId ? grandparentId : p))
234
+ .filter(Boolean); // Remove undefined if there was no grandparent
237
235
 
238
236
  // If there was no grandparent, the child becomes a root operation
239
237
  if (!grandparentId && child.parents.length === 0) {
@@ -252,15 +250,14 @@ export class OperationLog {
252
250
 
253
251
  // Update head if we abandoned the head operation
254
252
  if (this.headOperationId === operationId) {
255
- this.headOperationId = this.operations.length > 0
256
- ? this.operations[this.operations.length - 1].id
257
- : null;
253
+ this.headOperationId =
254
+ this.operations.length > 0 ? this.operations[this.operations.length - 1].id : null;
258
255
  }
259
256
 
260
257
  // Rewrite JSONL file
261
- const content = this.operations
262
- .map(op => JSON.stringify(op))
263
- .join('\n') + (this.operations.length > 0 ? '\n' : '');
258
+ const content =
259
+ this.operations.map((op) => JSON.stringify(op)).join('\n') +
260
+ (this.operations.length > 0 ? '\n' : '');
264
261
  await this.storage.write('repo/op_log/oplog.jsonl', content);
265
262
 
266
263
  return {