codex-overleaf-link 2.3.5 → 2.4.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 (57) hide show
  1. package/README.md +63 -31
  2. package/README.zh-CN.md +543 -0
  3. package/extension/bootstrap/manifest.template.json +1 -1
  4. package/extension/src/background.js +7 -0
  5. package/extension/src/backgroundUpdateCoordinator.js +88 -4
  6. package/extension/src/content/activeTurnControl.js +1 -0
  7. package/extension/src/content/assetTransferBroker.js +2 -0
  8. package/extension/src/content/contentRuntime.js +77 -57
  9. package/extension/src/content/contextTray.js +4 -0
  10. package/extension/src/content/diagnosticsController.js +17 -3
  11. package/extension/src/content/diagnosticsPanel.js +129 -5
  12. package/extension/src/content/generated/content.bundle.js +1422 -348
  13. package/extension/src/content/generated/content.bundle.meta.json +7 -3
  14. package/extension/src/content/legacyGlobalRegistry.js +3 -0
  15. package/extension/src/content/modelPicker.js +21 -9
  16. package/extension/src/content/modelPickerSupport.js +38 -1
  17. package/extension/src/content/nativeCompatibilityController.js +4 -1
  18. package/extension/src/content/otWarmMirror.js +126 -92
  19. package/extension/src/content/otWarmMirrorController.js +122 -1
  20. package/extension/src/content/pageBridgeClient.js +2 -0
  21. package/extension/src/content/panelMaintenance.js +26 -15
  22. package/extension/src/content/providerSettingsCoordinator.js +1 -0
  23. package/extension/src/content/runFailureNotice.js +104 -0
  24. package/extension/src/content/runResultActions.js +16 -1
  25. package/extension/src/content/runScrollLayout.js +124 -0
  26. package/extension/src/content/runTimelineView.js +37 -11
  27. package/extension/src/content/sessionForkController.js +20 -11
  28. package/extension/src/content/settingsPanel.js +28 -11
  29. package/extension/src/content/trackedChangeCaptureController.js +153 -0
  30. package/extension/src/content/updateNotice.js +24 -4
  31. package/extension/src/content/writebackOrchestrator.js +31 -0
  32. package/extension/src/page/binaryAssetUploader.js +101 -29
  33. package/extension/src/page/overleafRealtimeObserver.js +120 -25
  34. package/extension/src/page/textFileCreator.js +493 -0
  35. package/extension/src/page/trackedChangeCapture.js +342 -0
  36. package/extension/src/page/trackedChangesLifecycle.js +145 -149
  37. package/extension/src/page/treeOperations.js +30 -2
  38. package/extension/src/page/writebackRouter.js +94 -48
  39. package/extension/src/pageBridge.js +62 -25
  40. package/extension/src/shared/compatibility.js +1 -1
  41. package/extension/src/shared/i18n.js +77 -37
  42. package/extension/src/shared/managedUpdateProjection.js +1 -1
  43. package/extension/src/shared/otText.js +0 -1
  44. package/extension/src/shared/pageRpcContract.js +2 -1
  45. package/extension/src/shared/sessionState.js +23 -23
  46. package/extension/src/shared/sessionTitle.js +120 -0
  47. package/extension/src/shared/storageDb.js +10 -12
  48. package/extension/src/shared/storageRunActions.js +29 -1
  49. package/extension/src/shared/updateRuntimeIdentity.js +32 -0
  50. package/extension/src/shared/writebackSettlement.js +19 -5
  51. package/extension/styles/panel.css +128 -24
  52. package/native-host/src/mirrorWorkspace.js +20 -1
  53. package/native-host/src/releaseProxy.js +177 -0
  54. package/native-host/src/releaseTransport.js +154 -0
  55. package/native-host/src/taskRunnerRuntime.js +25 -0
  56. package/native-host/src/updateManager.js +40 -39
  57. package/package.json +5 -1
