@stage5/lumine 0.2.66 → 0.2.68

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 CHANGED
@@ -387,6 +387,17 @@ lumine admin subject unfeature 123 --json
387
387
  lumine admin featured reorder --subject-ids 30,20,10 --json
388
388
  lumine admin featured rotate --remove-subject-ids 30,20 \
389
389
  --add-subject-ids 50,40 --json
390
+ lumine admin featured plan --remove-subject-ids 30,20 \
391
+ --add-subject-ids 50,40 --subject-ids 40,50,10 \
392
+ --posted-after 2026-09-01T00:00:00+07:00 --output featured-plan.json --json
393
+ # Only after Mikey approves the exact mapping and final order:
394
+ lumine admin featured apply --file featured-plan.json --approve <exact-plan-hash> --json
395
+ lumine admin featured comments scan --checkpoint featured-read.json --json
396
+ # Read every page file before acknowledging; downloads are not reviews.
397
+ lumine admin featured comments acknowledge --checkpoint featured-read.json --reviewed --json
398
+ lumine admin featured comments recommend --file featured-decisions.json \
399
+ --checkpoint featured-recommend.json --json
400
+ lumine admin featured comments report --checkpoint featured-read.json --json
390
401
  lumine admin brief --days 3 --json
391
402
  lumine admin notable add Minecrarft_guy --note "Created 8 thoughtful subjects and helped peers in 23 comments this window." --json
392
403
  lumine admin post recommend comment:456 --anyone-can-reward --reward-twinkles 3 --json
@@ -418,7 +429,9 @@ Zero/Ciel responder without Lumine remaining active.
418
429
 
419
430
  `--scope full` is the default and authorizes the complete daily-management
420
431
  workflow. `--scope featured` is a deliberately narrow run for a requested
421
- Featured-only slice: it cannot comment, does not surface carry-over todos, does
432
+ Featured-only slice: it can review and encourage snapshot-bound Featured
433
+ comments, but cannot publish comments or use generic recommendation/reward
434
+ commands. It does not surface carry-over todos, does
422
435
  not write full-run queue coverage, does not require sponsor review, and never
423
436
  advances full-run review windows. Its dedicated start endpoint also prevents an
424
437
  older API from silently interpreting it as a full run. The
@@ -427,6 +440,18 @@ the server to prove both a strict posting-date boundary and complete
427
440
  never-Featured history. `featured history` returns that evidence without the
428
441
  large repeated board snapshots in the general audit trail.
429
442
 
