@stage5/lumine 0.3.80 → 0.3.82
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 +106 -1
- package/lib/commands.js +3 -1
- package/package.json +1 -1
- package/sdk/LUMINE_ADMIN.md +103 -0
package/lib/admin-arena.js
CHANGED
|
@@ -13,13 +13,19 @@ 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
|
+
// revise --file <revisions.json> in-place text/passage/visual/review fixes to
|
|
17
|
+
// live items: same item id, a new content version;
|
|
18
|
+
// a dry run unless --confirm; audited
|
|
19
|
+
// today-withdraw --user <id> (--keys <k,...> | --file <withdraw.json>) --reason <text>
|
|
20
|
+
// withdraw that member's unserved queued Today's
|
|
21
|
+
// Challenge questions (preview unless --confirm; audited)
|
|
16
22
|
// picture-audit [--items <ids> | --cursor <id>] [--limit 1-20] [--renders <dir>]
|
|
17
23
|
// advisory JEV image review list of live picture
|
|
18
24
|
// items; never retires or edits. --renders holds
|
|
19
25
|
// <itemId>.png renders of picture-option items
|
|
20
26
|
// (panels are drawn by the web, not stored)
|
|
21
27
|
export const ARENA_USAGE =
|
|
22
|
-
"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] | today-queue [--cursor <id>] [--limit <1-100>] | today-member --user <id> | today-import --file <reviewed-questions.json> [--confirm] | picture-audit [--items <id,id,...> | --cursor <id>] [--limit <1-20>] [--renders <dir of <itemId>.png>] [--output <review-list.json>] [--json]";
|
|
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]";
|
|
23
29
|
const MAX_ITEMS = 300;
|
|
24
30
|
|
|
25
31
|
function validationError(message) {
|
|
@@ -95,6 +101,58 @@ function readRenders(dir, itemIds) {
|
|
|
95
101
|
return renders;
|
|
96
102
|
}
|
|
97
103
|
|
|
104
|
+
const TODAY_WITHDRAW_USAGE =
|
|
105
|
+
'Use today-withdraw --user <member id> (--keys <key,key,...> | --file <withdraw.json>) --reason "<why>" [--confirm].';
|
|
106
|
+
const TODAY_WITHDRAW_MAX_KEYS = 100;
|
|
107
|
+
|
|
108
|
+
// A withdrawal file is an array of keys, or an object with `keys` (or
|
|
109
|
+
// `authorKeys`, or `items` of {key|authorKey}), an optional `userId` that must
|
|
110
|
+
// match --user, and an optional `reason` used when --reason is absent.
|
|
111
|
+
function readWithdrawFile(file, userId) {
|
|
112
|
+
const resolved = path.resolve(file);
|
|
113
|
+
let parsed;
|
|
114
|
+
try {
|
|
115
|
+
if (fs.statSync(resolved).size > 200_000) throw validationError("The withdrawal file must be under 200 KB.");
|
|
116
|
+
parsed = JSON.parse(fs.readFileSync(resolved, "utf8"));
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error.code === "CLI_ADMIN_CLI_VALIDATION") throw error;
|
|
119
|
+
throw validationError(`Cannot read the withdrawal file ${resolved}: ${error.message}`);
|
|
120
|
+
}
|
|
121
|
+
const object = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
122
|
+
if (object?.userId !== undefined && Number(object.userId) !== userId) {
|
|
123
|
+
throw validationError(`The withdrawal file is for member ${object.userId}, not --user ${userId}.`);
|
|
124
|
+
}
|
|
125
|
+
const list = Array.isArray(parsed) ? parsed : object?.keys || object?.authorKeys || object?.items;
|
|
126
|
+
if (!Array.isArray(list)) throw validationError("The withdrawal file needs a list of question keys (an array, or {keys:[...]}).");
|
|
127
|
+
const keys = list.map((entry) => (typeof entry === "string" ? entry : entry?.authorKey ?? entry?.key));
|
|
128
|
+
return { keys, reason: typeof object?.reason === "string" ? object.reason : "" };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildTodayWithdraw(options) {
|
|
132
|
+
const userId = Number(options.adminUser);
|
|
133
|
+
if (!Number.isSafeInteger(userId) || userId <= 0) throw validationError(TODAY_WITHDRAW_USAGE);
|
|
134
|
+
const keyList = String(options.adminArenaKeys || "");
|
|
135
|
+
if (Boolean(keyList) === Boolean(options.adminFile)) throw validationError(`Pass exactly one of --keys or --file. ${TODAY_WITHDRAW_USAGE}`);
|
|
136
|
+
const fromFile = options.adminFile ? readWithdrawFile(options.adminFile, userId) : null;
|
|
137
|
+
const keys = fromFile ? fromFile.keys : keyList.split(",").map((key) => key.trim()).filter(Boolean);
|
|
138
|
+
if (!keys.length || keys.length > TODAY_WITHDRAW_MAX_KEYS ||
|
|
139
|
+
keys.some((key) => typeof key !== "string" || !/^[a-zA-Z0-9_-]{1,100}$/.test(key))) {
|
|
140
|
+
throw validationError(`Withdraw 1-${TODAY_WITHDRAW_MAX_KEYS} question keys (letters, digits, _ and -).`);
|
|
141
|
+
}
|
|
142
|
+
if (new Set(keys).size !== keys.length) throw validationError("Each question key may appear once.");
|
|
143
|
+
const reason = String(options.adminReason || fromFile?.reason || "").trim();
|
|
144
|
+
if (reason.length < 10 || reason.length > 500) throw validationError(`Give a 10-500 character --reason. ${TODAY_WITHDRAW_USAGE}`);
|
|
145
|
+
const confirm = confirmed(options);
|
|
146
|
+
return {
|
|
147
|
+
name: confirm ? "arena.today.withdraw" : "arena.today.withdraw.preview",
|
|
148
|
+
method: "POST",
|
|
149
|
+
path: "/cli/admin/arena/today/withdraw",
|
|
150
|
+
body: { userId, keys, reason, dryRun: !confirm },
|
|
151
|
+
mutates: confirm,
|
|
152
|
+
requiresRun: false,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
98
156
|
export function buildArenaOperation({ action, options }) {
|
|
99
157
|
if (action === "picture-audit") {
|
|
100
158
|
const limit = Number(options.adminLimit ?? 10);
|
|
@@ -146,6 +204,7 @@ export function buildArenaOperation({ action, options }) {
|
|
|
146
204
|
const confirm = confirmed(options);
|
|
147
205
|
return { name: confirm ? "arena.today.import" : "arena.today.preview", method: "POST", path: "/cli/admin/arena/today/import", body: { ...payload, dryRun: !confirm }, mutates: confirm, requiresRun: false };
|
|
148
206
|
}
|
|
207
|
+
if (action === "today-withdraw") return buildTodayWithdraw(options);
|
|
149
208
|
if (action === "marking") {
|
|
150
209
|
const cursor = Number(options.adminCursor || 0);
|
|
151
210
|
const limit = Number(options.adminLimit ?? 100);
|
|
@@ -200,6 +259,32 @@ export function buildArenaOperation({ action, options }) {
|
|
|
200
259
|
timeoutMs: 120000,
|
|
201
260
|
};
|
|
202
261
|
}
|
|
262
|
+
if (action === "revise") {
|
|
263
|
+
if (!options.adminFile) throw validationError("Use revise --file <revisions.json> [--confirm].");
|
|
264
|
+
const file = path.resolve(options.adminFile);
|
|
265
|
+
let parsed;
|
|
266
|
+
try {
|
|
267
|
+
if (fs.statSync(file).size > 5_000_000) throw validationError("The revision file must be under 5 MB.");
|
|
268
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (error.code === "CLI_ADMIN_CLI_VALIDATION") throw error;
|
|
271
|
+
throw validationError(`Cannot read the revision file ${file}: ${error.message}`);
|
|
272
|
+
}
|
|
273
|
+
const items = Array.isArray(parsed) ? parsed : parsed?.items;
|
|
274
|
+
if (!Array.isArray(items) || !items.length || items.length > MAX_ITEMS) {
|
|
275
|
+
throw validationError(`A revision file holds 1 to ${MAX_ITEMS} revisions: an array, or {reason, items:[...]}.`);
|
|
276
|
+
}
|
|
277
|
+
const confirm = confirmed(options);
|
|
278
|
+
return {
|
|
279
|
+
name: confirm ? "arena.revise" : "arena.revise.preview",
|
|
280
|
+
method: "POST",
|
|
281
|
+
path: "/cli/admin/arena/items/revise",
|
|
282
|
+
body: { items, ...(typeof parsed?.reason === "string" ? { reason: parsed.reason } : {}), dryRun: !confirm },
|
|
283
|
+
mutates: confirm,
|
|
284
|
+
requiresRun: false,
|
|
285
|
+
timeoutMs: 120000,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
203
288
|
if (action === "status") {
|
|
204
289
|
const id = Number(options.adminArenaItem);
|
|
205
290
|
const to = String(options.adminArenaTo || "");
|
|
@@ -245,6 +330,16 @@ export function formatArenaResult({ operation, data }) {
|
|
|
245
330
|
if (operation.name === "arena.today.preview" || operation.name === "arena.today.import") return [
|
|
246
331
|
`${data?.dryRun ? "Dry run" : "Saved"}: ${data?.questions?.length || 0} reviewed personal questions.${data?.dryRun ? " Add --confirm to make new questions available." : ""}`,
|
|
247
332
|
];
|
|
333
|
+
if (operation.name === "arena.today.withdraw.preview" || operation.name === "arena.today.withdraw") {
|
|
334
|
+
const withdrawn = data?.withdrawn || [];
|
|
335
|
+
const skipped = data?.skipped || [];
|
|
336
|
+
return [
|
|
337
|
+
`${data?.dryRun ? "Dry run: would withdraw" : "Withdrew"} ${withdrawn.length} queued question(s) for member ${data?.userId}${withdrawn.length ? `: ${withdrawn.map((q) => q.key).join(", ")}` : ""}.`,
|
|
338
|
+
...skipped.map((s) => `Skipped ${s.key}: ${s.reason} (${s.detail})`),
|
|
339
|
+
`Ready at preferences v${data?.revision ?? "-"}: ${data?.readyBefore ?? 0} before, ${data?.readyAfter ?? 0} after${data?.refillDue ? `; refill to ${data?.target}` : ""}.`,
|
|
340
|
+
...(data?.dryRun && withdrawn.length ? ["Add --confirm to withdraw them."] : []),
|
|
341
|
+
];
|
|
342
|
+
}
|
|
248
343
|
if (operation.name === "arena.marking") {
|
|
249
344
|
const entries = data?.entries || [];
|
|
250
345
|
return [
|
|
@@ -271,6 +366,16 @@ export function formatArenaResult({ operation, data }) {
|
|
|
271
366
|
due.length ? `${due.length} type(s) due for a refill.` : "No standalone practice refill due; check paper-queue for waiting exams.",
|
|
272
367
|
];
|
|
273
368
|
}
|
|
369
|
+
if (operation.name === "arena.revise.preview" || operation.name === "arena.revise") {
|
|
370
|
+
const revised = data?.revised || [];
|
|
371
|
+
const unchanged = data?.unchanged || [];
|
|
372
|
+
return [
|
|
373
|
+
`${data?.dryRun ? "Dry run: would revise" : "Revised"} ${revised.length} item(s)${unchanged.length ? `; ${unchanged.length} already as written` : ""}.`,
|
|
374
|
+
...revised.map((r) => `Item ${r.itemId}${r.paper ? ` (${r.paper} #${r.number})` : r.number ? ` (#${r.number})` : ""}: v${r.fromVersion} -> v${r.toVersion}, ${r.changed.join(", ")}${r.fromPassageVersion !== r.toPassageVersion ? `; passage ${r.passageId} v${r.fromPassageVersion} -> v${r.toPassageVersion}` : ""}`),
|
|
375
|
+
...unchanged.map((r) => `Item ${r.itemId}: ${r.status === "already_applied" ? "already revised" : "no change"} (v${r.version})`),
|
|
376
|
+
...(data?.dryRun && revised.length ? ["Add --confirm to publish these versions."] : []),
|
|
377
|
+
];
|
|
378
|
+
}
|
|
274
379
|
if (operation.name === "arena.import.preview") {
|
|
275
380
|
return [`Dry run: ${data?.wouldImport ?? 0} item(s) pass validation. Add --confirm to import.`];
|
|
276
381
|
}
|
package/lib/commands.js
CHANGED
|
@@ -2726,6 +2726,7 @@ export function parseArgs(args) {
|
|
|
2726
2726
|
adminArenaTo: raw.to ? String(raw.to) : "",
|
|
2727
2727
|
adminArenaRenders: raw.renders ? String(raw.renders) : "",
|
|
2728
2728
|
adminArenaItems: raw.items ? String(raw.items) : "",
|
|
2729
|
+
adminArenaKeys: raw.keys ? String(raw.keys) : "",
|
|
2729
2730
|
// lumine admin jev-images (admin-jev-images.js)
|
|
2730
2731
|
adminJevArea: raw.area ? String(raw.area) : "",
|
|
2731
2732
|
adminJevKey: raw.key ? String(raw.key) : "",
|
|
@@ -3350,7 +3351,8 @@ export function printHelp() {
|
|
|
3350
3351
|
lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
|
|
3351
3352
|
lumine admin craft seed --build <id> --file <pairs.json> [--confirm] | stats --build <id> [--limit <1-100>] | revise --build <id> --item <id|name> [--why <text>] [--emoji <emoji>] [--category <field>] [--confirm] | revise --build <id> --pair <pairId|a+b> --why <text> [--confirm] | retire --build <id> --item <id|name> --reason <text> [--confirm] | unfizzle --build <id> [--since <iso>] [--pair <a+b>] [--confirm] (Twinkle.craft curation: dry run without --confirm)
|
|
3352
3353
|
lumine admin sliding-block status | add --file <levels.json> [--confirm] (sliding-block-v1 campaign refills, RushHour: dry run without --confirm; the server solves each board and appends it)
|
|
3353
|
-
lumine admin arena queue | paper-queue [--cursor <id>] [--limit <1-100>] | import --dir <paper folder> [--audio audio/<exam>/<paper>] [--live] [--confirm] | status --item <id> --to live|retired|checked --confirm | picture-audit [--items <id,id,...> | --cursor <id>] [--limit 1-20] [--renders <dir of <itemId>.png>] [--output <review-list.json>] (Arena's mock-exam bank; picture-audit is an advisory JEV image review list that never retires or edits items; import
|
|
3354
|
+
lumine admin arena queue | paper-queue [--cursor <id>] [--limit <1-100>] | import --dir <paper folder> [--audio audio/<exam>/<paper>] [--live] [--confirm] | status --item <id> --to live|retired|checked --confirm | revise --file <revisions.json> [--confirm] | picture-audit [--items <id,id,...> | --cursor <id>] [--limit 1-20] [--renders <dir of <itemId>.png>] [--output <review-list.json>] (Arena's mock-exam bank; picture-audit is an advisory JEV image review list that never retires or edits items; import and revise are validation-only dry runs without --confirm; revise fixes a live item in place as a new content version; upload audio first with twinkle-api scripts/arena-upload-media.ts)
|
|
3355
|
+
lumine admin arena today-queue [--cursor <id>] | today-member --user <id> | today-import --file <reviewed-questions.json> [--confirm] | today-withdraw --user <id> (--keys <key,...> | --file <withdraw.json>) --reason <text> [--confirm] (Today's Challenge personal queues; import and withdraw are dry runs without --confirm; withdraw removes only unserved queued questions and is audited)
|
|
3354
3356
|
lumine admin nonogram status | add --file <pictures.json> | retire <key> | restore <key> [--confirm] (private reviewed pictures; changes are dry runs without --confirm)
|
|
3355
3357
|
lumine admin build-plans stuck [--days <1-90>] [--all] | recheck <buildId> [<buildId> ...] [--confirm] (Lumine Build plans stuck on one milestone: runs, Energy vs estimate, gradings, loop-guard state; recheck re-grades the current milestone with today's evaluator, free for the creator, dry run without --confirm)
|
|
3356
3358
|
lumine admin storage list [--status pending|approved|rejected|all] [--cursor <id>] | show <user-id-or-username> | approve <request-id> [--size <500MB|1GB|2GB|n MB>] [--reason <text>] | reject <request-id> [--reason <text>] | grant <user-id-or-username> --size <500MB|1GB|2GB|n MB> [--reason <text>]
|
package/package.json
CHANGED
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -5695,6 +5695,109 @@ Bridge Builder qualifies from QR check-ins: 7 different people, or one meetup a
|
|
|
5695
5695
|
Every decision requires Mikey's authorization for that crew and action. Mutations are audited and idempotent; `--output <path>` is an explicit request to retain the canonical JSON. General write receipts omit private crew details.
|
|
5696
5696
|
|
|
5697
5697
|
|
|
5698
|
+
## Arena timed-bank item revisions
|
|
5699
|
+
|
|
5700
|
+
Reviewed text, option and visual fixes to a published paper go live in place, without
|
|
5701
|
+
re-importing the paper:
|
|
5702
|
+
|
|
5703
|
+
```
|
|
5704
|
+
lumine admin arena revise --file <revisions.json> --json
|
|
5705
|
+
lumine admin arena revise --file <revisions.json> --confirm --json
|
|
5706
|
+
```
|
|
5707
|
+
|
|
5708
|
+
The file is `{ "reason": "...", "items": [ { "itemId": <live id>,
|
|
5709
|
+
"expectedVersion": <version you read>, "paper": "<edition>", "number": <n>,
|
|
5710
|
+
"content": { "stem", "body", "choices", "answer", "accepted",
|
|
5711
|
+
"transformation", "review", "visual", "passage" (any subset) }, "reason": "...", "review":
|
|
5712
|
+
"<options-only check + blind solve evidence>" } ] }` (or a bare array). `paper`
|
|
5713
|
+
and `number` are optional guards; a per-item `reason` overrides the file's.
|
|
5714
|
+
Without `--confirm` it is a dry run.
|
|
5715
|
+
|
|
5716
|
+
- The item keeps its id, so exposures, question identity, paper completeness
|
|
5717
|
+
and passed/solo exclusions are unchanged. The old content is archived as its
|
|
5718
|
+
version and the item gets the next version. Finished papers, open sittings,
|
|
5719
|
+
marking, review cards and drills keep the version they froze. A cover or
|
|
5720
|
+
challenge prepared before the revision starts on the new version.
|
|
5721
|
+
- Stem, body, choices, key (`answer`, `accepted`, `transformation`), review,
|
|
5722
|
+
visual and an existing unrecorded text passage can change. Set membership,
|
|
5723
|
+
type, number, mode, rubric and recordings stay fixed. Changing choices, key,
|
|
5724
|
+
passage or visual requires the full rewritten `content.review`. Its explanation
|
|
5725
|
+
is non-empty text; optional `why` notes are an array of strings, one per choice.
|
|
5726
|
+
Option reasoning may instead be included in the full explanation. The same
|
|
5727
|
+
field types apply to a supplied Korean translation.
|
|
5728
|
+
- To change a passage, send `content.passage` and top-level
|
|
5729
|
+
`expectedPassageVersion` for **every live/checked item sharing that passage**,
|
|
5730
|
+
each with its own expected item version and review evidence. All siblings must
|
|
5731
|
+
carry the identical new text. The entire set is validated, and the old passage
|
|
5732
|
+
and item versions are archived in the same transaction. Old sittings and drills
|
|
5733
|
+
keep their original text; new starts use the revised item and passage together.
|
|
5734
|
+
A passage also used by a retired/draft item is refused, because its unchanged
|
|
5735
|
+
version must retain its old text.
|
|
5736
|
+
Use `visual: null` to remove a visual; supplied diagrams/charts must validate.
|
|
5737
|
+
- The result is checked with the import validators together with the rest of
|
|
5738
|
+
its set (choice count, key index, `why` lines, evidence spans). A version
|
|
5739
|
+
other than `expectedVersion`, a retired item, an essay/spoken task or any
|
|
5740
|
+
invalid result refuses the whole file; nothing is written.
|
|
5741
|
+
- Confirmed runs are audited (`arena.items.revise`: operator, time, item ids,
|
|
5742
|
+
reasons). Each item records the revision (versions, reason, evidence,
|
|
5743
|
+
audit id) in its authoring. A retried request replays its receipt;
|
|
5744
|
+
re-running an applied file reports `already applied` and writes nothing.
|
|
5745
|
+
- The new version starts its own calibration from the item's level, as on
|
|
5746
|
+
import; the old version's calibration stays on record.
|
|
5747
|
+
|
|
5748
|
+
### Ordered timed-review explanations
|
|
5749
|
+
|
|
5750
|
+
Authored timed items may include `review.steps`: 1–8 ordered `{text, scene?}`
|
|
5751
|
+
pairs. `text` is 1–1600 characters; the whole sequence is bounded to 40,000
|
|
5752
|
+
characters. Keep `review.explanation` as the full readable fallback. The review
|
|
5753
|
+
screen and Zero/Ciel use these exact step numbers. A translated `review.ko`
|
|
5754
|
+
must provide the same number of aligned steps when explicit steps are used.
|
|
5755
|
+
|
|
5756
|
+
Scenes use the existing `cards`, `bars` or `passage` contract (caption up to
|
|
5757
|
+
300 characters; 1–12 parts with text up to 1600 characters; bars need a finite
|
|
5758
|
+
value from 0 to 1,000,000), or the validated `diagram` contract with a required
|
|
5759
|
+
accessible `alt` (up to 200 characters). No executable markup or figure-plan
|
|
5760
|
+
placeholders. Render and review the actual scenes at phone size before import.
|
|
5761
|
+
Older text-only items retain the sentence-based step behavior.
|
|
5762
|
+
|
|
5763
|
+
## Arena Today's Challenge personal queues
|
|
5764
|
+
|
|
5765
|
+
Each member who chose Today's Challenge preferences is served the oldest
|
|
5766
|
+
`ready`, never-shown question written for their current preference revision.
|
|
5767
|
+
Questions are prepared editorially (see the repository's
|
|
5768
|
+
`agent-guides/lumine-admin.md`, Today's Challenge personal refills, for the
|
|
5769
|
+
content rules, including the 11-03 no-answer-revealing-options and real-test
|
|
5770
|
+
coverage rules).
|
|
5771
|
+
|
|
5772
|
+
```
|
|
5773
|
+
lumine admin arena today-queue [--cursor <id>] [--limit <1-100>] --json
|
|
5774
|
+
lumine admin arena today-member --user <id> --json
|
|
5775
|
+
lumine admin arena today-import --file <reviewed-questions.json> [--confirm] --json
|
|
5776
|
+
lumine admin arena today-withdraw --user <id> (--keys <key,key,...> | --file <withdraw.json>) --reason "<why>" [--confirm] --json
|
|
5777
|
+
```
|
|
5778
|
+
|
|
5779
|
+
`today-import` validates without publishing; `--confirm` publishes. Keys and
|
|
5780
|
+
content are immutable and duplicate questions are rejected.
|
|
5781
|
+
|
|
5782
|
+
`today-withdraw` removes that member's unserved queued questions that failed
|
|
5783
|
+
review, so corrected replacements are not served behind them. Without
|
|
5784
|
+
`--confirm` it is a preview. It withdraws only `ready` questions never assigned
|
|
5785
|
+
to the member; a question shown today or on an earlier day is skipped as
|
|
5786
|
+
`served`, and keys are looked up only in that member's queue (another member's
|
|
5787
|
+
key reads as `not_found`). Already-withdrawn keys are skipped, so a repeat is a
|
|
5788
|
+
no-op, and a retried request replays its original receipt. Each confirmed run is
|
|
5789
|
+
recorded in the administrator audit log (operator, time, member, keys, reason),
|
|
5790
|
+
and each withdrawn question keeps that record in its review evidence. The result
|
|
5791
|
+
lists `withdrawn` and `skipped` keys (`not_found`, `already_withdrawn`, `served`,
|
|
5792
|
+
`not_ready`) with `readyBefore`/`readyAfter` at the current preference revision.
|
|
5793
|
+
Withdrawn questions are never served or counted as ready. Their keys stay
|
|
5794
|
+
reserved and the identical question cannot be imported again; import the
|
|
5795
|
+
corrected version under a new key (it may keep the same passage and prompt).
|
|
5796
|
+
A withdrawal file is a JSON array of keys, or `{ "userId": <id>, "keys": [...],
|
|
5797
|
+
"reason": "..." }` (`userId` must match `--user`; `--reason` overrides the
|
|
5798
|
+
file's reason). Re-read `today-member` afterwards and refill if the member is
|
|
5799
|
+
due.
|
|
5800
|
+
|
|
5698
5801
|
## Nonogram picture collection
|
|
5699
5802
|
|
|
5700
5803
|
Nonogram's `nonogram-picture-v1` proof uses a private, reviewed picture bank.
|