codex-overleaf-link 2.2.1 → 2.3.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.
Files changed (71) hide show
  1. package/README.md +25 -23
  2. package/extension/bootstrap/manifest.template.json +1 -1
  3. package/extension/runtime-manifest.json +5 -71
  4. package/extension/src/background.js +53 -69
  5. package/extension/src/backgroundUpdateCoordinator.js +128 -66
  6. package/extension/src/content/applyResultFormatters.js +2 -3
  7. package/extension/src/content/contentRuntime.js +650 -1449
  8. package/extension/src/content/contextTray.js +4 -6
  9. package/extension/src/content/diagnosticsController.js +1 -2
  10. package/extension/src/content/diffReviewPanel.js +14 -13
  11. package/extension/src/content/generated/content.bundle.js +41370 -0
  12. package/extension/src/content/generated/content.bundle.meta.json +102 -0
  13. package/extension/src/content/legacyGlobalRegistry.js +129 -0
  14. package/extension/src/content/markdownText.js +5 -7
  15. package/extension/src/content/modelPicker.js +1 -1
  16. package/extension/src/content/modelPickerSupport.js +10 -5
  17. package/extension/src/content/moduleRegistryKernel.js +125 -0
  18. package/extension/src/content/nativeCompatibilityController.js +313 -0
  19. package/extension/src/content/otWarmMirror.js +7 -5
  20. package/extension/src/content/pageBridgeClient.js +224 -0
  21. package/extension/src/content/panelMaintenance.js +8 -3
  22. package/extension/src/content/postNavigationSettlementPersistence.js +100 -0
  23. package/extension/src/content/projectProviderSelection.js +4 -12
  24. package/extension/src/content/projectSessionCleanup.js +3 -3
  25. package/extension/src/content/providerSettingsCoordinator.js +59 -26
  26. package/extension/src/content/providerSettingsDialog.js +30 -18
  27. package/extension/src/content/recentProjects.js +15 -23
  28. package/extension/src/content/runGuidanceController.js +1 -1
  29. package/extension/src/content/runQueueScheduler.js +129 -29
  30. package/extension/src/content/runSettlementPersistence.js +51 -0
  31. package/extension/src/content/runTimelineView.js +11 -8
  32. package/extension/src/content/scopedPersistenceCoordinator.js +100 -0
  33. package/extension/src/content/scopedPersistenceDetachedCommit.js +47 -0
  34. package/extension/src/content/scopedPersistencePanelState.js +111 -0
  35. package/extension/src/content/sessionManager.js +15 -12
  36. package/extension/src/content/sessionPersistence.js +67 -8
  37. package/extension/src/content/updateNotice.js +10 -4
  38. package/extension/src/content/writebackOrchestrator.js +91 -23
  39. package/extension/src/contentScript.js +5 -1
  40. package/extension/src/page/trackedChangesLifecycle.js +6 -1
  41. package/extension/src/pageBridge.js +47 -98
  42. package/extension/src/shared/agentTranscript.js +8 -5
  43. package/extension/src/shared/compatibility.js +1 -1
  44. package/extension/src/shared/governanceRules.js +1 -1
  45. package/extension/src/shared/managedUpdateProjection.js +337 -0
  46. package/extension/src/shared/nativeRequestIdentity.js +60 -0
  47. package/extension/src/shared/otText.js +0 -19
  48. package/extension/src/shared/pageRpcContract.js +136 -0
  49. package/extension/src/shared/runExecutionSnapshot.js +177 -0
  50. package/extension/src/shared/runExecutionSnapshotCodec.js +168 -0
  51. package/extension/src/shared/runInputQueue.js +81 -34
  52. package/extension/src/shared/scopedPersistenceBrowserAdapter.js +79 -0
  53. package/extension/src/shared/scopedPersistenceQueuePolicy.js +181 -0
  54. package/extension/src/shared/scopedPersistenceTransaction.js +238 -0
  55. package/extension/src/shared/sessionState.js +64 -48
  56. package/extension/src/shared/settlementFacts.js +351 -0
  57. package/extension/src/shared/storageDb.js +19 -5
  58. package/extension/src/shared/storageMigration.js +275 -61
  59. package/extension/src/shared/storageSessionClaims.js +60 -0
  60. package/extension/src/shared/updateConsent.js +4 -0
  61. package/extension/src/shared/updateRevocationIntent.js +109 -0
  62. package/extension/src/shared/writebackEvidenceProjection.js +150 -0
  63. package/extension/src/shared/writebackOperationEvidence.js +143 -0
  64. package/extension/src/shared/writebackSettlement.js +705 -0
  65. package/native-host/src/codexRuntimeIdentity.js +1 -1
  66. package/native-host/src/codexSessionRunner.js +7 -13
  67. package/native-host/src/index.js +3 -69
  68. package/native-host/src/managedInstall.js +50 -8
  69. package/native-host/src/nativeTransportEnvelope.js +148 -0
  70. package/native-host/src/taskRunnerRuntime.js +3 -3
  71. package/package.json +13 -5