443
+ `featured plan`/`apply` bind approval to the exact server-stored board and
444
+ history revision; all approved replacements and ordering commit together.
445
+ Equal-size swaps and reorders support the website's 100-Subject capacity while
446
+ delegated growth remains capped at 20. The CLI verifies the final live order.
447
+ `featured comments scan` walks every thread (including old/nested replies),
448
+ with private, hashed pages and `--resume` recovery. `acknowledge --reviewed`
449
+ records actual reading separately from downloading. `recommend` applies only
450
+ agent-selected comments from acknowledged threads, with reward eligibility off
451
+ by default, and resumes the exact decision file/checkpoint safely. See
452
+ [the admin contract](sdk/LUMINE_ADMIN.md#exact-approved-refresh-and-resumable-comment-encouragement)
453
+ for the decision-file format, partial-coverage rules, and deployment prerequisites.
454
+
430
455
  Management agents also inspect recent public Build candidates during each full
431
456
  daily review.
432
457
  `builds review` opens one published app in an isolated temporary Chromium
@@ -0,0 +1,559 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ existsSync,
5
+ fsyncSync,
6
+ openSync,
7
+ readFileSync,
8
+ } from "node:fs";
9
+ import path from "node:path";
10
+ import { requestJson } from "./http.js";
11
+ import { readAdminJsonFile, writeAdminJsonFile } from "./admin-news.js";
12
+ import {
13
+ acquireCheckpointLock,
14
+ releaseCheckpointLock,
15
+ } from "./admin-workflows.js";
16
+
17
+ const MAX_BYTES = 16 * 1024 * 1024;
18
+ const BASE = "/cli/admin/subjects/featured";
19
+ const hashBytes = (value) => createHash("sha256").update(value).digest("hex");
20
+ const canonical = (value) =>
21
+ Array.isArray(value)
22
+ ? value.map(canonical)
23
+ : value && typeof value === "object"
24
+ ? Object.fromEntries(
25
+ Object.keys(value)
26
+ .sort()
27
+ .map((key) => [key, canonical(value[key])]),
28
+ )
29
+ : value;
30
+ export const featuredFingerprint = (value) =>
31
+ hashBytes(JSON.stringify(canonical(value)));
32
+
33
+ function invalid(message, details) {
34
+ const error = new Error(message);
35
+ error.code = "CLI_ADMIN_FEATURED_WORKFLOW_INVALID";
36
+ if (details)
37
+ error.data = {
38
+ ok: false,
39
+ status: "partial_failure",
40
+ error: {
41
+ code: error.code,
42
+ message,
43
+ details,
44
+ },
45
+ };
46
+ return error;
47
+ }
48
+ function positive(value, label) {
49
+ if (!Number.isSafeInteger(value) || value <= 0)
50
+ throw invalid(`${label} must be a positive integer.`);
51
+ return value;
52
+ }
53
+ function save(file, value) {
54
+ writeAdminJsonFile(file, value, { privateFile: true, maxBytes: MAX_BYTES });
55
+ const fd = openSync(file, "r");
56
+ try {
57
+ fsyncSync(fd);
58
+ } finally {
59
+ closeSync(fd);
60
+ }
61
+ }
62
+
63
+ export function readApprovedFeaturedPlan(file, approval) {
64
+ const raw = readAdminJsonFile(file, "the prepared Featured plan", {
65
+ maxBytes: MAX_BYTES,
66
+ });
67
+ const { plan, planHash } = raw.data || raw;
68
+ if (
69
+ !plan ||
70
+ plan.schemaVersion !== 1 ||
71
+ !Array.isArray(plan.finalIds) ||
72
+ !Array.isArray(plan.expectedIds) ||
73
+ !planHash ||
74
+ featuredFingerprint(plan) !== planHash ||
75
+ approval !== planHash
76
+ ) {
77
+ throw invalid(
78
+ "Read the prepared plan, obtain Mikey's go-ahead, then pass --approve <exact-plan-hash>. Modified plans cannot be applied.",
79
+ );
80
+ }
81
+ positive(plan.id, "Plan ID");
82
+ return { plan, planHash };
83
+ }
84
+
85
+ export function readFeaturedSelections(file) {
86
+ const raw = readAdminJsonFile(file, "agent-selected comment decisions", {
87
+ maxBytes: 2 * 1024 * 1024,
88
+ });
89
+ positive(raw?.reviewId, "Review ID");
90
+ positive(raw?.coverageId, "Reviewed coverage ID");
91
+ if (!Array.isArray(raw.selections) || raw.selections.length > 20_000) {
92
+ throw invalid(
93
+ "selections must be an array of at most 20000 agent-selected comments.",
94
+ );
95
+ }
96
+ const seen = new Set();
97
+ const selections = raw.selections.map((item) => {
98
+ positive(item?.commentId, "Comment ID");
99
+ positive(item?.pageId, "Page receipt ID");
100
+ if (seen.has(item.commentId))
101
+ throw invalid("Duplicate selected comment ID.");
102
+ seen.add(item.commentId);
103
+ if (
104
+ item.anyoneCanReward !== undefined &&
105
+ typeof item.anyoneCanReward !== "boolean"
106
+ ) {
107
+ throw invalid("anyoneCanReward must be a boolean.");
108
+ }
109
+ const anyoneCanReward = item.anyoneCanReward === true;
110
+ const rewardTwinkles = item.rewardTwinkles ?? 0;
111
+ if (
112
+ ![0, 3].includes(rewardTwinkles) ||
113
+ (rewardTwinkles && !anyoneCanReward)
114
+ ) {
115
+ throw invalid(
116
+ "rewardTwinkles must be 0, or 3 with anyoneCanReward=true.",
117
+ );
118
+ }
119
+ return {
120
+ commentId: item.commentId,
121
+ pageId: item.pageId,
122
+ anyoneCanReward,
123
+ rewardTwinkles,
124
+ };
125
+ });
126
+ return { reviewId: raw.reviewId, coverageId: raw.coverageId, selections };
127
+ }
128
+
129
+ function verifyPageFiles(state, checkpoint) {
130
+ if (!state.review) {
131
+ if (Object.keys(state.subjects || {}).length)
132
+ throw invalid("Checkpoint subjects have no review snapshot.");
133
+ return;
134
+ }
135
+ const ids = state.review.subjects.map((item) => String(item.id)).sort();
136
+ if (
137
+ JSON.stringify(ids) !==
138
+ JSON.stringify(Object.keys(state.subjects || {}).sort())
139
+ ) {
140
+ throw invalid(
141
+ "The checkpoint omits or adds Subjects relative to its review snapshot.",
142
+ );
143
+ }
144
+ for (const subject of Object.values(state.subjects || {})) {
145
+ const manifest = state.review.subjects.find(
146
+ (item) => item.id === subject.id,
147
+ );
148
+ if (!manifest || manifest.snapshotMaxId !== subject.snapshotMaxId)
149
+ throw invalid("Subject boundary changed.");
150
+ const verified = {
151
+ id: subject.id,
152
+ snapshotMaxId: subject.snapshotMaxId,
153
+ pages: [],
154
+ lastPageId: null,
155
+ commentsFetched: 0,
156
+ complete: false,
157
+ lastCommentId: subject.snapshotMaxId + 1,
158
+ };
159
+ let previous = null;
160
+ for (const entry of subject.pages || []) {
161
+ if (
162
+ entry.file !==
163
+ `${checkpoint}.page-${positive(entry.id, "Page ID")}.json`
164
+ ) {
165
+ throw invalid("A page file does not belong to this checkpoint.");
166
+ }
167
+ const bytes = readFileSync(entry.file);
168
+ if (bytes.length > MAX_BYTES || hashBytes(bytes) !== entry.sha256)
169
+ throw invalid(
170
+ "A downloaded page changed or is incomplete; do not claim review coverage.",
171
+ );
172
+ const result = JSON.parse(bytes.toString("utf8"));
173
+ assertPageResult(result, state, verified);
174
+ const page = result.data?.page;
175
+ if (
176
+ page?.id !== entry.id ||
177
+ page.reviewId !== state.review.id ||
178
+ page.subjectId !== subject.id ||
179
+ page.previousPageId !== previous ||
180
+ page.snapshotMaxId !== subject.snapshotMaxId
181
+ )
182
+ throw invalid("Page receipt chain does not match this review.");
183
+ previous = page.id;
184
+ verified.pages.push(entry);
185
+ verified.lastPageId = page.id;
186
+ verified.commentsFetched = page.commentsRead;
187
+ verified.complete = page.exhausted;
188
+ verified.lastCommentId =
189
+ result.data.comments.at(-1)?.id ?? verified.lastCommentId;
190
+ }
191
+ if (
192
+ subject.lastPageId !== previous ||
193
+ subject.complete !== verified.complete ||
194
+ subject.commentsFetched !== verified.commentsFetched ||
195
+ subject.lastCommentId !== verified.lastCommentId
196
+ ) {
197
+ throw invalid("Checkpoint progress does not match its downloaded pages.");
198
+ }
199
+ }
200
+ }
201
+
202
+ function assertPageResult(result, state, subject) {
203
+ const page = result?.data?.page;
204
+ positive(page?.id, "Canonical page ID");
205
+ const comments = result.data.comments;
206
+ if (
207
+ result.ok !== true ||
208
+ page.reviewId !== state.review.id ||
209
+ page.subjectId !== subject.id ||
210
+ page.snapshotMaxId !== subject.snapshotMaxId ||
211
+ page.previousPageId !== subject.lastPageId ||
212
+ page.pages !== subject.pages.length + 1 ||
213
+ !Array.isArray(comments) ||
214
+ page.commentsRead !== subject.commentsFetched + comments.length ||
215
+ typeof page.exhausted !== "boolean" ||
216
+ (page.exhausted ? page.nextCursor !== null : !page.nextCursor) ||
217
+ (!page.exhausted && !comments.length) ||
218
+ comments.some(
219
+ (item) =>
220
+ !Number.isSafeInteger(item.id) ||
221
+ item.id <= 0 ||
222
+ item.id > subject.snapshotMaxId,
223
+ ) ||
224
+ new Set(comments.map((item) => item.id)).size !== comments.length
225
+ ) {
226
+ throw invalid(
227
+ "The API did not confirm the exact continuing comment snapshot.",
228
+ );
229
+ }
230
+ if (
231
+ comments.some(
232
+ (item, index) =>
233
+ item.id >= (index ? comments[index - 1].id : subject.lastCommentId),
234
+ )
235
+ ) {
236
+ throw invalid(
237
+ "Comment pages repeated rows or did not advance in canonical order.",
238
+ );
239
+ }
240
+ return page;
241
+ }
242
+
243
+ export async function runFeaturedWorkflow({
244
+ options,
245
+ operation,
246
+ authToken,
247
+ runId,
248
+ request = requestJson,
249
+ }) {
250
+ const apiUrl = String(options.apiUrl).replace(/\/$/, "");
251
+ const call = async (suffix, body, key) =>
252
+ request({
253
+ method: body === undefined ? "GET" : "POST",
254
+ url: `${apiUrl}${BASE}${suffix}`,
255
+ authToken,
256
+ body,
257
+ timeoutMs: options.timeoutMs,
258
+ headers: {
259
+ "x-lumine-admin-run-id": String(runId),
260
+ ...(key ? { "x-lumine-idempotency-key": key } : {}),
261
+ },
262
+ });
263
+ if (operation.featuredWorkflow === "apply") {
264
+ const { plan, planHash } = readApprovedFeaturedPlan(
265
+ options.adminFile,
266
+ options.adminApprove,
267
+ );
268
+ if (plan.runId !== runId)
269
+ throw invalid(
270
+ "This plan belongs to a different run; prepare and approve a fresh plan.",
271
+ );
272
+ const result = await call(
273
+ "/plan/apply",
274
+ { planId: plan.id, approvedHash: planHash },
275
+ `cli:featured-apply:${runId}:${planHash.slice(0, 32)}`,
276
+ );
277
+ let listed;
278
+ try {
279
+ listed = await call("");
280
+ } catch (error) {
281
+ throw invalid(
282
+ "The server returned an apply receipt, but the final board read failed. Inspect the receipt and retry the same approved plan; do not assume no change occurred.",
283
+ {
284
+ canonicalResult: result,
285
+ expectedFinalIds: plan.finalIds,
286
+ verificationError: error.message,
287
+ },
288
+ );
289
+ }
290
+ const actual = listed?.data?.subjects?.map((item) => item.id);
291
+ if (
292
+ listed?.ok !== true ||
293
+ result?.ok !== true ||
294
+ result.data?.appliedPlanId !== plan.id ||
295
+ result.data?.approvedHash !== planHash ||
296
+ JSON.stringify(actual) !== JSON.stringify(plan.finalIds)
297
+ ) {
298
+ throw invalid(
299
+ "The approved plan's final board could not be verified. Do not restore or overwrite intervening changes.",
300
+ {
301
+ canonicalResult: result,
302
+ liveBoard: listed,
303
+ expectedFinalIds: plan.finalIds,
304
+ },
305
+ );
306
+ }
307
+ return { ...result, data: { ...result.data, verifiedFinalIds: actual } };
308
+ }
309
+
310
+ const workflow = operation.featuredWorkflow;
311
+ if (!options.adminCheckpoint)
312
+ throw invalid(
313
+ "Pass --checkpoint <private-file.json> for resumable Featured work.",
314
+ );
315
+ const checkpoint = path.resolve(options.adminCheckpoint);
316
+ const selection =
317
+ workflow === "recommend" ? readFeaturedSelections(options.adminFile) : null;
318
+ const fingerprint = featuredFingerprint({
319
+ apiUrl,
320
+ runId,
321
+ kind: selection ? "recommend" : "scan",
322
+ selection,
323
+ });
324
+ const lock = acquireCheckpointLock(checkpoint, fingerprint);
325
+ try {
326
+ let state;
327
+ if (
328
+ options.adminResume ||
329
+ workflow === "acknowledge" ||
330
+ workflow === "report"
331
+ ) {
332
+ state = readAdminJsonFile(checkpoint, "the Featured checkpoint", {
333
+ maxBytes: MAX_BYTES,
334
+ });
335
+ if (
336
+ state.schemaVersion !== 1 ||
337
+ state.fingerprint !== fingerprint ||
338
+ state.runId !== runId ||
339
+ state.apiUrl !== apiUrl
340
+ ) {
341
+ throw invalid(
342
+ "This checkpoint belongs to a different run, server, or selection. Resume the exact original operation.",
343
+ );
344
+ }
345
+ if (!selection) verifyPageFiles(state, checkpoint);
346
+ } else {
347
+ if (existsSync(checkpoint))
348
+ throw invalid(
349
+ "The checkpoint already exists. Use --resume or choose a new file.",
350
+ );
351
+ state = {
352
+ schemaVersion: 1,
353
+ fingerprint,
354
+ apiUrl,
355
+ runId,
356
+ requestId: `cli:featured-review:${randomUUID()}`,
357
+ subjects: {},
358
+ completed: {},
359
+ };
360
+ save(checkpoint, state);
361
+ }
362
+
363
+ if (workflow === "recommend") {
364
+ for (const item of selection.selections) {
365
+ if (state.completed[item.commentId]) continue;
366
+ try {
367
+ const result = await call(
368
+ `/reviews/${selection.reviewId}/recommendations/${item.commentId}`,
369
+ {
370
+ ...item,
371
+ coverageId: selection.coverageId,
372
+ },
373
+ `cli:featured-rec:${runId}:${fingerprint.slice(0, 24)}:${item.commentId}`,
374
+ );
375
+ const outcome = result?.data?.encouragement;
376
+ if (
377
+ result?.ok !== true ||
378
+ outcome?.commentId !== item.commentId ||
379
+ outcome?.reviewId !== selection.reviewId ||
380
+ outcome?.pageId !== item.pageId ||
381
+ outcome?.coverageId !== selection.coverageId ||
382
+ outcome?.anyoneCanRewardRequested !== item.anyoneCanReward ||
383
+ outcome?.rewardTwinklesRequested !== item.rewardTwinkles
384
+ ) {
385
+ throw invalid(
386
+ "The API did not confirm this exact comment decision.",
387
+ { canonicalResult: result },
388
+ );
389
+ }
390
+ state.completed[item.commentId] = outcome;
391
+ save(checkpoint, state);
392
+ } catch (error) {
393
+ error.featuredProgress = {
394
+ checkpointPath: checkpoint,
395
+ completedCount: Object.keys(state.completed).length,
396
+ failedCommentId: item.commentId,
397
+ targetCount: selection.selections.length,
398
+ };
399
+ throw error;
400
+ }
401
+ }
402
+ const report = await call(`/reviews/${selection.reviewId}/report`);
403
+ return {
404
+ ok: true,
405
+ status: "success",
406
+ changed: Object.values(state.completed).some((item) => item.changed),
407
+ data: {
408
+ batch: {
409
+ targetCount: selection.selections.length,
410
+ completedCount: Object.keys(state.completed).length,
411
+ checkpointPath: checkpoint,
412
+ outcomes: state.completed,
413
+ },
414
+ featuredReport: report.data,
415
+ },
416
+ };
417
+ }
418
+
419
+ if (!state.review) {
420
+ const result = await call("/reviews", {}, state.requestId);
421
+ const review = result?.data?.review;
422
+ positive(review?.id, "Canonical review ID");
423
+ if (
424
+ result.ok !== true ||
425
+ review.runId !== runId ||
426
+ !Array.isArray(review.subjects) ||
427
+ review.subjects.length > 100 ||
428
+ new Set(review.subjects.map((item) => item.id)).size !==
429
+ review.subjects.length
430
+ ) {
431
+ throw invalid(
432
+ "The API did not return a complete bounded Featured review snapshot.",
433
+ );
434
+ }
435
+ state.review = review;
436
+ for (const subject of review.subjects) {
437
+ positive(subject.id, "Subject ID");
438
+ if (
439
+ !Number.isSafeInteger(subject.snapshotMaxId) ||
440
+ subject.snapshotMaxId < 0
441
+ )
442
+ throw invalid("Invalid comment boundary.");
443
+ state.subjects[subject.id] = {
444
+ ...subject,
445
+ pages: [],
446
+ lastPageId: null,
447
+ lastCommentId: subject.snapshotMaxId + 1,
448
+ commentsFetched: 0,
449
+ complete: false,
450
+ blocked: null,
451
+ };
452
+ }
453
+ save(checkpoint, state);
454
+ }
455
+
456
+ if (workflow === "scan") {
457
+ for (const subject of Object.values(state.subjects)) {
458
+ subject.blocked = null;
459
+ while (!subject.complete) {
460
+ if (subject.pages.length >= 100_000)
461
+ throw invalid("Page safety limit reached; coverage is incomplete.");
462
+ try {
463
+ const result = await call(
464
+ `/reviews/${state.review.id}/pages`,
465
+ {
466
+ subjectId: subject.id,
467
+ previousPageId: subject.lastPageId,
468
+ },
469
+ `cli:featured-page:${runId}:${state.review.id}:${subject.id}:${subject.lastPageId || 0}`,
470
+ );
471
+ const page = assertPageResult(result, state, subject);
472
+ const file = `${checkpoint}.page-${page.id}.json`;
473
+ save(file, result);
474
+ subject.pages.push({
475
+ id: page.id,
476
+ file,
477
+ sha256: hashBytes(readFileSync(file)),
478
+ });
479
+ subject.lastPageId = page.id;
480
+ subject.commentsFetched = page.commentsRead;
481
+ subject.lastCommentId =
482
+ result.data.comments.at(-1)?.id ?? subject.lastCommentId;
483
+ subject.complete = page.exhausted;
484
+ save(checkpoint, state);
485
+ } catch (error) {
486
+ const code = error?.data?.error?.code || error.code;
487
+ if (
488
+ ![
489
+ "CLI_ADMIN_SECRET_REVEAL_REQUIRED",
490
+ "CLI_ADMIN_SUBJECT_NOT_FOUND",
491
+ "CLI_ADMIN_NOT_FOUND",
492
+ ].includes(code)
493
+ ) {
494
+ error.featuredProgress = {
495
+ checkpointPath: checkpoint,
496
+ failedSubjectId: subject.id,
497
+ subjectsCompleted: Object.values(state.subjects).filter(
498
+ (item) => item.complete,
499
+ ).length,
500
+ };
501
+ throw error;
502
+ }
503
+ subject.blocked = { code, message: error.message };
504
+ save(checkpoint, state);
505
+ break;
506
+ }
507
+ }
508
+ }
509
+ } else if (workflow === "acknowledge") {
510
+ if (!options.adminReviewed)
511
+ throw invalid("Read every downloaded page before passing --reviewed.");
512
+ const pageIds = Object.values(state.subjects)
513
+ .filter((item) => item.complete)
514
+ .map((item) => item.lastPageId);
515
+ const result = await call(
516
+ `/reviews/${state.review.id}/coverage`,
517
+ { pageIds, reviewed: true },
518
+ `cli:featured-covered:${runId}:${featuredFingerprint({ reviewId: state.review.id, pageIds }).slice(0, 32)}`,
519
+ );
520
+ if (
521
+ result?.ok !== true ||
522
+ result.data?.coverage?.reviewId !== state.review.id ||
523
+ result.data?.coverage?.reviewed !== true
524
+ ) {
525
+ throw invalid("The API did not confirm reviewed coverage.");
526
+ }
527
+ state.coverage = result.data.coverage;
528
+ save(checkpoint, state);
529
+ } else if (workflow === "report") {
530
+ return call(`/reviews/${state.review.id}/report`);
531
+ }
532
+ const subjects = Object.values(state.subjects);
533
+ return {
534
+ ok: true,
535
+ status: "success",
536
+ changed: false,
537
+ data: {
538
+ review: state.review,
539
+ checkpointPath: checkpoint,
540
+ fetched: {
541
+ complete: subjects.every((item) => item.complete),
542
+ subjectsCompleted: subjects.filter((item) => item.complete).length,
543
+ commentsFetched: subjects.reduce(
544
+ (sum, item) => sum + item.commentsFetched,
545
+ 0,
546
+ ),
547
+ blocked: subjects
548
+ .filter((item) => item.blocked)
549
+ .map((item) => ({ subjectId: item.id, ...item.blocked })),
550
+ },
551
+ // Downloading is not a claim that the agent read the content.
552
+ reviewedCoverage: state.coverage || null,
553
+ pageFiles: subjects.flatMap((item) => item.pages),
554
+ },
555
+ };
556
+ } finally {
557
+ releaseCheckpointLock(lock);
558
+ }
559
+ }