@erdemtuna/doc-review 0.11.1 → 0.13.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 +23 -25
- package/lib/SKILL.md +65 -202
- package/lib/agent-handoff.js +24 -0
- package/lib/agent-output.js +287 -0
- package/lib/anchor-text.js +31 -19
- package/lib/chrome-api.js +23 -4
- package/lib/chrome.html +3 -25
- package/lib/cli.js +282 -104
- package/lib/comment-target.js +4 -0
- package/lib/contracts/agent.js +122 -0
- package/lib/contracts/feedback.js +369 -1
- package/lib/contracts/frame.js +87 -0
- package/lib/contracts/history.js +189 -1
- package/lib/contracts/index.js +7 -2
- package/lib/contracts/page-boundary.js +187 -0
- package/lib/contracts/validation.js +200 -0
- package/lib/conversation-anchor-controller.js +84 -0
- package/lib/conversation-capture.js +81 -0
- package/lib/conversation-controller.js +798 -0
- package/lib/conversation-save.js +175 -0
- package/lib/conversation-server.js +184 -0
- package/lib/conversation-shell.js +1013 -0
- package/lib/conversation-store.js +809 -0
- package/lib/frame-controller.js +188 -23
- package/lib/frame-host.js +10 -0
- package/lib/frame-policy.js +1 -0
- package/lib/history-policy.js +4 -15
- package/lib/history-server.js +42 -326
- package/lib/html-transform.js +19 -4
- package/lib/icons.js +323 -0
- package/lib/new-message-target.js +27 -0
- package/lib/paths.js +4 -4
- package/lib/poll-transport.js +109 -26
- package/lib/positioning.js +51 -0
- package/lib/recovery-controller.js +9 -0
- package/lib/references/context-and-recovery.md +130 -0
- package/lib/references/response-contract.md +108 -0
- package/lib/references/source-edits.md +47 -0
- package/lib/review-palette.js +97 -0
- package/lib/revision-store.js +1 -1
- package/lib/save-controller.js +28 -14
- package/lib/sdk.js +568 -92
- package/lib/server.js +73 -595
- package/lib/setup.js +17 -48
- package/lib/state.js +21 -7
- package/lib/thread-anchor-controller.js +56 -0
- package/lib/toolbar-controller.js +3 -3
- package/lib/ui/THIRD_PARTY_NOTICES.md +127 -8
- package/lib/ui/chrome.css +1220 -2733
- package/lib/ui/chrome.js +81 -17
- package/package.json +2 -2
- package/lib/chrome-client.js +0 -1700
package/lib/frame-controller.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { record } from "./chrome-api.js";
|
|
2
|
-
|
|
2
|
+
import { isThemePayload } from "./contracts/frame.js";
|
|
3
|
+
export function createFrameController({ sessionId, host, request, suspended, transitioning = () => { }, failed, now = Date.now, activated = () => { }, diagnostic = () => { }, setTimer = globalThis.setTimeout, clearTimer = globalThis.clearTimeout, }) {
|
|
3
4
|
const state = {
|
|
4
5
|
key: null, generation: 0, renderId: null, capability: null, sourceHash: null,
|
|
5
6
|
execution: null, policy: null, phase: { kind: "loading" }, readyAt: 0,
|
|
@@ -17,6 +18,10 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
17
18
|
let flushSequence = 0;
|
|
18
19
|
let configuration = null;
|
|
19
20
|
let expectedConfiguration = null;
|
|
21
|
+
let desired = { theme: "light", themeRevision: 1 };
|
|
22
|
+
let currentTheme = null;
|
|
23
|
+
let previousTheme = null;
|
|
24
|
+
let retainedCandidate = null;
|
|
20
25
|
const alive = () => state.phase.kind !== "disposed";
|
|
21
26
|
const loading = () => state.phase.kind !== "ready";
|
|
22
27
|
const publish = () => { if (alive())
|
|
@@ -49,7 +54,114 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
49
54
|
...message, capability: state.capability, generation: state.generation, pageKey: state.key,
|
|
50
55
|
}, state.policy?.targetOrigin || "*");
|
|
51
56
|
}
|
|
52
|
-
function
|
|
57
|
+
function cancelTheme(channel) {
|
|
58
|
+
if (!channel)
|
|
59
|
+
return;
|
|
60
|
+
cancelTimer(channel.timer);
|
|
61
|
+
channel.timer = null;
|
|
62
|
+
channel.pending = null;
|
|
63
|
+
channel.failed = false;
|
|
64
|
+
channel.settle?.(false);
|
|
65
|
+
channel.settle = null;
|
|
66
|
+
}
|
|
67
|
+
function clearPreviousTheme() {
|
|
68
|
+
cancelTheme(previousTheme);
|
|
69
|
+
previousTheme = null;
|
|
70
|
+
publish();
|
|
71
|
+
}
|
|
72
|
+
const stopRemoval = host.onPreviousRemoved(clearPreviousTheme);
|
|
73
|
+
const themeApplied = (channel) => channel?.applied?.themeRevision === desired.themeRevision && channel.applied.theme === desired.theme &&
|
|
74
|
+
!channel.pending && !channel.failed;
|
|
75
|
+
function handoff() {
|
|
76
|
+
if (!host.previous || loading() || !expectedConfiguration ||
|
|
77
|
+
state.configurationGeneration !== state.generation || !themeApplied(currentTheme))
|
|
78
|
+
return;
|
|
79
|
+
const started = identity();
|
|
80
|
+
const appliedConfiguration = expectedConfiguration;
|
|
81
|
+
const revision = desired.themeRevision;
|
|
82
|
+
host.afterPaint(() => {
|
|
83
|
+
if (matches(started) && !loading() && expectedConfiguration === appliedConfiguration &&
|
|
84
|
+
state.configurationGeneration === state.generation && desired.themeRevision === revision &&
|
|
85
|
+
themeApplied(currentTheme))
|
|
86
|
+
finishReplacement();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function exposeReady() {
|
|
90
|
+
if (state.phase.kind !== "confirming" || !state.execution || !themeApplied(currentTheme))
|
|
91
|
+
return;
|
|
92
|
+
state.phase = { kind: "ready" };
|
|
93
|
+
host.ready(state.execution);
|
|
94
|
+
publish();
|
|
95
|
+
activated(state.execution);
|
|
96
|
+
}
|
|
97
|
+
function requestTheme(channel) {
|
|
98
|
+
cancelTheme(channel);
|
|
99
|
+
channel.pending = { ...desired };
|
|
100
|
+
const pending = channel.pending;
|
|
101
|
+
const promise = new Promise((resolve) => { channel.settle = resolve; });
|
|
102
|
+
channel.timer = later(() => {
|
|
103
|
+
if (channel.pending !== pending)
|
|
104
|
+
return;
|
|
105
|
+
channel.timer = null;
|
|
106
|
+
channel.pending = null;
|
|
107
|
+
channel.failed = true;
|
|
108
|
+
channel.settle?.(false);
|
|
109
|
+
channel.settle = null;
|
|
110
|
+
publish();
|
|
111
|
+
}, 3000);
|
|
112
|
+
const message = {
|
|
113
|
+
type: "eh:setTheme", ...pending, capability: channel.capability,
|
|
114
|
+
generation: channel.generation, pageKey: channel.pageKey,
|
|
115
|
+
};
|
|
116
|
+
if (channel === currentTheme)
|
|
117
|
+
host.send(message, channel.policy.targetOrigin);
|
|
118
|
+
else if (channel.source && channel.source === host.previousWindow) {
|
|
119
|
+
channel.source.postMessage(message, channel.policy.targetOrigin);
|
|
120
|
+
}
|
|
121
|
+
publish();
|
|
122
|
+
return promise;
|
|
123
|
+
}
|
|
124
|
+
function handleThemeMessage(event) {
|
|
125
|
+
const value = event.data;
|
|
126
|
+
if (!alive() || !value || typeof value !== "object" || !("type" in value) ||
|
|
127
|
+
(value.type !== "eh:themeApplied" && value.type !== "eh:setTheme"))
|
|
128
|
+
return false;
|
|
129
|
+
const channel = [currentTheme, previousTheme].find((candidate) => candidate && candidate.source &&
|
|
130
|
+
candidate.source === event.source && candidate.policy.incomingOrigin === event.origin &&
|
|
131
|
+
"capability" in value && value.capability === candidate.capability &&
|
|
132
|
+
"generation" in value && value.generation === candidate.generation &&
|
|
133
|
+
"pageKey" in value && value.pageKey === candidate.pageKey);
|
|
134
|
+
if (!channel)
|
|
135
|
+
return true;
|
|
136
|
+
if (channel === previousTheme && channel.source !== host.previousWindow) {
|
|
137
|
+
clearPreviousTheme();
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
if (value.type !== "eh:themeApplied" || !isThemePayload(value)) {
|
|
141
|
+
if (!channel.diagnosed) {
|
|
142
|
+
channel.diagnosed = true;
|
|
143
|
+
diagnostic("invalid-theme-acknowledgment");
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (!channel.pending || channel.pending.themeRevision !== value.themeRevision ||
|
|
148
|
+
channel.pending.theme !== value.theme)
|
|
149
|
+
return true;
|
|
150
|
+
channel.applied = { theme: value.theme, themeRevision: value.themeRevision };
|
|
151
|
+
cancelTimer(channel.timer);
|
|
152
|
+
channel.timer = null;
|
|
153
|
+
channel.pending = null;
|
|
154
|
+
channel.failed = false;
|
|
155
|
+
channel.settle?.(true);
|
|
156
|
+
channel.settle = null;
|
|
157
|
+
if (channel === currentTheme) {
|
|
158
|
+
exposeReady();
|
|
159
|
+
handoff();
|
|
160
|
+
}
|
|
161
|
+
publish();
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
function suspend(preserveTheme = false) {
|
|
53
165
|
if (!alive())
|
|
54
166
|
return;
|
|
55
167
|
suspended();
|
|
@@ -57,6 +169,13 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
57
169
|
flush.settle(false);
|
|
58
170
|
configuration?.settle(false);
|
|
59
171
|
expectedConfiguration = null;
|
|
172
|
+
cancelTheme(currentTheme);
|
|
173
|
+
cancelTheme(previousTheme);
|
|
174
|
+
if (!preserveTheme) {
|
|
175
|
+
previousTheme = null;
|
|
176
|
+
retainedCandidate = null;
|
|
177
|
+
}
|
|
178
|
+
currentTheme = null;
|
|
60
179
|
for (const timer of timers)
|
|
61
180
|
clearTimer(timer);
|
|
62
181
|
timers.clear();
|
|
@@ -73,7 +192,8 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
73
192
|
if (!alive())
|
|
74
193
|
throw new Error("Review ended");
|
|
75
194
|
transitioning();
|
|
76
|
-
|
|
195
|
+
retainedCandidate = currentTheme || retainedCandidate;
|
|
196
|
+
suspend(true);
|
|
77
197
|
state.key = key;
|
|
78
198
|
state.generation++;
|
|
79
199
|
state.sourceHash = null;
|
|
@@ -117,6 +237,13 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
117
237
|
state.phase = { kind: "loading" };
|
|
118
238
|
initialLoad = true;
|
|
119
239
|
host.navigate(value.path, state.reloading);
|
|
240
|
+
if (previousTheme?.source !== host.previousWindow)
|
|
241
|
+
clearPreviousTheme();
|
|
242
|
+
if (!previousTheme && retainedCandidate?.source === host.previousWindow)
|
|
243
|
+
previousTheme = retainedCandidate;
|
|
244
|
+
retainedCandidate = null;
|
|
245
|
+
if (previousTheme && !themeApplied(previousTheme))
|
|
246
|
+
void requestTheme(previousTheme);
|
|
120
247
|
publish();
|
|
121
248
|
readinessTimer = later(() => {
|
|
122
249
|
if (state.key !== key || state.generation !== generation || !loading())
|
|
@@ -181,10 +308,15 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
181
308
|
feedbackOnly: value.savePolicy === "feedback-only",
|
|
182
309
|
executionNotice: typeof value.executionNotice === "string" ? value.executionNotice : null,
|
|
183
310
|
};
|
|
184
|
-
state.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
311
|
+
if (!state.policy || !state.key)
|
|
312
|
+
throw new Error("Missing frame policy.");
|
|
313
|
+
currentTheme = {
|
|
314
|
+
source: host.currentWindow, capability: state.capability, generation: state.generation,
|
|
315
|
+
pageKey: state.key, policy: { ...state.policy }, applied: null, pending: null,
|
|
316
|
+
failed: false, timer: null, settle: null, diagnosed: false,
|
|
317
|
+
};
|
|
318
|
+
await requestTheme(currentTheme);
|
|
319
|
+
return matches(started) && !loading() ? state.execution : null;
|
|
188
320
|
}
|
|
189
321
|
catch (error) {
|
|
190
322
|
if (matches(started)) {
|
|
@@ -206,14 +338,15 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
206
338
|
}
|
|
207
339
|
}, 5000);
|
|
208
340
|
}
|
|
209
|
-
function configure(mode, savePolicy, wait = true) {
|
|
341
|
+
function configure(mode, savePolicy, canComment, wait = true) {
|
|
210
342
|
if (loading() || !state.capability || !state.execution)
|
|
211
343
|
return Promise.resolve(false);
|
|
212
344
|
configuration?.settle(false);
|
|
213
|
-
expectedConfiguration = { mode, savePolicy };
|
|
345
|
+
expectedConfiguration = { mode, savePolicy, canComment };
|
|
346
|
+
state.configurationGeneration = null;
|
|
214
347
|
configurationTimeout();
|
|
215
348
|
if (!wait) {
|
|
216
|
-
send({ type: "eh:configureReview", mode, savePolicy });
|
|
349
|
+
send({ type: "eh:configureReview", mode, savePolicy, canComment });
|
|
217
350
|
return Promise.resolve(true);
|
|
218
351
|
}
|
|
219
352
|
return new Promise((resolve) => {
|
|
@@ -225,8 +358,8 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
225
358
|
cancelTimer(timer);
|
|
226
359
|
resolve(applied);
|
|
227
360
|
};
|
|
228
|
-
configuration = { mode, savePolicy, settle };
|
|
229
|
-
send({ type: "eh:configureReview", mode, savePolicy });
|
|
361
|
+
configuration = { mode, savePolicy, canComment, settle };
|
|
362
|
+
send({ type: "eh:configureReview", mode, savePolicy, canComment });
|
|
230
363
|
});
|
|
231
364
|
}
|
|
232
365
|
function flush(strict = false) {
|
|
@@ -252,6 +385,40 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
252
385
|
return {
|
|
253
386
|
get state() { return state; },
|
|
254
387
|
get loading() { return loading(); },
|
|
388
|
+
get themeSync() {
|
|
389
|
+
const channels = [currentTheme, previousTheme].filter((channel) => channel !== null);
|
|
390
|
+
const failure = channels.some((channel) => channel.failed);
|
|
391
|
+
return {
|
|
392
|
+
desired: { ...desired },
|
|
393
|
+
status: failure ? "failed" : channels.some((channel) => channel.pending) ? "pending" :
|
|
394
|
+
themeApplied(currentTheme) ? "applied" : "idle",
|
|
395
|
+
appliedRevision: currentTheme?.applied?.themeRevision ?? null,
|
|
396
|
+
message: failure ? "Annotation theme synchronization was not confirmed. Your page and drafts are unchanged. Retry theme without reloading." : null,
|
|
397
|
+
};
|
|
398
|
+
},
|
|
399
|
+
setTheme(theme) {
|
|
400
|
+
if (!alive() || (theme !== "light" && theme !== "dark") || desired.theme === theme)
|
|
401
|
+
return;
|
|
402
|
+
if (desired.themeRevision === Number.MAX_SAFE_INTEGER)
|
|
403
|
+
throw new Error("Theme revision limit reached.");
|
|
404
|
+
desired = { theme, themeRevision: desired.themeRevision + 1 };
|
|
405
|
+
if (currentTheme)
|
|
406
|
+
void requestTheme(currentTheme);
|
|
407
|
+
if (previousTheme && previousTheme.source === host.previousWindow)
|
|
408
|
+
void requestTheme(previousTheme);
|
|
409
|
+
publish();
|
|
410
|
+
},
|
|
411
|
+
async retryTheme() {
|
|
412
|
+
if (!alive())
|
|
413
|
+
return false;
|
|
414
|
+
const pending = [];
|
|
415
|
+
if (currentTheme)
|
|
416
|
+
pending.push(requestTheme(currentTheme));
|
|
417
|
+
if (previousTheme && previousTheme.source === host.previousWindow)
|
|
418
|
+
pending.push(requestTheme(previousTheme));
|
|
419
|
+
return pending.length > 0 && (await Promise.all(pending)).every(Boolean);
|
|
420
|
+
},
|
|
421
|
+
handleThemeMessage,
|
|
255
422
|
identity, matches, begin, register, suspend, ready, configure, flush, send, fail,
|
|
256
423
|
subscribe(listener) {
|
|
257
424
|
listeners.add(listener);
|
|
@@ -277,24 +444,21 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
277
444
|
return false;
|
|
278
445
|
return value.capability === state.capability && value.generation === state.generation &&
|
|
279
446
|
value.pageKey === state.key && typeof value.type === "string" &&
|
|
280
|
-
(!loading() || value.type === "eh:ready"
|
|
447
|
+
(!loading() || value.type === "eh:ready" ||
|
|
448
|
+
(state.phase.kind === "confirming" && value.type === "eh:themeApplied" &&
|
|
449
|
+
isThemePayload(value) && value.theme === currentTheme?.pending?.theme &&
|
|
450
|
+
value.themeRevision === currentTheme.pending.themeRevision));
|
|
281
451
|
},
|
|
282
|
-
configured(mode, savePolicy) {
|
|
452
|
+
configured(mode, savePolicy, canComment) {
|
|
283
453
|
if (!expectedConfiguration || expectedConfiguration.mode !== mode ||
|
|
284
|
-
expectedConfiguration.savePolicy !== savePolicy || loading())
|
|
454
|
+
expectedConfiguration.savePolicy !== savePolicy || expectedConfiguration.canComment !== canComment || loading())
|
|
285
455
|
return false;
|
|
286
456
|
// Confirmation is complete even when background tabs pause the visual handoff.
|
|
287
457
|
cancelTimer(configurationTimer);
|
|
288
458
|
configurationTimer = null;
|
|
289
459
|
state.configurationGeneration = state.generation;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (host.previous)
|
|
293
|
-
host.afterPaint(() => {
|
|
294
|
-
if (matches(started) && expectedConfiguration === appliedConfiguration && !loading())
|
|
295
|
-
finishReplacement();
|
|
296
|
-
});
|
|
297
|
-
if (configuration && configuration.mode === mode && configuration.savePolicy === savePolicy) {
|
|
460
|
+
handoff();
|
|
461
|
+
if (configuration && configuration.mode === mode && configuration.savePolicy === savePolicy && configuration.canComment === canComment) {
|
|
298
462
|
configuration.settle(true);
|
|
299
463
|
}
|
|
300
464
|
return true;
|
|
@@ -313,6 +477,7 @@ export function createFrameController({ sessionId, host, request, suspended, tra
|
|
|
313
477
|
suspend();
|
|
314
478
|
state.phase = { kind: "disposed" };
|
|
315
479
|
stopLoad();
|
|
480
|
+
stopRemoval();
|
|
316
481
|
host.dispose();
|
|
317
482
|
listeners.clear();
|
|
318
483
|
},
|
package/lib/frame-host.js
CHANGED
|
@@ -4,6 +4,7 @@ export function createFrameHost(initial, artifactOrigin) {
|
|
|
4
4
|
let disposed = false;
|
|
5
5
|
const loadListeners = new Set();
|
|
6
6
|
const focusListeners = new Set();
|
|
7
|
+
const removalListeners = new Set();
|
|
7
8
|
const detach = new Map();
|
|
8
9
|
const paintFrames = new Set();
|
|
9
10
|
function paint(callback) {
|
|
@@ -41,6 +42,12 @@ export function createFrameHost(initial, artifactOrigin) {
|
|
|
41
42
|
return {
|
|
42
43
|
get current() { return current; },
|
|
43
44
|
get previous() { return previous; },
|
|
45
|
+
get currentWindow() { return current.contentWindow; },
|
|
46
|
+
get previousWindow() { return previous?.contentWindow || null; },
|
|
47
|
+
onPreviousRemoved(listener) {
|
|
48
|
+
removalListeners.add(listener);
|
|
49
|
+
return () => { removalListeners.delete(listener); };
|
|
50
|
+
},
|
|
44
51
|
get visibleExecution() {
|
|
45
52
|
const visible = previous || current;
|
|
46
53
|
const savePolicy = visible.dataset.savePolicy;
|
|
@@ -98,6 +105,8 @@ export function createFrameHost(initial, artifactOrigin) {
|
|
|
98
105
|
if (previous)
|
|
99
106
|
remove(previous);
|
|
100
107
|
previous = null;
|
|
108
|
+
for (const listener of removalListeners)
|
|
109
|
+
listener();
|
|
101
110
|
delete current.dataset.replacing;
|
|
102
111
|
},
|
|
103
112
|
suspend() { current.removeAttribute("data-sdk-ready"); },
|
|
@@ -131,6 +140,7 @@ export function createFrameHost(initial, artifactOrigin) {
|
|
|
131
140
|
paintFrames.clear();
|
|
132
141
|
loadListeners.clear();
|
|
133
142
|
focusListeners.clear();
|
|
143
|
+
removalListeners.clear();
|
|
134
144
|
},
|
|
135
145
|
};
|
|
136
146
|
}
|
package/lib/frame-policy.js
CHANGED
|
@@ -8,6 +8,7 @@ export const TRUSTED_SDK_MODULE_PATHS = Object.freeze([
|
|
|
8
8
|
"/editing.js", "/frame-channel.js", "/icons.js", "/review-mode.js",
|
|
9
9
|
"/semantic-snapshot.js", "/serialize.js", "/positioning.js",
|
|
10
10
|
"/revision-schema.js", "/view-identity.js",
|
|
11
|
+
"/thread-anchor-controller.js", "/contracts/frame.js", "/contracts/feedback.js", "/contracts/validation.js",
|
|
11
12
|
]);
|
|
12
13
|
export function interactiveFileCsp(sdkOrigin) {
|
|
13
14
|
const origin = new URL(sdkOrigin);
|
package/lib/history-policy.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isRevisionId } from "./revision-schema.js";
|
|
2
2
|
function references(value, out = new Set()) {
|
|
3
3
|
if (isRevisionId(value))
|
|
4
4
|
out.add(value);
|
|
@@ -23,21 +23,10 @@ export function historyRevisionReferences(data) {
|
|
|
23
23
|
const out = unsentRevisionReferences(data);
|
|
24
24
|
references(data.histories, out);
|
|
25
25
|
references(data.batches, out);
|
|
26
|
+
references(data.conversations, out);
|
|
26
27
|
return out;
|
|
27
28
|
}
|
|
28
29
|
export function retainHistory(data) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
for (const history of Object.values(data.histories || {})) {
|
|
32
|
-
const completed = history.rounds
|
|
33
|
-
.filter((round) => round.completedAt)
|
|
34
|
-
.sort((a, b) => b.completedAt - a.completedAt || b.ordinal - a.ordinal);
|
|
35
|
-
const keep = new Set(completed.slice(0, COMPLETED_ROUNDS_TO_KEEP).map((round) => round.roundId));
|
|
36
|
-
const rounds = history.rounds.filter((round) => !round.completedAt || keep.has(round.roundId) || [...references(round.targets)].some((id) => pinned.has(id)));
|
|
37
|
-
if (rounds.length !== history.rounds.length) {
|
|
38
|
-
history.rounds = rounds;
|
|
39
|
-
changed = true;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return changed;
|
|
30
|
+
// Referenced comparisons are durable review records, not age/count debris.
|
|
31
|
+
return false;
|
|
43
32
|
}
|