codex-overleaf-link 2.5.1 → 2.6.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.
- package/README.md +149 -398
- package/README.zh-CN.md +153 -404
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/background.js +78 -3
- package/extension/src/backgroundSharedSessions.js +55 -2
- package/extension/src/backgroundUpdateCoordinator.js +128 -12
- package/extension/src/content/activeTurnControl.js +164 -7
- package/extension/src/content/composerAttachments.js +11 -2
- package/extension/src/content/composerPanel.js +92 -12
- package/extension/src/content/contentRuntime.js +920 -276
- package/extension/src/content/contextTray.js +8 -2
- package/extension/src/content/contextUsageView.js +267 -0
- package/extension/src/content/diagnosticsPanel.js +18 -0
- package/extension/src/content/generated/content.bundle.js +4496 -924
- package/extension/src/content/generated/content.bundle.meta.json +6 -3
- package/extension/src/content/legacyGlobalRegistry.js +4 -0
- package/extension/src/content/localSkillsPanel.js +2 -2
- package/extension/src/content/markdownDomRenderer.js +5 -3
- package/extension/src/content/markdownText.js +42 -5
- package/extension/src/content/modelPicker.js +1 -0
- package/extension/src/content/nativeCompatibilityController.js +29 -10
- package/extension/src/content/otWarmMirror.js +1 -1
- package/extension/src/content/pageBridgeClient.js +2 -2
- package/extension/src/content/panelMaintenance.js +6 -2
- package/extension/src/content/panelRenderer.js +51 -27
- package/extension/src/content/pendingInputView.js +1 -1
- package/extension/src/content/projectSettingsCoordinator.js +30 -6
- package/extension/src/content/providerSettingsDialog.js +2 -1
- package/extension/src/content/recentProjects.js +4 -3
- package/extension/src/content/runActivitySummary.js +122 -14
- package/extension/src/content/runCompletionMeta.js +194 -6
- package/extension/src/content/runController.js +2 -1
- package/extension/src/content/runFailureNotice.js +8 -1
- package/extension/src/content/runPresence.js +18 -8
- package/extension/src/content/runQueueScheduler.js +9 -5
- package/extension/src/content/runRecoveryActions.js +2 -1
- package/extension/src/content/runResultActions.js +22 -20
- package/extension/src/content/runReviewController.js +94 -35
- package/extension/src/content/runTimelineView.js +224 -25
- package/extension/src/content/scopedPersistencePanelState.js +92 -23
- package/extension/src/content/sensitivePreflight.js +14 -1
- package/extension/src/content/sessionManager.js +49 -3
- package/extension/src/content/sessionMenuView.js +54 -18
- package/extension/src/content/sessionPersistence.js +25 -5
- package/extension/src/content/settingsPanel.js +15 -2
- package/extension/src/content/settingsWorkbench.js +127 -89
- package/extension/src/content/sharedSessionViewSync.js +4 -3
- package/extension/src/content/subagentActivityView.js +23 -7
- package/extension/src/content/updateNotice.js +60 -6
- package/extension/src/content/writebackOrchestrator.js +35 -22
- package/extension/src/page/compileBridge.js +193 -180
- package/extension/src/page/overleafCapabilities.js +4 -1
- package/extension/src/page/textCoordinates.js +17 -1
- package/extension/src/page/textFileCreator.js +9 -1
- package/extension/src/page/writebackRouter.js +24 -77
- package/extension/src/pageBridge.js +107 -66
- package/extension/src/popup.js +1 -1
- package/extension/src/shared/agentTranscript.js +350 -17
- package/extension/src/shared/auditRecords.js +2 -0
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/compileAdapter.js +58 -17
- package/extension/src/shared/composerAttachmentDraft.js +6 -1
- package/extension/src/shared/contextUsage.js +125 -0
- package/extension/src/shared/failureReasons.js +42 -3
- package/extension/src/shared/i18n.js +299 -29
- package/extension/src/shared/lineReferences.js +38 -31
- package/extension/src/shared/pageRpcContract.js +1 -1
- package/extension/src/shared/panelIcons.js +39 -0
- package/extension/src/shared/reviewing.js +130 -1
- package/extension/src/shared/runActivityModel.js +70 -4
- package/extension/src/shared/runExecutionSnapshot.js +13 -0
- package/extension/src/shared/sessionState.js +63 -4
- package/extension/src/shared/sessionTitle.js +14 -1
- package/extension/src/shared/sharedSessionBridge.js +81 -13
- package/extension/src/shared/staleGuard.js +72 -63
- package/extension/src/shared/storageDb.js +35 -3
- package/extension/src/shared/storageMigration.js +7 -1
- package/extension/src/shared/storageRunActions.js +62 -8
- package/extension/src/shared/undoOperations.js +126 -28
- package/extension/src/shared/updateRuntimeIdentity.js +16 -2
- package/extension/src/shared/writebackSettlement.js +21 -3
- package/extension/styles/panel.css +1106 -490
- package/native-host/src/bootstrapUpdate.js +52 -16
- package/native-host/src/chatBridgeRequest.js +60 -6
- package/native-host/src/chatBridgeResponse.js +10 -1
- package/native-host/src/chatCompletionsBridge.js +29 -2
- package/native-host/src/codexPromptAssembly.js +86 -118
- package/native-host/src/codexSessionRunner.js +126 -17
- package/native-host/src/providerContextError.js +20 -0
- package/native-host/src/taskRunnerRuntime.js +18 -6
- package/native-host/src/textPatch.js +473 -377
- package/native-host/src/updateManager.js +120 -15
- package/package.json +5 -8
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
'experimentalOtByProject',
|
|
20
20
|
'customInstructionsByProject',
|
|
21
21
|
'governanceRulesByProject',
|
|
22
|
+
'reviewingAvailabilityByProject',
|
|
22
23
|
'selectedLocalSkillIdsByProject'
|
|
23
24
|
];
|
|
24
25
|
var projectPreferenceLockTails = {};
|
|
@@ -130,7 +131,12 @@
|
|
|
130
131
|
? incoming[mapName]
|
|
131
132
|
: {};
|
|
132
133
|
if (Object.prototype.hasOwnProperty.call(incomingMap, projectId)) {
|
|
133
|
-
storedMap[scopedKey] =
|
|
134
|
+
const previous = storedMap[scopedKey], observation = incomingMap[projectId];
|
|
135
|
+
if (mapName !== 'reviewingAvailabilityByProject'
|
|
136
|
+
|| !(Number(previous?.observedAt) > Number(observation?.observedAt)
|
|
137
|
+
|| (previous?.observedAt === observation?.observedAt && previous?.status === 'available'))) {
|
|
138
|
+
storedMap[scopedKey] = copyPreferenceValue(observation);
|
|
139
|
+
}
|
|
134
140
|
}
|
|
135
141
|
delete storedMap[projectId];
|
|
136
142
|
next[mapName] = storedMap;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
(function initStorageRunActions(root, factory) {
|
|
2
2
|
if (typeof module === 'object' && module.exports) {
|
|
3
|
-
module.exports = factory(require('./selectionContext'), require('./writebackIntent'), require('./writingStyle'), require('./directoryCleanupProof'));
|
|
3
|
+
module.exports = factory(require('./selectionContext'), require('./writebackIntent'), require('./writingStyle'), require('./directoryCleanupProof'), require('./undoOperations'));
|
|
4
4
|
} else {
|
|
5
|
-
root.CodexOverleafModuleRegistry.define('StorageRunActions', ['SelectionContext', 'WritebackIntent', 'WritingStyle', 'DirectoryCleanupProof'], factory);
|
|
5
|
+
root.CodexOverleafModuleRegistry.define('StorageRunActions', ['SelectionContext', 'WritebackIntent', 'WritingStyle', 'DirectoryCleanupProof', 'UndoOperations'], factory);
|
|
6
6
|
}
|
|
7
|
-
})(typeof globalThis !== 'undefined' ? globalThis : window, function storageRunActionsFactory(SelectionContext, WritebackIntent, WritingStyle, DirectoryCleanupProof) {
|
|
7
|
+
})(typeof globalThis !== 'undefined' ? globalThis : window, function storageRunActionsFactory(SelectionContext, WritebackIntent, WritingStyle, DirectoryCleanupProof, UndoOperations) {
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
// Share a bounded budget across the retained history, rather than expiring
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
&& Array.isArray(run.undoExpectedFiles) && run.undoExpectedFiles.length > 0
|
|
130
130
|
&& Array.isArray(run.appliedOperations) && run.appliedOperations.length > 0;
|
|
131
131
|
var legacyUndo = Array.isArray(run && run.undoOperations) && run.undoOperations.length > 0;
|
|
132
|
-
return trackedLifecycle || reviewingEditorUndo || legacyUndo;
|
|
132
|
+
return trackedLifecycle || reviewingEditorUndo || legacyUndo || UndoOperations.getUndoUnavailableFiles(run).length > 0;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
function runDirectoryCleanup(run) {
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
function compactCoreActionPayload(run, keepActionPayload, options = {}) {
|
|
189
|
-
var empty = emptyActionPayload();
|
|
189
|
+
var empty = { ...emptyActionPayload(), appliedOperations: UndoOperations.compactUnavailableUndoOperations(run) };
|
|
190
190
|
if (!keepActionPayload || !hasReloadableRunActionPayload(run)) {
|
|
191
191
|
if (hasReloadableRunActionPayload(run)) empty.recoveryRetention = omitted('fallback');
|
|
192
192
|
else if (run?.recoveryRetention?.state === 'omitted' && !run.forkSnapshot
|
|
@@ -308,11 +308,48 @@
|
|
|
308
308
|
return out;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
const REVIEW_FIELDS = ['trackedChangeStatus', 'undoStatus', 'undoOwnerProtocol', 'undoTrackedChanges', 'undoExpectedFiles',
|
|
312
|
+
'undoOperations', 'undoBaseFiles', 'appliedOperations', 'trackedChangeCaptures', 'recoveryRetention',
|
|
313
|
+
'interruptedDraft', 'retryWriteback', 'saveCheck'];
|
|
314
|
+
const REVIEW_FACT_FIELDS = ['documentEffect', 'fileSettlements', 'failures', 'recoveryAvailability', 'recoveryDisposition'];
|
|
315
|
+
const terminalReview = run => ['accepted', 'rejected'].includes(run?.trackedChangeStatus) || run?.undoStatus === 'applied';
|
|
316
|
+
const factsOf = run => run?.settlement || run?.settlementFacts || {};
|
|
317
|
+
|
|
318
|
+
function copyRunReviewState(target, source) {
|
|
319
|
+
const merged = { ...target };
|
|
320
|
+
const copyFields = (into, from, fields) => {
|
|
321
|
+
for (const field of fields) {
|
|
322
|
+
if (Object.hasOwn(from, field)) into[field] = JSON.parse(JSON.stringify(from[field]));
|
|
323
|
+
else delete into[field];
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
copyFields(merged, source, REVIEW_FIELDS);
|
|
327
|
+
const sourceFacts = factsOf(source), targetFacts = factsOf(target);
|
|
328
|
+
const facts = { ...targetFacts };
|
|
329
|
+
copyFields(facts, sourceFacts, REVIEW_FACT_FIELDS);
|
|
330
|
+
const sourceEvidence = sourceFacts.evidence || {};
|
|
331
|
+
const evidence = { ...(targetFacts.evidence || {}) };
|
|
332
|
+
copyFields(evidence, sourceEvidence, [...new Set([...Object.keys(evidence), ...Object.keys(sourceEvidence)])]
|
|
333
|
+
.filter(key => !['mirrored', 'compiled'].includes(key)));
|
|
334
|
+
if (Object.keys(evidence).length) facts.evidence = evidence; else delete facts.evidence;
|
|
335
|
+
delete merged.settlementFacts;
|
|
336
|
+
if (Object.keys(facts).length) merged.settlement = facts; else delete merged.settlement;
|
|
337
|
+
return merged;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function reviewStateSnapshot(run) {
|
|
341
|
+
const facts = factsOf(run);
|
|
342
|
+
return JSON.stringify([REVIEW_FIELDS.map(key => run?.[key]), REVIEW_FACT_FIELDS.map(key => facts[key]),
|
|
343
|
+
Object.entries(facts.evidence || {}).filter(([key]) => !['mirrored', 'compiled'].includes(key))
|
|
344
|
+
.sort(([a], [b]) => a.localeCompare(b))]);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Shared submissions retain the caller's envelope and private baseline. The
|
|
348
|
+
// legacy site-local path explicitly keeps its historical timestamp merge.
|
|
349
|
+
function mergeSessionReviewState(incoming, existing, reviewRunIds, options = {}) {
|
|
313
350
|
if (!existing || incoming.id !== existing.id || incoming.projectId !== existing.projectId
|
|
314
351
|
|| !incoming.accountScopeId || incoming.accountScopeId !== existing.accountScopeId) return incoming;
|
|
315
|
-
var terminal =
|
|
352
|
+
var terminal = terminalReview;
|
|
316
353
|
var incomingRuns = new Map((incoming.runs || []).map(run => [run.id, run]));
|
|
317
354
|
var existingRuns = new Map((existing.runs || []).map(run => [run.id, run]));
|
|
318
355
|
var requestedIds = new Set(reviewRunIds || []);
|
|
@@ -321,6 +358,21 @@
|
|
|
321
358
|
return ['accepted', 'rejected', 'needs_review'].includes(run?.trackedChangeStatus)
|
|
322
359
|
|| ['applied', 'partial'].includes(run?.undoStatus);
|
|
323
360
|
});
|
|
361
|
+
if (options.shared !== false) {
|
|
362
|
+
const runs = (incoming.runs || []).map(run => {
|
|
363
|
+
const previous = existingRuns.get(run.id);
|
|
364
|
+
const source = terminal(previous) ? previous : null;
|
|
365
|
+
if (!source || (source.runProjectId && source.runProjectId !== incoming.projectId)) return run;
|
|
366
|
+
// The same decision can still gain save/recovery evidence. Let the
|
|
367
|
+
// three-way merge retain those edits and reject genuine collisions.
|
|
368
|
+
if (run.trackedChangeStatus === source.trackedChangeStatus && run.undoStatus === source.undoStatus) return run;
|
|
369
|
+
// An absent local settlement has no edits to its metadata or auxiliary
|
|
370
|
+
// evidence. Retain the stored container as well as the terminal fields.
|
|
371
|
+
const target = !run.settlement && !run.settlementFacts ? { ...run, settlement: factsOf(source) } : run;
|
|
372
|
+
return copyRunReviewState(target, source);
|
|
373
|
+
});
|
|
374
|
+
return { ...incoming, runs };
|
|
375
|
+
}
|
|
324
376
|
var newer = (Date.parse(incoming.updatedAt) || 0) >= (Date.parse(existing.updatedAt) || 0);
|
|
325
377
|
if (!newer && !explicitReview) return incoming;
|
|
326
378
|
var base = newer ? incoming : existing;
|
|
@@ -404,6 +456,8 @@
|
|
|
404
456
|
compactSessionBatchActions: compactSessionBatchActions,
|
|
405
457
|
compactStructuredEventValue: compactStructuredEventValue,
|
|
406
458
|
mergeSessionReviewState: mergeSessionReviewState,
|
|
459
|
+
copyRunReviewState: copyRunReviewState,
|
|
460
|
+
reviewStateSnapshot: reviewStateSnapshot,
|
|
407
461
|
compactRunsForStorage: compactRunsForStorage,
|
|
408
462
|
compactRunActionPayload: compactRunActionPayload,
|
|
409
463
|
compactProviderSnapshot: compactProviderSnapshot,
|
|
@@ -8,6 +8,41 @@
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
function buildUndoOperations(project, appliedOperations) {
|
|
11
|
+
return buildUndoPlan(project, appliedOperations).undoOperations;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getUndoUnavailableFiles(run = {}) {
|
|
15
|
+
const payload = run.recoveryPayload || run;
|
|
16
|
+
const files = new Map();
|
|
17
|
+
for (const operation of payload.appliedOperations || []) {
|
|
18
|
+
const reason = operation?.undoUnavailable;
|
|
19
|
+
if (typeof reason?.path === 'string' && reason.path === operation?.path
|
|
20
|
+
&& ['stale_source_changed', 'snapshot_undo_unavailable'].includes(reason.code)) {
|
|
21
|
+
files.set(reason.path, { path: reason.path, code: reason.code });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return [...files.values()];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function withAvailableUndo(run = {}) {
|
|
28
|
+
const unavailable = new Set(getUndoUnavailableFiles(run).map(item => item.path));
|
|
29
|
+
if (!unavailable.size) return run;
|
|
30
|
+
const result = { ...run };
|
|
31
|
+
for (const field of ['undoOperations', 'undoBaseFiles', 'undoExpectedFiles', 'undoTrackedChanges', 'trackedChangeCaptures']) {
|
|
32
|
+
result[field] = (run[field] || []).filter(item => !unavailable.has(item?.path) && !unavailable.has(item?.to));
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Fallback history keeps only bounded reasons, never document bytes or
|
|
38
|
+
// authority to reconstruct a withheld checkpoint.
|
|
39
|
+
function compactUnavailableUndoOperations(run = {}) {
|
|
40
|
+
if (run.forkSnapshot || run.undoStatus === 'applied' || ['accepted', 'rejected'].includes(run.trackedChangeStatus)) return [];
|
|
41
|
+
return getUndoUnavailableFiles(run).filter(item => item.path.length <= 1024).slice(0, 100)
|
|
42
|
+
.map(item => ({ type: 'edit', path: item.path, undoUnavailable: item }));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildUndoPlan(project, appliedOperations) {
|
|
11
46
|
const workingFilesByPath = new Map();
|
|
12
47
|
for (const file of project?.files || []) {
|
|
13
48
|
if (typeof file?.path === 'string' && typeof file.content === 'string') {
|
|
@@ -17,6 +52,10 @@
|
|
|
17
52
|
|
|
18
53
|
const sourceFilesByPath = new Map(workingFilesByPath);
|
|
19
54
|
const undo = [];
|
|
55
|
+
const inverseBases = new Map();
|
|
56
|
+
const mappedPaths = new Set((appliedOperations || []).filter(operation => Array.isArray(operation?.appliedPatches))
|
|
57
|
+
.map(operation => operation.path));
|
|
58
|
+
const unavailable = new Map(getUndoUnavailableFiles({ appliedOperations }).map(item => [item.path, item.code]));
|
|
20
59
|
const fullRestoreEditPaths = new Set();
|
|
21
60
|
|
|
22
61
|
for (const operation of appliedOperations || []) {
|
|
@@ -30,23 +69,33 @@
|
|
|
30
69
|
}
|
|
31
70
|
const currentBeforeEdit = workingFilesByPath.get(operation.path);
|
|
32
71
|
if (!fullRestoreEditPaths.has(operation.path)) {
|
|
33
|
-
const
|
|
34
|
-
|
|
72
|
+
const hasAppliedPatches = Array.isArray(operation.appliedPatches) && typeof operation.verifiedContent === 'string';
|
|
73
|
+
const inversePatches = hasAppliedPatches
|
|
74
|
+
? buildVerifiedInversePatches(operation.appliedPatches, operation.verifiedContent)
|
|
75
|
+
: buildInverseTextPatches(sourceFilesByPath.get(operation.path), operation.patches,
|
|
76
|
+
operation.verifiedContent, operation.path);
|
|
77
|
+
let inverse;
|
|
35
78
|
if (inversePatches.length) {
|
|
36
|
-
|
|
79
|
+
inverse = normalizeUndoOperation({
|
|
37
80
|
type: 'edit',
|
|
38
81
|
path: operation.path,
|
|
39
82
|
patches: inversePatches,
|
|
40
83
|
reason: 'Undo edit'
|
|
41
|
-
})
|
|
42
|
-
} else {
|
|
84
|
+
});
|
|
85
|
+
} else if (!hasAppliedPatches) {
|
|
43
86
|
fullRestoreEditPaths.add(operation.path);
|
|
44
|
-
|
|
87
|
+
inverse = normalizeUndoOperation({
|
|
45
88
|
type: 'edit',
|
|
46
89
|
path: operation.path,
|
|
47
90
|
replaceAll: currentBeforeEdit,
|
|
48
91
|
reason: 'Undo edit'
|
|
49
|
-
})
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
unavailable.set(operation.path, 'snapshot_undo_unavailable');
|
|
95
|
+
}
|
|
96
|
+
if (inverse) {
|
|
97
|
+
undo.push(inverse);
|
|
98
|
+
if (typeof operation.verifiedContent === 'string') inverseBases.set(inverse, operation.verifiedContent);
|
|
50
99
|
}
|
|
51
100
|
}
|
|
52
101
|
} else if (operation.type === 'create') {
|
|
@@ -97,7 +146,51 @@
|
|
|
97
146
|
applyOperationToFiles(sourceFilesByPath, { ...operation, verifiedContent: undefined });
|
|
98
147
|
}
|
|
99
148
|
|
|
100
|
-
|
|
149
|
+
// Each inverse was measured in its own verified post-image. Compose in
|
|
150
|
+
// reverse order against the latest image, preserving changes between
|
|
151
|
+
// entries. Empty inverse insertions must never reuse an older offset.
|
|
152
|
+
const composed = [];
|
|
153
|
+
for (const inverse of undo.reverse()) {
|
|
154
|
+
let operation = inverse;
|
|
155
|
+
if (mappedPaths.has(inverse.path) && inverseBases.has(inverse)) {
|
|
156
|
+
const current = workingFilesByPath.get(inverse.path);
|
|
157
|
+
const base = StaleGuard.buildBaseFileLookup([{ path: inverse.path, content: inverseBases.get(inverse) }]);
|
|
158
|
+
const fresh = StaleGuard.checkOperationFreshness(inverse, current, base);
|
|
159
|
+
const prepared = fresh.ok && inverse.patches
|
|
160
|
+
? fresh.preparedPatches || StaleGuard.prepareOperationPatches(inverse, current, base) : fresh;
|
|
161
|
+
if (typeof current !== 'string' || !prepared.ok) {
|
|
162
|
+
unavailable.set(inverse.path, 'stale_source_changed');
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (inverse.patches) operation = { ...inverse, patches: prepared.patches };
|
|
166
|
+
}
|
|
167
|
+
composed.push(operation);
|
|
168
|
+
applyOperationToFiles(workingFilesByPath, operation);
|
|
169
|
+
}
|
|
170
|
+
return { undoOperations: composed.filter(operation => !unavailable.has(operation.path)),
|
|
171
|
+
undoUnavailable: [...unavailable].map(([path, code]) => ({ path, code })) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Invert the ranges actually dispatched to C, not the original Native
|
|
175
|
+
// coordinates in B. Verify against the observed post-image before storing
|
|
176
|
+
// recovery; invalid evidence must not fall back to overwriting C with B.
|
|
177
|
+
function buildVerifiedInversePatches(patches, postImage) {
|
|
178
|
+
const beforeLength = patches.reduce((length, patch) =>
|
|
179
|
+
length + Number(patch?.to) - Number(patch?.from) - String(patch?.insert ?? '').length, postImage.length);
|
|
180
|
+
if (!Number.isSafeInteger(beforeLength) || beforeLength < 0) return [];
|
|
181
|
+
const normalized = normalizeTextPatches(patches, beforeLength);
|
|
182
|
+
if (!normalized.ok || normalized.patches.some(patch => patch.expected.length !== patch.to - patch.from)) return [];
|
|
183
|
+
const inverse = invertPatches(normalized.patches);
|
|
184
|
+
return applyTextPatches(postImage, inverse).ok ? inverse : [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function invertPatches(patches) {
|
|
188
|
+
let offset = 0;
|
|
189
|
+
return patches.map(patch => {
|
|
190
|
+
const from = patch.from + offset;
|
|
191
|
+
offset += patch.insert.length - (patch.to - patch.from);
|
|
192
|
+
return { from, to: from + patch.insert.length, expected: patch.insert, insert: patch.expected };
|
|
193
|
+
});
|
|
101
194
|
}
|
|
102
195
|
|
|
103
196
|
function buildInverseTextPatches(textBeforeEdit, patches, verifiedContent, path) {
|
|
@@ -113,28 +206,20 @@
|
|
|
113
206
|
return [];
|
|
114
207
|
}
|
|
115
208
|
|
|
116
|
-
|
|
117
|
-
const inverse = normalized.patches.map(patch => {
|
|
118
|
-
const from = patch.from + offset;
|
|
119
|
-
const to = from + patch.insert.length;
|
|
120
|
-
offset += patch.insert.length - (patch.to - patch.from);
|
|
121
|
-
return {
|
|
122
|
-
from,
|
|
123
|
-
to,
|
|
124
|
-
expected: patch.insert,
|
|
125
|
-
insert: patch.expected
|
|
126
|
-
};
|
|
127
|
-
});
|
|
209
|
+
const inverse = invertPatches(normalized.patches);
|
|
128
210
|
if (typeof verifiedContent !== 'string' || verifiedContent === applied.text) return inverse;
|
|
129
211
|
// The stored checkpoint uses the verified editor post-image. Its inverse
|
|
130
212
|
// must use those same offsets, while Native patches retain their source
|
|
131
213
|
// representation for later operations in the batch.
|
|
132
|
-
const prepared = StaleGuard.
|
|
214
|
+
const prepared = StaleGuard.prepareStoredInversePatches({ type: 'edit', path, patches: inverse }, verifiedContent,
|
|
133
215
|
StaleGuard.buildBaseFileLookup([{ path, content: applied.text }]));
|
|
134
216
|
return prepared.ok && applyTextPatches(verifiedContent, prepared.patches).ok ? prepared.patches : [];
|
|
135
217
|
}
|
|
136
218
|
|
|
137
219
|
function selectExpectedFilesForTrackedUndo(project, operations = [], trackedChanges = [], previousExpectedFiles = []) {
|
|
220
|
+
const checkpoint = buildUndoCheckpoint(project, operations);
|
|
221
|
+
const unavailable = new Set(checkpoint.undoUnavailable.map(item => item.path));
|
|
222
|
+
const mappedPaths = new Set(operations.filter(operation => Array.isArray(operation?.appliedPatches)).map(operation => operation.path));
|
|
138
223
|
const paths = new Set();
|
|
139
224
|
for (const change of trackedChanges || []) if (change?.path) paths.add(change.path);
|
|
140
225
|
for (const operation of operations || []) {
|
|
@@ -143,7 +228,8 @@
|
|
|
143
228
|
}
|
|
144
229
|
const expectedByPath = new Map();
|
|
145
230
|
for (const file of previousExpectedFiles || []) {
|
|
146
|
-
if (paths.has(file?.path) &&
|
|
231
|
+
if (paths.has(file?.path) && !unavailable.has(file.path) && !mappedPaths.has(file.path)
|
|
232
|
+
&& typeof file?.content === 'string' && !expectedByPath.has(file.path)) {
|
|
147
233
|
expectedByPath.set(file.path, { path: file.path, content: file.content });
|
|
148
234
|
}
|
|
149
235
|
}
|
|
@@ -152,10 +238,9 @@
|
|
|
152
238
|
// content, including when the ZIP baseline used a different representation.
|
|
153
239
|
const hasVerifiedPatches = operations.some(operation => typeof operation?.verifiedContent === 'string'
|
|
154
240
|
&& Array.isArray(operation.patches) && operation.patches.length);
|
|
155
|
-
const
|
|
156
|
-
const restored = checkpoint ? buildExpectedFilesAfterOperations({ files: checkpoint.undoBaseFiles }, checkpoint.undoOperations) : new Map();
|
|
241
|
+
const restored = hasVerifiedPatches ? buildExpectedFilesAfterOperations({ files: checkpoint.undoBaseFiles }, checkpoint.undoOperations) : new Map();
|
|
157
242
|
for (const file of project?.files || []) {
|
|
158
|
-
if (paths.has(file?.path) && typeof file?.content === 'string' && !expectedByPath.has(file.path)) {
|
|
243
|
+
if (paths.has(file?.path) && !unavailable.has(file.path) && typeof file?.content === 'string' && !expectedByPath.has(file.path)) {
|
|
159
244
|
expectedByPath.set(file.path, { path: file.path, content: restored.get(file.path) ?? file.content });
|
|
160
245
|
}
|
|
161
246
|
}
|
|
@@ -163,11 +248,15 @@
|
|
|
163
248
|
}
|
|
164
249
|
|
|
165
250
|
function buildUndoCheckpoint(project, appliedOperations) {
|
|
166
|
-
const undoOperations =
|
|
251
|
+
const { undoOperations, undoUnavailable } = buildUndoPlan(project, appliedOperations);
|
|
252
|
+
const unavailable = new Map(undoUnavailable.map(item => [item.path, item]));
|
|
167
253
|
const expectedFiles = buildExpectedFilesAfterOperations(project, appliedOperations);
|
|
168
254
|
return {
|
|
169
255
|
undoOperations,
|
|
170
|
-
undoBaseFiles: filesMapToList(selectUndoBaseFiles(expectedFiles, undoOperations))
|
|
256
|
+
undoBaseFiles: filesMapToList(selectUndoBaseFiles(expectedFiles, undoOperations)),
|
|
257
|
+
undoUnavailable,
|
|
258
|
+
appliedOperations: (appliedOperations || []).map(operation => unavailable.has(operation?.path)
|
|
259
|
+
? { ...operation, undoUnavailable: unavailable.get(operation.path) } : operation)
|
|
171
260
|
};
|
|
172
261
|
}
|
|
173
262
|
|
|
@@ -236,6 +325,7 @@
|
|
|
236
325
|
// file creation, backed by both the forward delete and the captured original.
|
|
237
326
|
// This also recovers older Track-on records that omitted that inverse.
|
|
238
327
|
function getFileUndoOperations(run = {}) {
|
|
328
|
+
run = withAvailableUndo(run);
|
|
239
329
|
const operations = (run.undoOperations || []).filter(isFileUndoOperation);
|
|
240
330
|
const before = filesListToMap(run.undoExpectedFiles);
|
|
241
331
|
const after = buildExpectedFilesAfterOperations({ files: run.undoExpectedFiles }, run.appliedOperations);
|
|
@@ -246,6 +336,8 @@
|
|
|
246
336
|
}
|
|
247
337
|
|
|
248
338
|
function prepareRunUndo(run = {}) {
|
|
339
|
+
const original = run;
|
|
340
|
+
run = withAvailableUndo(run);
|
|
249
341
|
const files = getFileUndoOperations(run);
|
|
250
342
|
const restored = new Set(files.filter(operation => operation.type === 'create').map(operation => operation.path));
|
|
251
343
|
const pending = [...files], operations = [];
|
|
@@ -255,10 +347,12 @@
|
|
|
255
347
|
if (index >= 0) operations.push(...pending.splice(index, 1));
|
|
256
348
|
} else if (!restored.has(operation.path)) operations.push(operation);
|
|
257
349
|
}
|
|
258
|
-
return { ...
|
|
350
|
+
return { ...original, undoOperations: [...operations, ...pending],
|
|
351
|
+
...(run !== original ? { undoBaseFiles: run.undoBaseFiles, undoExpectedFiles: run.undoExpectedFiles } : {}) };
|
|
259
352
|
}
|
|
260
353
|
|
|
261
354
|
function textUndoRun(run = {}) {
|
|
355
|
+
run = withAvailableUndo(run);
|
|
262
356
|
const restored = new Set(getFileUndoOperations(run).flatMap(operation => operation.type === 'create' ? [operation.path]
|
|
263
357
|
: operation.type === 'move' ? [operation.path, operation.to] : []));
|
|
264
358
|
return { ...run,
|
|
@@ -332,6 +426,7 @@
|
|
|
332
426
|
}
|
|
333
427
|
|
|
334
428
|
function buildSnapshotRestoreUndo(run = {}) {
|
|
429
|
+
run = withAvailableUndo(run);
|
|
335
430
|
const undoOperations = Array.isArray(run.undoOperations) ? run.undoOperations : [];
|
|
336
431
|
const originalByPath = buildOriginalFilesForSnapshotUndo(run, undoOperations);
|
|
337
432
|
if (!originalByPath.size) {
|
|
@@ -571,6 +666,9 @@
|
|
|
571
666
|
}
|
|
572
667
|
|
|
573
668
|
return {
|
|
669
|
+
getUndoUnavailableFiles,
|
|
670
|
+
withAvailableUndo,
|
|
671
|
+
compactUnavailableUndoOperations,
|
|
574
672
|
isFileUndoOperation,
|
|
575
673
|
getFileUndoOperations,
|
|
576
674
|
prepareRunUndo,
|
|
@@ -12,6 +12,18 @@
|
|
|
12
12
|
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function verifiedRollback(status = {}) {
|
|
16
|
+
const transaction = status.transaction;
|
|
17
|
+
const pair = status.restoredPair;
|
|
18
|
+
const digest = value => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value);
|
|
19
|
+
return Boolean(transaction?.id && transaction.state === 'rolled_back'
|
|
20
|
+
&& version(transaction.sourceVersion) && version(transaction.targetVersion)
|
|
21
|
+
&& status.installedAligned === true && status.activeVersion === transaction.sourceVersion
|
|
22
|
+
&& pair?.extensionVersion === transaction.sourceVersion && pair.nativeVersion === transaction.sourceVersion
|
|
23
|
+
&& digest(pair.extensionSha256) && digest(pair.nativeSha256)
|
|
24
|
+
&& (!transaction.bootstrapRevision || digest(pair.bootstrapSha256)));
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
function inspectInstalledRuntime(status = {}, options = {}) {
|
|
16
28
|
const result = { installedVersion: version(status.activeVersion), nativeVersion: version(status.runtimeVersion),
|
|
17
29
|
extensionVersion: version(options.extensionVersion), runtimeVersion: version(options.runtimeVersion), state: 'unknown' };
|
|
@@ -24,9 +36,11 @@
|
|
|
24
36
|
if (status.installedAligned !== true) return { ...result, state: 'repair_required' };
|
|
25
37
|
const loaded = [result.nativeVersion, result.extensionVersion, result.runtimeVersion];
|
|
26
38
|
if (loaded.every(value => value === result.installedVersion)) return { ...result, state: 'aligned' };
|
|
27
|
-
|
|
39
|
+
const rollbackReload = verifiedRollback(status) && loaded.every(value =>
|
|
40
|
+
value === status.transaction.sourceVersion || value === status.transaction.targetVersion);
|
|
41
|
+
return { ...result, state: rollbackReload || loaded.every(value => compare(result.installedVersion, value) >= 0)
|
|
28
42
|
? 'reload_required' : 'repair_required' };
|
|
29
43
|
}
|
|
30
44
|
|
|
31
|
-
return Object.freeze({ inspectInstalledRuntime });
|
|
45
|
+
return Object.freeze({ inspectInstalledRuntime, verifiedRollback });
|
|
32
46
|
});
|
|
@@ -28,7 +28,18 @@
|
|
|
28
28
|
const SETTLEMENT_FACTS_MAX_BYTES = SettlementFacts.SETTLEMENT_FACTS_MAX_BYTES;
|
|
29
29
|
const RECOVERY_FIELDS = SettlementFacts.RECOVERY_FIELDS;
|
|
30
30
|
const compactSettlementFacts = SettlementFacts.compactSettlementFacts;
|
|
31
|
-
|
|
31
|
+
function projectRunSettlement(run = {}) {
|
|
32
|
+
const projection = SettlementFacts.projectRunSettlement(run);
|
|
33
|
+
const undoUnavailable = UndoOperations.getUndoUnavailableFiles(run);
|
|
34
|
+
if (!undoUnavailable.length) return projection;
|
|
35
|
+
const payload = UndoOperations.withAvailableUndo(run.recoveryPayload || run);
|
|
36
|
+
// Forward receipts remain audit evidence, not fallback Undo authority for
|
|
37
|
+
// a run whose inverse checkpoint could not be safely composed.
|
|
38
|
+
const available = SettlementFacts.projectRunSettlement({ ...run,
|
|
39
|
+
recoveryPayload: { ...payload, appliedOperations: [] } });
|
|
40
|
+
return { ...projection, canUndo: available.canUndo, undoUnavailable,
|
|
41
|
+
actions: { ...projection.actions, undo: available.canUndo } };
|
|
42
|
+
}
|
|
32
43
|
const deriveLegacyProjection = SettlementFacts.deriveLegacyProjection;
|
|
33
44
|
const aggregateFileSettlements = EvidenceProjection.aggregateFileSettlements;
|
|
34
45
|
const deriveAppliedEvidence = EvidenceProjection.deriveAppliedEvidence;
|
|
@@ -297,7 +308,8 @@
|
|
|
297
308
|
// Both actions cover every targeted file. Partial success must retain
|
|
298
309
|
// the original recovery material for unresolved siblings.
|
|
299
310
|
const incomplete = collectRunResultSkipped(input.result).length > 0
|
|
300
|
-
|| input.result?.ok === false
|
|
311
|
+
|| input.result?.ok === false
|
|
312
|
+
|| (kind === 'reject' && UndoOperations.getUndoUnavailableFiles(input.run).length > 0);
|
|
301
313
|
const needsReview = incomplete || !successful || failures.some(failure =>
|
|
302
314
|
failure.terminalState === 'needs_review' || reviewCodes.has(failure.code)
|
|
303
315
|
);
|
|
@@ -340,6 +352,7 @@
|
|
|
340
352
|
|
|
341
353
|
function settleLegacyUndo(input = {}) {
|
|
342
354
|
const status = input.status === 'applied'
|
|
355
|
+
&& !UndoOperations.getUndoUnavailableFiles(input.run).length
|
|
343
356
|
&& isUndoResultEffectivelyApplied(input.run, input.result, input.run?.undoOperations) ? 'applied' : 'partial';
|
|
344
357
|
const recovery = preserveRecovery();
|
|
345
358
|
return {
|
|
@@ -650,7 +663,12 @@
|
|
|
650
663
|
? { v: 1, kind: rawMove.kind, sha256: rawMove.sha256.toLowerCase(), sourceEntityId: rawMove.sourceEntityId,
|
|
651
664
|
targetEntityId: rawMove.targetEntityId, ...(['doc', 'file'].includes(rawMove.entityType) ? { entityType: rawMove.entityType } : {}), ...(createdDirectories.length ? { createdDirectories } : {}) } : null;
|
|
652
665
|
if (!(operation.type === 'edit' && typeof result?.verifiedContent === 'string') && !createdDirectories.length && !moveProof) return operation;
|
|
653
|
-
return { ...operation, ...(operation.type === 'edit' && typeof result?.verifiedContent === 'string' ? {
|
|
666
|
+
return { ...operation, ...(operation.type === 'edit' && typeof result?.verifiedContent === 'string' ? {
|
|
667
|
+
verifiedContent: result.verifiedContent,
|
|
668
|
+
...(result.ok === true && result.verified === true && Array.isArray(result.appliedPatches)
|
|
669
|
+
? { appliedPatches: result.appliedPatches.map(patch => ({ from: patch.from, to: patch.to,
|
|
670
|
+
expected: String(patch.expected ?? ''), insert: String(patch.insert ?? '') })) } : {})
|
|
671
|
+
} : {}),
|
|
654
672
|
...(createdDirectories.length ? { createdDirectories } : {}), ...(moveProof ? { moveProof } : {}) };
|
|
655
673
|
}
|
|
656
674
|
|