@stage5/lumine 0.3.84 → 0.3.85
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/lib/admin-arena.js +60 -1
- package/lib/admin-daily.js +3 -2
- package/package.json +1 -1
package/lib/admin-arena.js
CHANGED
|
@@ -13,6 +13,10 @@ import path from "node:path";
|
|
|
13
13
|
// at once; a dry run (validation only) unless --confirm;
|
|
14
14
|
// re-importing a folder retires that paper's earlier items
|
|
15
15
|
// status --item <id> --to live|retired|checked
|
|
16
|
+
// paper-assemble --file <manifest.json> publish a complete paper from existing,
|
|
17
|
+
// never-served IDs, including reviewed text passages
|
|
18
|
+
// and unscored writing; preview unless --confirm;
|
|
19
|
+
// preserves content versions and exposures
|
|
16
20
|
// revise --file <revisions.json> in-place text/passage/visual/review fixes to
|
|
17
21
|
// live items: same item id, a new content version;
|
|
18
22
|
// a dry run unless --confirm; audited
|
|
@@ -25,7 +29,7 @@ import path from "node:path";
|
|
|
25
29
|
// <itemId>.png renders of picture-option items
|
|
26
30
|
// (panels are drawn by the web, not stored)
|
|
27
31
|
export const ARENA_USAGE =
|
|
28
|
-
"Usage: lumine admin arena queue | paper-queue [--cursor <id>] [--limit <1-100>] | marking [--cursor <id>] [--limit <1-200>] | import (--dir <paper folder> | --additive --file <reviewed-items.json>) [--audio audio/<exam>/<paper>] [--live] [--confirm] | status --item <id> --to live|retired|checked [--confirm] | revise --file <revisions.json> [--confirm] | today-queue [--cursor <id>] [--limit <1-100>] | today-member --user <id> | today-import --file <reviewed-questions.json> [--confirm] | today-withdraw --user <id> (--keys <key,...> | --file <withdraw.json>) --reason <text> [--confirm] | picture-audit [--items <id,id,...> | --cursor <id>] [--limit <1-20>] [--renders <dir of <itemId>.png>] [--output <review-list.json>] [--json]";
|
|
32
|
+
"Usage: lumine admin arena queue | paper-queue [--cursor <id>] [--limit <1-100>] | marking [--cursor <id>] [--limit <1-200>] | import (--dir <paper folder> | --additive --file <reviewed-items.json>) [--audio audio/<exam>/<paper>] [--live] [--confirm] | paper-assemble --file <manifest.json> [--confirm] | status --item <id> --to live|retired|checked [--confirm] | revise --file <revisions.json> [--confirm] | today-queue [--cursor <id>] [--limit <1-100>] | today-member --user <id> | today-import --file <reviewed-questions.json> [--confirm] | today-withdraw --user <id> (--keys <key,...> | --file <withdraw.json>) --reason <text> [--confirm] | picture-audit [--items <id,id,...> | --cursor <id>] [--limit <1-20>] [--renders <dir of <itemId>.png>] [--output <review-list.json>] [--json]";
|
|
29
33
|
const MAX_ITEMS = 300;
|
|
30
34
|
|
|
31
35
|
function validationError(message) {
|
|
@@ -154,6 +158,56 @@ function buildTodayWithdraw(options) {
|
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
export function buildArenaOperation({ action, options }) {
|
|
161
|
+
if (action === "paper-assemble") {
|
|
162
|
+
if (!options.adminFile || options.adminDir || options.adminArenaAdditive || options.adminArenaAudio) {
|
|
163
|
+
throw validationError("Use paper-assemble --file <manifest.json> [--confirm]; it uses existing item IDs, never a content import.");
|
|
164
|
+
}
|
|
165
|
+
const file = path.resolve(options.adminFile);
|
|
166
|
+
let parsed;
|
|
167
|
+
try {
|
|
168
|
+
if (fs.statSync(file).size > 200_000) throw validationError("The assembly manifest must be under 200 KB.");
|
|
169
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error.code === "CLI_ADMIN_CLI_VALIDATION") throw error;
|
|
172
|
+
throw validationError(`Cannot read the assembly manifest ${file}: ${error.message}`);
|
|
173
|
+
}
|
|
174
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) ||
|
|
175
|
+
typeof parsed.exam !== "string" || !["easy", "normal", "hard"].includes(parsed.difficulty) ||
|
|
176
|
+
typeof parsed.paper !== "string" || !/^[A-Za-z0-9][A-Za-z0-9-]{0,39}$/.test(parsed.paper) ||
|
|
177
|
+
!Array.isArray(parsed.items) || !parsed.items.length || parsed.items.length > MAX_ITEMS || !parsed.review) {
|
|
178
|
+
throw validationError("The manifest needs exam, difficulty, paper, reviewed existing items and review evidence.");
|
|
179
|
+
}
|
|
180
|
+
for (const item of parsed.items) {
|
|
181
|
+
if (!item || !Number.isSafeInteger(item.itemId) || item.itemId <= 0 ||
|
|
182
|
+
!Number.isSafeInteger(item.expectedVersion) || item.expectedVersion < 1 ||
|
|
183
|
+
!Number.isSafeInteger(item.number) || item.number < 1 ||
|
|
184
|
+
typeof item.contentHash !== "string" || !/^[a-f0-9]{64}$/.test(item.contentHash) ||
|
|
185
|
+
(item.passageId !== undefined && (!Number.isSafeInteger(item.passageId) || item.passageId <= 0)) ||
|
|
186
|
+
Object.keys(item).some(key => !["itemId", "expectedVersion", "number", "contentHash", "passageId"].includes(key))) {
|
|
187
|
+
throw validationError("Each entry needs itemId, expectedVersion, number and contentHash; replacement content is not accepted.");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (new Set(parsed.items.map(i => i.itemId)).size !== parsed.items.length || new Set(parsed.items.map(i => i.number)).size !== parsed.items.length) {
|
|
191
|
+
throw validationError("Item IDs and blueprint numbers must be unique.");
|
|
192
|
+
}
|
|
193
|
+
if (parsed.passages !== undefined && (!Array.isArray(parsed.passages) || parsed.passages.length > MAX_ITEMS ||
|
|
194
|
+
parsed.passages.some(p => !p || !Number.isSafeInteger(p.passageId) || p.passageId <= 0 ||
|
|
195
|
+
!Number.isSafeInteger(p.expectedVersion) || p.expectedVersion <= 0 ||
|
|
196
|
+
typeof p.contentHash !== "string" || !/^[a-f0-9]{64}$/.test(p.contentHash) ||
|
|
197
|
+
Object.keys(p).some(key => !["passageId", "expectedVersion", "contentHash"].includes(key))) ||
|
|
198
|
+
new Set(parsed.passages.map(p => p.passageId)).size !== parsed.passages.length)) {
|
|
199
|
+
throw validationError("Passages need unique existing IDs, expectedVersion and canonical hashes, never replacement text.");
|
|
200
|
+
}
|
|
201
|
+
const referenced = [...new Set(parsed.items.flatMap(i => i.passageId === undefined ? [] : [i.passageId]))].sort((a, b) => a - b);
|
|
202
|
+
const reviewed = (parsed.passages || []).map(p => p.passageId).sort((a, b) => a - b);
|
|
203
|
+
if (JSON.stringify(referenced) !== JSON.stringify(reviewed)) throw validationError("Review every referenced passage exactly once and omit unreferenced passages.");
|
|
204
|
+
const confirm = confirmed(options);
|
|
205
|
+
return { name: confirm ? "arena.paper.assemble" : "arena.paper.assemble.preview", method: "POST",
|
|
206
|
+
path: "/cli/admin/arena/papers/assemble",
|
|
207
|
+
body: { exam: parsed.exam, difficulty: parsed.difficulty, paper: parsed.paper, items: parsed.items,
|
|
208
|
+
...(parsed.passages !== undefined ? { passages: parsed.passages } : {}), review: parsed.review, dryRun: !confirm },
|
|
209
|
+
mutates: confirm, requiresRun: false, timeoutMs: 120000 };
|
|
210
|
+
}
|
|
157
211
|
if (action === "picture-audit") {
|
|
158
212
|
const limit = Number(options.adminLimit ?? 10);
|
|
159
213
|
if (!Number.isInteger(limit) || limit < 1 || limit > PICTURE_AUDIT_MAX) throw validationError(`Use --limit from 1 to ${PICTURE_AUDIT_MAX}.`);
|
|
@@ -307,6 +361,11 @@ export function buildArenaOperation({ action, options }) {
|
|
|
307
361
|
}
|
|
308
362
|
|
|
309
363
|
export function formatArenaResult({ operation, data }) {
|
|
364
|
+
if (operation.name === "arena.paper.assemble" || operation.name === "arena.paper.assemble.preview") {
|
|
365
|
+
if (data?.alreadyAssembled) return [`${data.paper}: already assembled from the same existing items; nothing changed.`];
|
|
366
|
+
return [`${data?.dryRun ? "Dry run: would assemble" : "Assembled"} ${data?.wouldAssemble ?? data?.assembled ?? 0} existing items as ${data?.paper}. IDs, content versions and exposures are preserved.`,
|
|
367
|
+
...(data?.dryRun ? ["Add --confirm to publish this reviewed complete paper."] : [])];
|
|
368
|
+
}
|
|
310
369
|
if (operation.name === "arena.paper.queue") return [
|
|
311
370
|
`Checked ${data?.scannedMembers ?? 0} members; ${data?.entries?.length ?? 0} exam tracks are waiting for fresh papers on this page.`,
|
|
312
371
|
...(data?.entries || []).map(row => `${row.username} (${row.userId}): ${row.examKey} ${row.difficulty}/${row.length}${row.reason === "solo_mastery_paper" ? " — solo mastery ready" : ""}`),
|
package/lib/admin-daily.js
CHANGED
|
@@ -250,7 +250,7 @@ export async function dailyRunGather(options, { run = runAdmin, now, clock } = {
|
|
|
250
250
|
step("brief", ["brief"], "brief.json");
|
|
251
251
|
step("todos", ["todo", "list"], "todos.json");
|
|
252
252
|
step("escalations", ["escalation", "list"], "escalations.json");
|
|
253
|
-
const paperQueue = { complete: false, pages: 0, waitingTracks: 0 };
|
|
253
|
+
const paperQueue = { complete: false, pages: 0, waitingTracks: 0, demandBases: [] };
|
|
254
254
|
let paperCursor = 0;
|
|
255
255
|
for (;;) {
|
|
256
256
|
paperQueue.pages++;
|
|
@@ -260,11 +260,12 @@ export async function dailyRunGather(options, { run = runAdmin, now, clock } = {
|
|
|
260
260
|
if (!res.ok) break;
|
|
261
261
|
const data = res.json?.data;
|
|
262
262
|
const next = data?.nextCursor;
|
|
263
|
-
if (
|
|
263
|
+
if (!["waiting-published-papers-v1", "cleared-exhausted-papers-v2"].includes(data?.demandBasis) || !Array.isArray(data?.entries) ||
|
|
264
264
|
!(next === null || (Number.isSafeInteger(next) && next > paperCursor))) {
|
|
265
265
|
failLast("Invalid or non-advancing Arena paper-waiting page.");
|
|
266
266
|
break;
|
|
267
267
|
}
|
|
268
|
+
if (!paperQueue.demandBases.includes(data.demandBasis)) paperQueue.demandBases.push(data.demandBasis);
|
|
268
269
|
paperQueue.waitingTracks += data.entries.length;
|
|
269
270
|
if (next === null) { paperQueue.complete = true; break; }
|
|
270
271
|
if (paperQueue.pages >= 100) {
|