@@ -0,0 +1,342 @@
1
+ (function (root, factory) {
2
+ const api = factory();
3
+ if (typeof module === 'object' && module.exports) module.exports = api;
4
+ if (root) root.CodexOverleafTrackedChangeCapture = api;
5
+ })(typeof window !== 'undefined' ? window : globalThis, function () {
6
+ 'use strict';
7
+
8
+ function create(deps = {}) {
9
+ const { collectElements, compact, delay, getActiveFilePath, readActiveEditorText,
10
+ isInsideCodexPanel, normalizeSafeProjectPath, readNodeSignalText } = deps;
11
+ const now = deps.now || (() => Date.now());
12
+ const limit = 1200;
13
+ const fingerprint = text => {
14
+ let a = 2166136261, b = 5381;
15
+ const value = String(text ?? '');
16
+ for (let index = 0; index < value.length; index++) {
17
+ const code = value.charCodeAt(index);
18
+ a = Math.imul(a ^ code, 16777619) >>> 0;
19
+ b = (Math.imul(b, 33) ^ code) >>> 0;
20
+ }
21
+ return value.length + ':' + a.toString(16) + ':' + b.toString(16);
22
+ };
23
+ const explicitId = node => {
24
+ for (const attr of ['data-change-id', 'data-review-id', 'data-track-change-id', 'data-ol-change-id']) {
25
+ const value = node.getAttribute?.(attr);
26
+ if (value) return String(value);
27
+ }
28
+ const value = node.getAttribute?.('data-id') || node.getAttribute?.('id') || '';
29
+ return value && /(?:ol-cm-change|review-panel-entry-change|track-change|review-change)/i.test(node.className || '') ? String(value) : '';
30
+ };
31
+ const marker = node => Boolean(node && !isInsideCodexPanel?.(node)
32
+ && (explicitId(node) || /(?:^|\s)(?:ol-cm-change(?:-\w+)?|review-panel-entry-change|track-change|review-change[\w-]*)(?:\s|$)/i.test(node.className || '')));
33
+ function collectTrackedChangeNodes() {
34
+ const selector = [
35
+ '[data-change-id]', '[data-review-id]', '[data-track-change-id]',
36
+ '[data-ol-change-id]', '.ol-cm-change', '.review-panel-entry-change',
37
+ '[class*="track-change" i]', '[class*="review-change" i]'
38
+ ].join(',');
39
+ return Array.from(new Set(collectElements(selector, limit))).filter(marker);
40
+ }
41
+ function trackedChangeRefFromNode(node, fallbackPath = '') {
42
+ const id = explicitId(node);
43
+ const path = normalizeSafeProjectPath(node.getAttribute?.('data-path')
44
+ || node.getAttribute?.('data-file-path') || node.getAttribute?.('data-doc-path') || fallbackPath);
45
+ const rawPosition = node.getAttribute?.('data-pos');
46
+ const position = rawPosition !== null && rawPosition !== undefined && rawPosition !== ''
47
+ && Number.isSafeInteger(Number(rawPosition)) && Number(rawPosition) >= 0 ? Number(rawPosition) : null;
48
+ const parts = Array.from(node.querySelectorAll?.('.review-panel-expandable-content') || [])
49
+ .map(part => part.textContent || '');
50
+ const body = parts.length ? JSON.stringify(parts)
51
+ : (node.querySelector?.('.review-panel-change-body')?.textContent || node.textContent || '');
52
+ const author = node.querySelector?.('.review-panel-entry-user')?.textContent || '';
53
+ const kind = /(?:delete|change-d)(?:\s|$)/.test(node.className || '') ? 'delete' : 'insert';
54
+ const legacyKey = 'sig:' + compact(readNodeSignalText(node), 180);
55
+ return {
56
+ key: id ? 'id:' + id : position !== null
57
+ ? 'pos:' + position + ':' + kind + ':' + fingerprint(body + '\0' + author)
58
+ : legacyKey,
59
+ id, path, label: compact(readNodeSignalText(node), 180)
60
+ };
61
+ }
62
+ function collectTrackedChangeRefsForPaths(paths = []) {
63
+ const allowed = new Set(paths.filter(Boolean));
64
+ const active = getActiveFilePath();
65
+ return collectTrackedChangeNodes().map(node => trackedChangeRefFromNode(node, active))
66
+ .filter(ref => ref.key && (!allowed.size || !ref.path || allowed.has(ref.path)));
67
+ }
68
+ // Overleaf rangesDataField holds the complete ledger, not viewport DOM.
69
+ // Adapter boundary: CodeMirror's internal values layout is shape-checked.
70
+ // https://github.com/overleaf/overleaf/blob/main/services/web/frontend/js/features/source-editor/extensions/ranges.ts
71
+ function readNativeSnapshot(path, pinnedDocId = '') {
72
+ const unavailable = reason => ({ supported: true, source: 'native', ready: false, reason, refs: [] });
73
+ try {
74
+ const state = deps.getCodeMirrorEditorView?.()?.state;
75
+ if (!Array.isArray(state?.values) || typeof deps.getTrackedChangeDocumentId !== 'function') return { supported: false };
76
+ const nativeDocId = pinnedDocId || deps.getTrackedChangeDocumentId(path);
77
+ if (!nativeDocId || state.values.length > 2048) return unavailable('native_document_unavailable');
78
+ const entries = state.values.filter(value => value && typeof value === 'object'
79
+ && Object.prototype.hasOwnProperty.call(value, 'ranges')
80
+ && Object.prototype.hasOwnProperty.call(value, 'threads')
81
+ && value.ranges?.docId === nativeDocId
82
+ && Array.isArray(value.ranges.changes) && Array.isArray(value.ranges.comments));
83
+ if (entries.length !== 1) return unavailable('native_ledger_not_ready');
84
+ const changes = entries[0].ranges.changes;
85
+ const text = state.doc.toString();
86
+ if (changes.length >= limit || !Number.isSafeInteger(state.doc.length) || state.doc.length !== text.length)
87
+ return unavailable('native_ledger_limit_or_length');
88
+ const refs = [], seen = new Set();
89
+ for (const change of changes) {
90
+ const id = change?.id, op = change?.op;
91
+ if (typeof id !== 'string' || !id || id.length > 256 || !Number.isSafeInteger(op?.p)
92
+ || op.p < 0 || op.p > text.length) return unavailable('native_change_invalid');
93
+ const insertion = typeof op.i === 'string', deletion = typeof op.d === 'string';
94
+ if (insertion === deletion) return unavailable('native_change_invalid');
95
+ const changedText = insertion ? op.i : op.d;
96
+ if (!changedText.length) continue;
97
+ const from = op.p, to = insertion ? from + changedText.length : from;
98
+ if (to > text.length || (insertion && text.slice(from, to) !== changedText))
99
+ return unavailable('native_ledger_stale');
100
+ const kind = insertion ? 'insert' : 'delete', textHash = fingerprint(changedText);
101
+ const key = 'native:' + JSON.stringify([id, kind, from, to, textHash]);
102
+ if (seen.has(key)) continue;
103
+ seen.add(key);
104
+ refs.push({ key, id, path, source: 'native', from, to, kind, textHash,
105
+ textLength: changedText.length, label: kind + ' at ' + from });
106
+ }
107
+ const acceptByIdSupported = changes.every(change => !Object.prototype.hasOwnProperty.call(change, 'snapshotRange')
108
+ && !/^change-(?:insert|delete)-\d+$/.test(change.id));
109
+ return { supported: true, source: 'native', ready: true, nativeDocId, refs, acceptByIdSupported };
110
+ } catch (_error) { return unavailable('native_ledger_unavailable'); }
111
+ }
112
+ function prepareTrackedChangeCapture(path) {
113
+ const snapshot = readNativeSnapshot(path);
114
+ return snapshot.supported ? snapshot
115
+ : { source: 'dom', ready: true, refs: collectTrackedChangeRefsForPaths([path]) };
116
+ }
117
+ function getTrackedChangeCaptureStatus() {
118
+ const snapshot = prepareTrackedChangeCapture(getActiveFilePath());
119
+ return { adapter: 'cm-ranges-v1', source: snapshot.source, ready: snapshot.ready,
120
+ count: snapshot.refs.length, docId: snapshot.nativeDocId || '', reason: snapshot.reason || '' };
121
+ }
122
+ function prepareUntrackedUndo(path) {
123
+ const snapshot = readNativeSnapshot(path);
124
+ return { ...snapshot, ok: snapshot.ready === true && snapshot.refs.length === 0, unrelated: [],
125
+ reason: snapshot.ready ? (snapshot.refs.length ? 'native_untracked_undo_pending_changes' : '')
126
+ : snapshot.reason || 'native_review_unavailable' };
127
+ }
128
+ function prepareTrackedChangeReview(path, refs = []) {
129
+ const snapshot = readNativeSnapshot(path);
130
+ const blocked = reason => ({ ...snapshot, ok: false, reason });
131
+ if (!snapshot.ready) return blocked(snapshot.reason || 'native_review_unavailable');
132
+ const targets = refs.filter(ref => ref.path === path), ids = new Set(targets.map(ref => ref.id));
133
+ let expected;
134
+ try {
135
+ expected = targets.map(ref => {
136
+ if (!ref.key.startsWith('native:') || ref.key.length > 1024) throw new Error('identity');
137
+ const tuple = JSON.parse(ref.key.slice(7));
138
+ if (!Array.isArray(tuple) || tuple.length !== 5 || !ref.id || tuple[0] !== ref.id
139
+ || !['insert', 'delete'].includes(tuple[1]) || typeof tuple[4] !== 'string'
140
+ || !Number.isSafeInteger(tuple[2]) || !Number.isSafeInteger(tuple[3])
141
+ || tuple[2] < 0 || tuple[3] < tuple[2]) throw new Error('identity');
142
+ return JSON.stringify([tuple[0], tuple[1], tuple[4]]);
143
+ }).sort();
144
+ } catch (_error) { return blocked('native_review_identity_unavailable'); }
145
+ // All fragments sharing an ID must be owned, including after a split or merge.
146
+ const current = snapshot.refs.filter(ref => ids.has(ref.id))
147
+ .map(ref => JSON.stringify([ref.id, ref.kind, ref.textHash])).sort();
148
+ if (!targets.length || JSON.stringify(expected) !== JSON.stringify(current))
149
+ return blocked('native_review_scope_changed');
150
+ return { ...snapshot, ok: true, targets, ids: [...ids],
151
+ unrelated: snapshot.refs.filter(ref => !ids.has(ref.id)) };
152
+ }
153
+ function nativeOwned(ref, capture) {
154
+ if (ref.source !== 'native' || !Number.isSafeInteger(ref.from) || !Number.isSafeInteger(ref.to)) return false;
155
+ if (ref.kind === 'delete') return capture.ranges.some(range => ref.from >= range.start && ref.from <= range.end
156
+ && ref.textLength === range.removedLength && ref.textHash === range.removedHash && range.removedLength > 0);
157
+ let covered = ref.from;
158
+ for (const range of capture.ranges) {
159
+ if (range.end < covered) continue;
160
+ if (range.start > covered) break;
161
+ covered = Math.max(covered, range.end);
162
+ if (covered >= ref.to) return ref.to > ref.from;
163
+ }
164
+ return false;
165
+ }
166
+ const identity = ref => (ref.path || '') + '\0' + ref.key;
167
+ function diff(before, after) {
168
+ const seen = new Set(before.map(identity));
169
+ return after.filter(ref => {
170
+ const key = identity(ref);
171
+ if (!ref.key || seen.has(key)) return false;
172
+ seen.add(key);
173
+ return true;
174
+ });
175
+ }
176
+ function rangesFor(operation, beforeContent, postContent) {
177
+ const patches = Array.isArray(operation.patches) && operation.patches.length
178
+ ? operation.patches
179
+ : [{ from: 0, to: beforeContent.length, insert: postContent }];
180
+ let delta = 0;
181
+ return patches.map(patch => {
182
+ const range = { from: patch.from, to: patch.to,
183
+ start: patch.from + delta, end: patch.from + delta + String(patch.insert ?? '').length,
184
+ removedLength: patch.to - patch.from, removedHash: fingerprint(beforeContent.slice(patch.from, patch.to)) };
185
+ delta += String(patch.insert ?? '').length - (patch.to - patch.from);
186
+ return range;
187
+ });
188
+ }
189
+ function shiftedBaseline(before, ranges) {
190
+ let overlap = false;
191
+ const refs = before.map(ref => {
192
+ const match = /^pos:(\d+):/.exec(ref.key);
193
+ if (!match) return ref;
194
+ const pos = Number(match[1]);
195
+ let delta = 0;
196
+ for (const range of ranges) {
197
+ if (pos >= range.from && pos < range.to) overlap = true;
198
+ if (pos >= range.to) delta += (range.end - range.start) - (range.to - range.from);
199
+ }
200
+ return { ...ref, key: ref.key.replace(/^pos:\d+:/, 'pos:' + (pos + delta) + ':') };
201
+ });
202
+ return { refs, overlap };
203
+ }
204
+ function allowedRef(ref, capture) {
205
+ if (ref.path !== capture.path) return false;
206
+ if (capture.source === 'native') return nativeOwned(ref, capture);
207
+ const match = /^pos:(\d+):/.exec(ref.key);
208
+ if (match) return capture.ranges.some(range =>
209
+ Number(match[1]) >= range.start && Number(match[1]) <= range.end);
210
+ // Stable native IDs can be compared with the fixed pre-write baseline.
211
+ // Text-only viewport signatures are retained for legacy lookup, but are
212
+ // insufficient to assign a new task ownership after a delayed capture.
213
+ return Boolean(ref.id);
214
+ }
215
+ function scopeCovered(refs, capture) {
216
+ if (capture.source === 'native') return capture.ranges.every(range => refs.some(ref =>
217
+ ref.from <= range.end && ref.to >= range.start));
218
+ const positioned = refs.filter(ref => /^pos:\d+:/.test(ref.key));
219
+ if (refs.some(ref => ref.id)) return true;
220
+ return capture.ranges.every(range => positioned.some(ref => {
221
+ const pos = Number(/^pos:(\d+):/.exec(ref.key)[1]);
222
+ return pos >= range.start && pos <= range.end;
223
+ }));
224
+ }
225
+ async function waitForTrackedChangeDiff(before, paths, options = {}) {
226
+ const waitMs = Math.max(0, Number(options.waitMs ?? 5000));
227
+ const intervalMs = Math.max(1, Number(options.intervalMs ?? 180));
228
+ const stableMs = Math.max(0, Number(options.stableMs ?? 500));
229
+ const start = now(), deadline = start + waitMs;
230
+ const found = new Map();
231
+ const native = options.capture?.source === 'native', beforeIds = new Set(before.map(ref => ref.id));
232
+ let lastSnapshotReady = true, sourceReason = '';
233
+ let lastChange = start, signature = '', polls = 0, afterCount = 0, filtered = 0;
234
+ let reason = 'capture_timeout';
235
+ while (now() <= deadline) {
236
+ if (options.guard && !options.guard()) { reason = 'capture_context_changed'; break; }
237
+ const snapshot = options.readSnapshot ? options.readSnapshot() : { ready: true, refs: collectTrackedChangeRefsForPaths(paths) };
238
+ lastSnapshotReady = snapshot.ready === true; sourceReason = snapshot.reason || '';
239
+ const after = lastSnapshotReady ? snapshot.refs : [];
240
+ if (native) found.clear();
241
+ polls++; afterCount = after.length;
242
+ const candidates = native ? after.filter(ref => !beforeIds.has(ref.id)) : diff(before, after);
243
+ for (const ref of candidates) {
244
+ if (options.capture && !allowedRef(ref, options.capture)) { filtered++; continue; }
245
+ found.set(identity(ref), ref);
246
+ }
247
+ const nextSignature = after.map(ref => native ? ref.key : identity(ref)).sort().join('\n');
248
+ if (nextSignature !== signature) { signature = nextSignature; lastChange = now(); }
249
+ if (options.stopOnFirst && found.size) { reason = 'observed'; break; }
250
+ if (now() >= deadline) break;
251
+ await delay(Math.min(intervalMs, deadline - now()));
252
+ }
253
+ const trackedChanges = Array.from(found.values());
254
+ if (lastSnapshotReady && reason !== 'capture_context_changed' && trackedChanges.length
255
+ && now() - lastChange >= stableMs
256
+ && (!options.capture || scopeCovered(trackedChanges, options.capture))
257
+ && afterCount < limit) reason = 'observed';
258
+ return {
259
+ ok: reason === 'observed' || options.stopOnFirst === true,
260
+ trackedChanges, reason, waitMs,
261
+ diagnostics: { beforeCount: before.length, afterCount, capturedCount: trackedChanges.length,
262
+ filteredCount: filtered, polls, elapsedMs: Math.max(0, now() - start), reason,
263
+ source: options.capture?.source || 'dom', sourceReason }
264
+ };
265
+ }
266
+ async function captureTrackedWrite({ trackedBefore, captureBaseline, operation, beforeContent, postContent, runProjectId }) {
267
+ if (beforeContent === postContent) return { trackedChanges: [], capture: null };
268
+ const ranges = rangesFor(operation, beforeContent, postContent);
269
+ const baseline = shiftedBaseline(trackedBefore, ranges);
270
+ const startedAt = now();
271
+ const capture = {
272
+ version: 1, path: operation.path, runProjectId, startedAt, expiresAt: startedAt + 35000,
273
+ expected: fingerprint(postContent), ranges, source: captureBaseline?.source || 'dom',
274
+ nativeDocId: captureBaseline?.nativeDocId || '',
275
+ before: baseline.refs.map(({ key, id, path }) => ({ key, id, path })),
276
+ refs: [], state: 'pending', reason: ''
277
+ };
278
+ if (captureBaseline && !captureBaseline.ready) {
279
+ capture.state = 'needs_review'; capture.reason = captureBaseline.reason || 'capture_baseline_unavailable';
280
+ return { trackedChanges: [], capture };
281
+ }
282
+ if (baseline.overlap || trackedBefore.length >= limit) {
283
+ capture.state = 'needs_review';
284
+ capture.reason = baseline.overlap ? 'preexisting_change_overlap' : 'capture_baseline_limit';
285
+ return { trackedChanges: [], capture };
286
+ }
287
+ return observe(capture, postContent, 5000);
288
+ }
289
+ function validCapture(capture) {
290
+ return capture?.version === 1 && typeof capture.path === 'string'
291
+ && capture.path === normalizeSafeProjectPath(capture.path)
292
+ && Boolean(capture.path) && typeof capture.expected === 'string'
293
+ && Number.isFinite(capture.expiresAt) && Number.isFinite(capture.startedAt)
294
+ && capture.expiresAt > capture.startedAt && capture.expiresAt - capture.startedAt <= 35000
295
+ && Array.isArray(capture.before) && capture.before.length < limit
296
+ && capture.before.every(ref => ref && typeof ref.key === 'string' && ref.path === capture.path)
297
+ && [undefined, 'dom', 'native'].includes(capture.source)
298
+ && (capture.source !== 'native' || (typeof capture.nativeDocId === 'string' && capture.nativeDocId
299
+ && capture.before.every(ref => typeof ref.id === 'string' && ref.id)))
300
+ && Array.isArray(capture.ranges) && capture.ranges.length > 0 && capture.ranges.length <= 1000
301
+ && capture.ranges.every(r => Number.isSafeInteger(r.start) && Number.isSafeInteger(r.end)
302
+ && r.start >= 0 && r.end >= r.start);
303
+ }
304
+ async function observe(input, expectedContent, waitMs) {
305
+ const capture = JSON.parse(JSON.stringify(input));
306
+ if (!validCapture(capture) || typeof expectedContent !== 'string'
307
+ || fingerprint(expectedContent) !== capture.expected) {
308
+ return { ok: false, trackedChanges: [], capture: { ...capture, state: 'needs_review', reason: 'capture_invalid_baseline' } };
309
+ }
310
+ if (now() >= capture.expiresAt) {
311
+ return { ok: false, trackedChanges: [], capture: { ...capture, state: 'needs_review', reason: 'capture_expired' } };
312
+ }
313
+ if (getActiveFilePath() !== capture.path) {
314
+ return { ok: false, trackedChanges: [], capture: { ...capture, state: 'pending', reason: 'capture_view_not_active' } };
315
+ }
316
+ const guard = () => getActiveFilePath() === capture.path && readActiveEditorText() === expectedContent
317
+ && (!deps.getProjectId || deps.getProjectId() === capture.runProjectId);
318
+ const result = await waitForTrackedChangeDiff(capture.before, [capture.path], {
319
+ waitMs: Math.min(waitMs, capture.expiresAt - now()), intervalMs: 180, stableMs: 500, capture, guard,
320
+ readSnapshot: capture.source === 'native' ? () => readNativeSnapshot(capture.path, capture.nativeDocId) : null
321
+ });
322
+ capture.refs = result.trackedChanges;
323
+ capture.diagnostics = result.diagnostics;
324
+ capture.reason = result.reason;
325
+ capture.state = result.reason === 'observed' ? 'observed'
326
+ : result.reason === 'capture_context_changed' ? 'needs_review' : 'pending';
327
+ if (now() >= capture.expiresAt && capture.state === 'pending') capture.state = 'needs_review';
328
+ return { ok: capture.state === 'observed', trackedChanges: capture.state === 'observed' ? capture.refs : [], capture };
329
+ }
330
+ async function reconcileTrackedChangeCapture(params = {}) {
331
+ const blocked = deps.checkWritebackRunProjectId?.(params);
332
+ if (blocked) return blocked;
333
+ if (params.capture?.runProjectId !== params.runProjectId) {
334
+ return { ok: false, code: 'capture_project_mismatch', trackedChanges: [] };
335
+ }
336
+ return observe(params.capture, params.expectedContent, 1200);
337
+ }
338
+ return { collectTrackedChangeNodes, trackedChangeRefFromNode, collectTrackedChangeRefsForPaths,
339
+ waitForTrackedChangeDiff, prepareTrackedChangeCapture, prepareTrackedChangeReview, prepareUntrackedUndo, getTrackedChangeCaptureStatus, captureTrackedWrite, reconcileTrackedChangeCapture };
340
+ }
341
+ return { create };
342
+ });