@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 +26 -1
- package/lib/admin-featured.js +559 -0
- package/lib/admin-runtime-logs.js +786 -0
- package/lib/admin-workflows.js +121 -46
- package/lib/admin.js +474 -40
- package/lib/commands.js +52 -8
- package/lib/sponsor-duty.js +479 -28
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +2 -2
- package/sdk/LUMINE_ADMIN.md +515 -83
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
|
|
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
|
+
}
|