@@ -0,0 +1,705 @@
1
+ (function initCodexOverleafWritebackSettlement(root, factory) {
2
+ if (typeof module === 'object' && module.exports) {
3
+ module.exports = factory(
4
+ require('./failureReasons'),
5
+ require('./settlementFacts'),
6
+ require('./writebackEvidenceProjection'),
7
+ require('./writebackOperationEvidence')
8
+ );
9
+ } else {
10
+ root.CodexOverleafModuleRegistry.define('WritebackSettlement', [
11
+ 'FailureReasons',
12
+ 'SettlementFacts',
13
+ 'WritebackEvidenceProjection',
14
+ 'WritebackOperationEvidence'
15
+ ], factory);
16
+ }
17
+ })(typeof window !== 'undefined' ? window : globalThis, function writebackSettlementFactory(
18
+ DefaultFailureReasons,
19
+ SettlementFacts,
20
+ EvidenceProjection,
21
+ OperationEvidence
22
+ ) {
23
+ 'use strict';
24
+
25
+ const SETTLEMENT_FACTS_MAX_BYTES = SettlementFacts.SETTLEMENT_FACTS_MAX_BYTES;
26
+ const RECOVERY_FIELDS = SettlementFacts.RECOVERY_FIELDS;
27
+ const compactSettlementFacts = SettlementFacts.compactSettlementFacts;
28
+ const projectRunSettlement = SettlementFacts.projectRunSettlement;
29
+ const deriveLegacyProjection = SettlementFacts.deriveLegacyProjection;
30
+ const aggregateFileSettlements = EvidenceProjection.aggregateFileSettlements;
31
+ const deriveAppliedEvidence = EvidenceProjection.deriveAppliedEvidence;
32
+ const deriveApplyResultReadbacks = EvidenceProjection.deriveApplyResultReadbacks;
33
+ const deriveDocumentEffect = EvidenceProjection.deriveDocumentEffect;
34
+ const deriveReadbackEvidence = EvidenceProjection.deriveReadbackEvidence;
35
+ const normalizeFileReadbackEvidence = EvidenceProjection.normalizeFileReadbackEvidence;
36
+ const normalizeFailure = OperationEvidence.normalizeFailure;
37
+ const normalizeOperationEvidence = OperationEvidence.normalizeOperationEvidence;
38
+ const normalizeTrackedChanges = OperationEvidence.normalizeTrackedChanges;
39
+ const ACCEPT_NEEDS_REVIEW_CODES = new Set([
40
+ 'tracked_changes_remain',
41
+ 'accept_not_verified',
42
+ 'tracked_changes_created_unexpectedly',
43
+ 'accept_replay_created_tracked_changes',
44
+ 'write_observed_mismatch'
45
+ ]);
46
+ const REJECT_NEEDS_REVIEW_CODES = new Set([
47
+ 'undo_not_verified',
48
+ 'undo_operation_failed',
49
+ 'undo_reviewing_restore_unverified',
50
+ 'tracked_change_nodes_not_identified',
51
+ 'tracked_changes_remain',
52
+ 'write_observed_mismatch'
53
+ ]);
54
+ const CONTENT_FAILURE_CATALOG = {
55
+ project_snapshot_unavailable: {
56
+ stage: 'context', severity: 'error', defaultRetryable: true,
57
+ fallbackUserMessage: 'Codex could not read the Overleaf project snapshot.',
58
+ fallbackNextAction: 'Refresh Overleaf, then rerun the task.'
59
+ },
60
+ selected_context_unresolved: {
61
+ stage: 'context', severity: 'warning', defaultRetryable: true,
62
+ fallbackUserMessage: 'Codex could not resolve the requested selection or context.',
63
+ fallbackNextAction: 'Select the target again or specify the file/section explicitly.'
64
+ },
65
+ codex_no_usable_result: {
66
+ stage: 'codex', severity: 'error', defaultRetryable: true,
67
+ fallbackUserMessage: 'Local Codex returned no usable final report or operations.',
68
+ fallbackNextAction: 'Open Technical Details and resolve the local Codex error.'
69
+ },
70
+ codex_project_locked: {
71
+ stage: 'codex', severity: 'blocked', defaultRetryable: true,
72
+ fallbackUserMessage: 'Another Codex task is already running for this Overleaf project.',
73
+ fallbackNextAction: 'Wait for the active task to finish, or cancel it before retrying.'
74
+ },
75
+ storage_quota_exceeded: {
76
+ stage: 'storage', severity: 'warning', defaultRetryable: true,
77
+ fallbackUserMessage: 'Browser storage quota was exceeded.',
78
+ fallbackNextAction: 'Clear old run history or reduce attachments.'
79
+ },
80
+ native_bridge_unavailable: {
81
+ stage: 'native', severity: 'blocked', defaultRetryable: true,
82
+ fallbackUserMessage: 'Extension cannot connect to the Codex native host.',
83
+ fallbackNextAction: 'Run install-native or reload the extension.'
84
+ },
85
+ undo_not_verified: {
86
+ stage: 'undo', severity: 'warning', defaultRetryable: true,
87
+ fallbackUserMessage: 'Undo ran, but Codex could not prove the file returned to pre-run content.',
88
+ fallbackNextAction: 'Inspect the file manually before continuing.'
89
+ },
90
+ accept_not_verified: {
91
+ stage: 'accept', severity: 'warning', defaultRetryable: true,
92
+ fallbackUserMessage: 'Accept appeared to run but Codex could not prove final content/state.',
93
+ fallbackNextAction: 'Inspect Overleaf Reviewing before continuing.'
94
+ }
95
+ };
96
+ const ACCEPT_DIAGNOSTIC_TITLE_KEYS = Object.freeze({
97
+ editorUndo: 'runAcceptTrackedStepEditorUndo',
98
+ modeBefore: 'runAcceptTrackedStepModeBefore',
99
+ forceEditing: 'runAcceptTrackedStepForceEditing',
100
+ replayStart: 'runAcceptTrackedStepReplayStart',
101
+ replayDone: 'runAcceptTrackedStepReplayDone',
102
+ restoreReviewing: 'runAcceptTrackedStepRestoreReviewing'
103
+ });
104
+
105
+ function settle(input = {}) {
106
+ const operationEvidence = normalizeOperationEvidence(input.operations, input.applyResult);
107
+ const readbacks = input.readbacks === undefined
108
+ ? deriveApplyResultReadbacks(input.applyResult)
109
+ : input.readbacks;
110
+ const fileSettlements = aggregateFileSettlements(operationEvidence, readbacks);
111
+ const failures = operationEvidence.flatMap(entry => entry.failures);
112
+ const failureReasons = input.failureReasons || DefaultFailureReasons;
113
+ const primaryFailure = selectPrimaryFailure(failures, failureReasons);
114
+ const documentEffect = deriveDocumentEffect(operationEvidence);
115
+ const recovery = preserveRecovery();
116
+ const facts = compactSettlementFacts({
117
+ schemaVersion: 1,
118
+ documentEffect,
119
+ evidence: {
120
+ applied: deriveAppliedEvidence(input.operations, operationEvidence),
121
+ readBack: deriveReadbackEvidence(fileSettlements, readbacks),
122
+ saved: normalizeSaveEvidence(input.saveVerification).confidence,
123
+ mirrored: normalizeMirrorEvidence(input.mirror),
124
+ compiled: normalizeCompileEvidence(input.compile),
125
+ settled: deriveSettledEvidence(primaryFailure, fileSettlements)
126
+ },
127
+ fileSettlements,
128
+ failures,
129
+ recoveryAvailability: deriveRecoveryAvailability(input.recovery, operationEvidence),
130
+ recoveryDisposition: projectRecoveryDisposition(recovery),
131
+ capturedAt: normalizeTimestamp(input.capturedAt)
132
+ });
133
+ return {
134
+ schemaVersion: 1,
135
+ statePatch: {},
136
+ recovery,
137
+ facts
138
+ };
139
+ }
140
+
141
+ function normalizeSaveEvidence(value = {}) {
142
+ if (value?.state === 'verified_saved') {
143
+ return { state: 'verified_saved', confidence: 'verified' };
144
+ }
145
+ if (value?.state === 'verified_quiet' || (value?.ok === true && !value?.state)) {
146
+ return { state: 'verified_quiet', confidence: 'quiet-observed' };
147
+ }
148
+ const state = normalizeText(value?.state);
149
+ if (state === 'not_required' || state === 'not-required') {
150
+ return { state, confidence: 'not-required' };
151
+ }
152
+ if (state === 'unavailable') {
153
+ return { state, confidence: 'unavailable' };
154
+ }
155
+ if (value?.ok === false) {
156
+ return { state: state || 'failed', confidence: 'failed' };
157
+ }
158
+ if (!state) {
159
+ return { state: 'not_started', confidence: 'not-attempted' };
160
+ }
161
+ return {
162
+ state,
163
+ confidence: 'unavailable'
164
+ };
165
+ }
166
+
167
+ function normalizeAuxiliaryEvidence(value = {}) {
168
+ if (!value || typeof value !== 'object') {
169
+ return { state: 'not_started' };
170
+ }
171
+ return {
172
+ state: normalizeText(value.state || value.status) || 'unknown',
173
+ reason: normalizeText(value.reason)
174
+ };
175
+ }
176
+
177
+ function normalizeMirrorEvidence(value = {}) {
178
+ const state = normalizeText(value?.state || value?.status);
179
+ if (state === 'pending') return 'not-attempted';
180
+ return ['not-attempted', 'complete', 'partial', 'failed', 'unknown'].includes(state)
181
+ ? state
182
+ : (state === 'not_started' || !state ? 'not-attempted' : 'unknown');
183
+ }
184
+
185
+ function normalizeCompileEvidence(value = {}) {
186
+ const state = normalizeText(value?.state || value?.status);
187
+ if (state === 'success' || state === 'completed') return 'succeeded';
188
+ return ['not-attempted', 'succeeded', 'failed', 'unknown'].includes(state)
189
+ ? state
190
+ : (state === 'not_started' || !state ? 'not-attempted' : 'unknown');
191
+ }
192
+
193
+ function deriveSettledEvidence(primaryFailure, fileSettlements = []) {
194
+ if (primaryFailure?.terminalState === 'needs_review'
195
+ || fileSettlements.some(file => file.readBack === 'mismatch')) {
196
+ return 'needs-review';
197
+ }
198
+ if (primaryFailure) return 'partial';
199
+ return 'complete';
200
+ }
201
+
202
+ function deriveRecoveryAvailability(recovery = {}, operationEvidence = []) {
203
+ const value = recovery && typeof recovery === 'object' ? recovery : {};
204
+ return {
205
+ trackedChanges: hasItems(value.undoTrackedChanges)
206
+ || operationEvidence.some(entry => entry.trackedChanges.length > 0),
207
+ editorHistory: hasItems(value.undoOperations)
208
+ || hasItems(value.appliedOperations)
209
+ || operationEvidence.some(entry => entry.undoIds.length > 0),
210
+ legacyBaseline: hasItems(value.undoBaseFiles)
211
+ || hasItems(value.undoExpectedFiles)
212
+ || operationEvidence.some(entry => entry.baseAvailable || entry.expectedAvailable)
213
+ };
214
+ }
215
+
216
+ function projectRecoveryDisposition(recovery = {}) {
217
+ return Object.fromEntries(RECOVERY_FIELDS.map(field => [
218
+ field,
219
+ recovery[field]?.kind === 'clear' ? 'clear' : 'preserve'
220
+ ]));
221
+ }
222
+
223
+ function hasItems(value) {
224
+ if (Array.isArray(value)) return value.length > 0;
225
+ return Boolean(value && typeof value === 'object' && Object.keys(value).length);
226
+ }
227
+
228
+ function settlePostNavigation(input = {}) {
229
+ if (input.navigation?.occurred !== true) return null;
230
+ if (input.navigation?.identityGuardFailed === true) {
231
+ return 'abandoned_after_navigation';
232
+ }
233
+ const skipped = collectRunResultSkipped(input.runResult);
234
+ if (skipped.some(entry => {
235
+ const code = entry?.result?.code || entry?.result?.failure?.code || entry?.failure?.code;
236
+ return code === 'aborted_project_changed' || code === 'editor_project_id_unavailable';
237
+ })) {
238
+ return 'abandoned_after_navigation';
239
+ }
240
+ if (input.runResult?.hasSkippedOperations === true || skipped.length > 0) {
241
+ return 'needs_review_after_navigation';
242
+ }
243
+ return 'background_completed';
244
+ }
245
+
246
+ function collectRunResultSkipped(value = {}) {
247
+ return [
248
+ ...(Array.isArray(value?.applied?.skipped) ? value.applied.skipped : []),
249
+ ...(Array.isArray(value?.skipped) ? value.skipped : [])
250
+ ].filter(Boolean);
251
+ }
252
+
253
+ function settleTrackedChangeLifecycle(input = {}) {
254
+ const kind = input.kind === 'reject' ? 'reject' : 'accept';
255
+ const failures = collectFailuresFromResult(input.result, input.failureReasons);
256
+ const primary = selectPrimaryFailure(failures, input.failureReasons);
257
+ if (primary?.terminalState === 'blocked' || primary?.severity === 'blocked') {
258
+ return {
259
+ decision: 'blocked',
260
+ statePatch: {},
261
+ recovery: preserveRecovery(),
262
+ facts: {
263
+ documentEffect: primary?.changedDocument === true ? 'possibly-changed' : 'unknown',
264
+ failures,
265
+ evidence: { settled: 'needs-review' },
266
+ recoveryAvailability: deriveRecoveryAvailability(input.run),
267
+ recoveryDisposition: projectRecoveryDisposition(preserveRecovery())
268
+ }
269
+ };
270
+ }
271
+ const successful = isSuccessfulTrackedChangeSettlement(input.result);
272
+ const reviewCodes = kind === 'accept' ? ACCEPT_NEEDS_REVIEW_CODES : REJECT_NEEDS_REVIEW_CODES;
273
+ const needsReview = !successful && failures.some(failure =>
274
+ failure.terminalState === 'needs_review' || reviewCodes.has(failure.code)
275
+ );
276
+ const status = needsReview ? 'needs_review' : kind === 'accept' ? 'accepted' : 'rejected';
277
+ return {
278
+ decision: status,
279
+ statePatch: { trackedChangeStatus: status },
280
+ recovery: status === 'accepted' || status === 'rejected'
281
+ ? terminalTrackedChangeRecovery()
282
+ : preserveRecovery(),
283
+ facts: {
284
+ documentEffect: failures.some(failure => failure?.changedDocument === true)
285
+ ? 'possibly-changed'
286
+ : 'unknown',
287
+ failures,
288
+ evidence: { settled: needsReview ? 'needs-review' : 'complete' },
289
+ recoveryAvailability: deriveRecoveryAvailability(input.run),
290
+ recoveryDisposition: projectRecoveryDisposition(
291
+ status === 'accepted' || status === 'rejected'
292
+ ? terminalTrackedChangeRecovery()
293
+ : preserveRecovery()
294
+ )
295
+ }
296
+ };
297
+ }
298
+
299
+ function transitionTrackedChangeStatus(status) {
300
+ const recovery = status === 'accepted' || status === 'rejected'
301
+ ? terminalTrackedChangeRecovery()
302
+ : preserveRecovery();
303
+ return {
304
+ statePatch: { trackedChangeStatus: status },
305
+ recovery,
306
+ facts: {
307
+ evidence: { settled: status === 'needs_review' ? 'needs-review' : 'complete' },
308
+ recoveryDisposition: projectRecoveryDisposition(recovery)
309
+ }
310
+ };
311
+ }
312
+
313
+ function settleLegacyUndo(input = {}) {
314
+ const status = input.status === 'applied' ? 'applied' : 'partial';
315
+ const recovery = preserveRecovery();
316
+ return {
317
+ statePatch: { undoStatus: status },
318
+ recovery,
319
+ facts: {
320
+ documentEffect: status === 'applied' ? 'changed' : 'possibly-changed',
321
+ failures: collectFailuresFromResult(input.result, input.failureReasons),
322
+ evidence: { settled: status === 'applied' ? 'complete' : 'needs-review' },
323
+ recoveryAvailability: deriveRecoveryAvailability(input.run),
324
+ recoveryDisposition: projectRecoveryDisposition(recovery)
325
+ }
326
+ };
327
+ }
328
+
329
+ function transitionPostNavigationStatus(input = {}) {
330
+ const status = normalizeText(input.status);
331
+ const recovery = preserveRecovery();
332
+ return {
333
+ statePatch: {
334
+ status,
335
+ statusText: normalizeText(input.statusText),
336
+ finishedAt: normalizeText(input.finishedAt)
337
+ },
338
+ recovery,
339
+ facts: {
340
+ evidence: {
341
+ settled: status === 'background_completed' ? 'complete' : 'needs-review'
342
+ },
343
+ recoveryDisposition: projectRecoveryDisposition(recovery)
344
+ }
345
+ };
346
+ }
347
+
348
+ function applySettlementTransition(currentRun = {}, settlementResult = {}) {
349
+ const next = cloneValue(currentRun);
350
+ const patch = settlementResult.statePatch && typeof settlementResult.statePatch === 'object'
351
+ ? settlementResult.statePatch
352
+ : {};
353
+ Object.assign(next, cloneValue(patch));
354
+ const recovery = settlementResult.recovery || {};
355
+ for (const field of RECOVERY_FIELDS) {
356
+ const disposition = recovery[field] || { kind: 'preserve' };
357
+ if (disposition.kind === 'clear') {
358
+ next[field] = Array.isArray(next[field]) ? [] : '';
359
+ } else if (disposition.kind === 'replace') {
360
+ next[field] = cloneValue(disposition.value);
361
+ }
362
+ }
363
+ if (settlementResult.facts) {
364
+ const existingSettlement = next.settlement && typeof next.settlement === 'object'
365
+ ? next.settlement
366
+ : next.settlementFacts;
367
+ next.settlement = compactSettlementFacts({
368
+ ...(existingSettlement && typeof existingSettlement === 'object' ? existingSettlement : {}),
369
+ ...settlementResult.facts,
370
+ evidence: {
371
+ ...(existingSettlement?.evidence || {}),
372
+ ...(settlementResult.facts.evidence || {})
373
+ },
374
+ recoveryAvailability: {
375
+ ...(existingSettlement?.recoveryAvailability || {}),
376
+ ...(settlementResult.facts.recoveryAvailability || {})
377
+ },
378
+ recoveryDisposition: {
379
+ ...(existingSettlement?.recoveryDisposition || {}),
380
+ ...(settlementResult.facts.recoveryDisposition || {})
381
+ }
382
+ });
383
+ delete next.settlementFacts;
384
+ }
385
+ return next;
386
+ }
387
+
388
+ function attachUndoNotVerifiedFailure(run, result, options = {}) {
389
+ if (!result || typeof result !== 'object' || result.ok === false) return result;
390
+ if (Array.isArray(result.skipped) && result.skipped.length > 0) return result;
391
+ const expectedFiles = Array.isArray(run?.undoExpectedFiles) ? run.undoExpectedFiles : [];
392
+ if (!expectedFiles.length || isUndoVerifiedContentMatching(run, result)) return result;
393
+ const path = expectedFiles.find(entry => typeof entry?.path === 'string')?.path || '';
394
+ appendSyntheticFailure(result, options.buildFailure?.('undo_not_verified', { path, type: 'undo' }, {
395
+ changedDocument: true,
396
+ terminalState: 'needs_review',
397
+ evidence: { undoApplied: true, verified: false, expectedFileCount: expectedFiles.length }
398
+ }), path, 'undo_not_verified', 'undo');
399
+ return result;
400
+ }
401
+
402
+ function isUndoVerifiedContentMatching(run, result) {
403
+ const expectedByPath = new Map((run?.undoExpectedFiles || [])
404
+ .filter(file => typeof file?.path === 'string' && typeof file?.content === 'string')
405
+ .map(file => [normalizeSettlementPath(file.path), file.content])
406
+ .filter(([path]) => path));
407
+ if (!expectedByPath.size) return true;
408
+ const applied = Array.isArray(result?.applied) ? result.applied : [];
409
+ return Array.from(expectedByPath).every(([path, expected]) =>
410
+ applied.some(entry => isVerifiedUndoEntryForPath(entry, path, expected))
411
+ );
412
+ }
413
+
414
+ function isUndoResultEffectivelyApplied(run, result, undoOperations = []) {
415
+ if (!result?.skipped?.length) return true;
416
+ const expectedByPath = new Map((run?.undoExpectedFiles || [])
417
+ .filter(file => file?.path && typeof file.content === 'string')
418
+ .map(file => [normalizeSettlementPath(file.path), file.content])
419
+ .filter(([path]) => path));
420
+ if (!expectedByPath.size) return false;
421
+ const editPaths = Array.from(new Set((Array.isArray(undoOperations) ? undoOperations : [])
422
+ .filter(operation =>
423
+ operation?.type === 'edit'
424
+ && operation.path
425
+ && expectedByPath.has(normalizeSettlementPath(operation.path))
426
+ )
427
+ .map(operation => normalizeSettlementPath(operation.path))));
428
+ return editPaths.length > 0 && editPaths.every(path => (result.applied || []).some(item =>
429
+ isVerifiedUndoEntryForPath(item, path, expectedByPath.get(path))
430
+ ));
431
+ }
432
+
433
+ function isVerifiedUndoEntryForPath(entry, path, expectedContent) {
434
+ const inner = entry?.result || {};
435
+ if (inner.ok === false) return false;
436
+ const normalizedPath = normalizeSettlementPath(path);
437
+ const operationPath = normalizeSettlementPath(entry?.operation?.path);
438
+ if (entry?.operation?.type === 'edit' && operationPath === normalizedPath) {
439
+ return inner.verifiedContent === expectedContent;
440
+ }
441
+
442
+ const trackedPath = normalizeSettlementPath(entry?.trackedChange?.path);
443
+ if (trackedPath !== normalizedPath) return false;
444
+ const key = String(entry?.trackedChange?.key || '');
445
+ if (key === `editor-undo:${normalizedPath}`) {
446
+ return inner.method === 'overleaf-editor-undo'
447
+ && inner.verified === true
448
+ && inner.verifiedContent === expectedContent;
449
+ }
450
+ if (key === `snapshot-undo:${normalizedPath}`) {
451
+ return inner.verifiedContent === expectedContent;
452
+ }
453
+ return false;
454
+ }
455
+
456
+ function normalizeSettlementPath(value) {
457
+ return String(value || '')
458
+ .replace(/\\/g, '/')
459
+ .replace(/\/+/g, '/')
460
+ .replace(/^\/+|\/+$/g, '');
461
+ }
462
+
463
+ function attachAcceptNotVerifiedFailure(run, result, options = {}) {
464
+ if (!result || typeof result !== 'object' || result.ok === false) return result;
465
+ if (Array.isArray(result.skipped) && result.skipped.length > 0) return result;
466
+ if (isAcceptResultEffectivelyVerified(run, result)) return result;
467
+ const expectedFiles = Array.isArray(run?.undoExpectedFiles) ? run.undoExpectedFiles : [];
468
+ const path = expectedFiles.find(entry => typeof entry?.path === 'string')?.path
469
+ || (run?.undoTrackedChanges || []).find(change => typeof change?.path === 'string')?.path
470
+ || '';
471
+ appendSyntheticFailure(result, options.buildFailure?.('accept_not_verified', { path, type: 'accept' }, {
472
+ changedDocument: true,
473
+ terminalState: 'needs_review',
474
+ evidence: {
475
+ acceptApplied: true,
476
+ verified: false,
477
+ expectedFileCount: expectedFiles.length,
478
+ trackedChangeCount: Array.isArray(run?.undoTrackedChanges) ? run.undoTrackedChanges.length : 0
479
+ }
480
+ }), path, 'accept_not_verified', 'accept');
481
+ return result;
482
+ }
483
+
484
+ function isAcceptResultEffectivelyVerified(run, result) {
485
+ if (!result || typeof result !== 'object') return true;
486
+ const expectedFiles = Array.isArray(run?.undoExpectedFiles) ? run.undoExpectedFiles : [];
487
+ if (!expectedFiles.length || result.verified === true) return true;
488
+ const trackedChanges = Array.isArray(run?.undoTrackedChanges) ? run.undoTrackedChanges : [];
489
+ if (!trackedChanges.length) return true;
490
+ const applied = Array.isArray(result.applied) ? result.applied : [];
491
+
492
+ // The current Accept lifecycle first removes this run's tracked writeback,
493
+ // then replays each changed file in Editing mode. Its successful evidence
494
+ // is therefore file-scoped (`accept-replay:<path>`), while
495
+ // `undoTrackedChanges` contains the many node-scoped refs captured before
496
+ // the replay. Requiring every original node key to reappear in `applied`
497
+ // produces a false accept_not_verified result even when the page verified
498
+ // the final replayed content.
499
+ const normalizePath = value => String(value || '')
500
+ .replace(/\\/g, '/')
501
+ .replace(/\/+/g, '/')
502
+ .replace(/^\/+|\/+$/g, '');
503
+ const replayEntries = applied.filter(entry =>
504
+ String(entry?.trackedChange?.key || '').startsWith('accept-replay:')
505
+ );
506
+ if (replayEntries.length > 0) {
507
+ const expectedPaths = Array.from(new Set(expectedFiles
508
+ .map(file => normalizePath(file?.path))
509
+ .filter(Boolean)));
510
+ return expectedPaths.length > 0 && expectedPaths.every(path =>
511
+ replayEntries.some(entry => {
512
+ const inner = entry?.result || {};
513
+ return normalizePath(entry?.trackedChange?.path) === path
514
+ && inner.ok !== false
515
+ && (inner.verified === true || typeof inner.verifiedContent === 'string');
516
+ })
517
+ );
518
+ }
519
+
520
+ // Compatibility path for the earlier direct-node Accept implementation.
521
+ return trackedChanges.every(change => {
522
+ const key = change?.key || change?.id || change?.label;
523
+ return Boolean(key) && applied.some(entry => {
524
+ const ref = entry?.trackedChange;
525
+ return (ref?.key || ref?.id || ref?.label) === key && entry?.result?.ok !== false;
526
+ });
527
+ });
528
+ }
529
+
530
+ function collectFailuresFromResult(result, failureReasons = DefaultFailureReasons) {
531
+ const failures = [];
532
+ const applied = collectTrackedChangeAppliedEntries(result);
533
+ const skipped = collectRunResultSkipped(result);
534
+ const failedEntries = [
535
+ ...skipped,
536
+ ...applied.filter(item => (item?.result || item)?.ok === false)
537
+ ];
538
+ if (result?.ok === false && failedEntries.length === 0) failedEntries.push(result);
539
+ for (const entry of failedEntries) {
540
+ const inner = entry?.result || entry;
541
+ if (!inner || inner.ok === true) continue;
542
+ const operation = entry?.operation || (entry?.trackedChange ? { path: entry.trackedChange.path } : undefined);
543
+ failures.push(failureReasons?.normalizeFailureReason instanceof Function
544
+ ? failureReasons.normalizeFailureReason(inner, operation)
545
+ : normalizeFailure(inner, operation));
546
+ }
547
+ return failures.filter(Boolean);
548
+ }
549
+
550
+ function collectTrackedChangeAppliedEntries(result) { return Array.isArray(result?.applied) ? result.applied : (Array.isArray(result?.applied?.applied) ? result.applied.applied : []); }
551
+
552
+ function isSuccessfulTrackedChangeSettlement(result) {
553
+ if (!result || typeof result !== 'object') return false;
554
+ const hasSuccessfulAppliedEntry = collectTrackedChangeAppliedEntries(result).some(entry => {
555
+ const inner = entry?.result || entry;
556
+ return !inner || inner.ok !== false;
557
+ });
558
+ if (hasSuccessfulAppliedEntry) return true;
559
+ return result.ok === true;
560
+ }
561
+
562
+ function buildContentFailure(code, operation = {}, overrides = {}, failureReasons = DefaultFailureReasons) {
563
+ const entry = CONTENT_FAILURE_CATALOG[code] || failureReasons?.FAILURE_CODE_CATALOG?.[code];
564
+ if (!entry) return null;
565
+ const failure = {
566
+ code,
567
+ stage: entry.stage,
568
+ severity: entry.severity,
569
+ userMessage: overrides.userMessage || entry.fallbackUserMessage,
570
+ retryable: overrides.retryable === undefined ? entry.defaultRetryable : overrides.retryable === true,
571
+ nextAction: overrides.nextAction || entry.fallbackNextAction
572
+ };
573
+ const file = overrides.file !== undefined ? overrides.file : operation?.path;
574
+ const operationType = overrides.operationType !== undefined ? overrides.operationType : operation?.type;
575
+ if (file) failure.file = file;
576
+ if (operationType) failure.operationType = operationType;
577
+ for (const field of [
578
+ 'activeFile',
579
+ 'terminalState',
580
+ 'technicalMessage',
581
+ 'evidence'
582
+ ]) {
583
+ if (overrides[field] !== undefined) failure[field] = overrides[field];
584
+ }
585
+ if (overrides.changedDocument !== undefined) {
586
+ failure.changedDocument = overrides.changedDocument === true;
587
+ }
588
+ return failure;
589
+ }
590
+
591
+ function attachVerifiedContentToOperation(operation, result) {
592
+ if (!operation || typeof operation !== 'object') return operation;
593
+ return operation.type === 'edit' && typeof result?.verifiedContent === 'string'
594
+ ? { ...operation, verifiedContent: result.verifiedContent }
595
+ : operation;
596
+ }
597
+
598
+ function selectExpectedFilesForTrackedUndo(project, operations = [], trackedChanges = []) {
599
+ const paths = new Set();
600
+ for (const change of trackedChanges || []) {
601
+ if (change?.path) paths.add(change.path);
602
+ }
603
+ for (const operation of operations || []) {
604
+ if (operation?.path) paths.add(operation.path);
605
+ if (operation?.to) paths.add(operation.to);
606
+ }
607
+ return (project?.files || [])
608
+ .filter(file => paths.has(file.path) && typeof file.content === 'string')
609
+ .map(file => ({ path: file.path, content: file.content }));
610
+ }
611
+
612
+ function acceptDiagnosticStatus(step, info) {
613
+ if (step === 'modeBefore' || step === 'replayStart') return 'info';
614
+ if (step === 'editorUndo'
615
+ || step === 'forceEditing'
616
+ || step === 'restoreReviewing'
617
+ || step === 'replayDone') {
618
+ return info?.ok === true ? 'info' : 'failed';
619
+ }
620
+ return 'info';
621
+ }
622
+
623
+ function selectPrimaryFailure(failures, failureReasons) {
624
+ return failureReasons?.selectPrimaryFailure instanceof Function
625
+ ? failureReasons.selectPrimaryFailure(failures)
626
+ : failures[0] || null;
627
+ }
628
+
629
+ function appendSyntheticFailure(result, failure, path, code, type) {
630
+ const normalized = failure || {
631
+ code,
632
+ stage: type,
633
+ severity: 'warning',
634
+ userMessage: code,
635
+ retryable: true,
636
+ file: path,
637
+ changedDocument: true,
638
+ terminalState: 'needs_review'
639
+ };
640
+ if (!Array.isArray(result.skipped)) result.skipped = [];
641
+ result.skipped.push({
642
+ operation: { path, type },
643
+ result: { ok: false, code, reason: normalized.userMessage, failure: normalized }
644
+ });
645
+ result.ok = false;
646
+ }
647
+
648
+ function preserveRecovery() {
649
+ return Object.fromEntries(RECOVERY_FIELDS.map(field => [field, { kind: 'preserve' }]));
650
+ }
651
+
652
+ function terminalTrackedChangeRecovery() {
653
+ const recovery = preserveRecovery();
654
+ recovery.undoTrackedChanges = { kind: 'clear' };
655
+ recovery.undoExpectedFiles = { kind: 'clear' };
656
+ return recovery;
657
+ }
658
+
659
+ function cloneValue(value) {
660
+ if (value === undefined) return undefined;
661
+ return JSON.parse(JSON.stringify(value));
662
+ }
663
+
664
+ function normalizeText(value) {
665
+ return typeof value === 'string' ? value.trim() : '';
666
+ }
667
+
668
+ function normalizeTimestamp(value) {
669
+ return typeof value === 'string' && value ? value : new Date().toISOString();
670
+ }
671
+
672
+ return {
673
+ ACCEPT_DIAGNOSTIC_TITLE_KEYS,
674
+ RECOVERY_FIELDS,
675
+ SETTLEMENT_FACTS_MAX_BYTES,
676
+ aggregateFileSettlements,
677
+ applySettlementTransition,
678
+ acceptDiagnosticStatus,
679
+ attachVerifiedContentToOperation,
680
+ attachAcceptNotVerifiedFailure,
681
+ attachUndoNotVerifiedFailure,
682
+ collectFailuresFromResult,
683
+ collectRunResultSkipped,
684
+ compactSettlementFacts,
685
+ deriveApplyResultReadbacks,
686
+ deriveLegacyProjection,
687
+ buildContentFailure,
688
+ isAcceptResultEffectivelyVerified,
689
+ isSuccessfulTrackedChangeSettlement,
690
+ isUndoResultEffectivelyApplied,
691
+ isUndoVerifiedContentMatching,
692
+ normalizeApplyTrackedChanges: normalizeTrackedChanges,
693
+ normalizeFileReadbackEvidence,
694
+ normalizeOperationEvidence,
695
+ normalizeSaveEvidence,
696
+ projectRunSettlement,
697
+ selectExpectedFilesForTrackedUndo,
698
+ settle,
699
+ settleLegacyUndo,
700
+ settlePostNavigation,
701
+ settleTrackedChangeLifecycle,
702
+ transitionPostNavigationStatus,
703
+ transitionTrackedChangeStatus
704
+ };
705
+ });