@stage5/lumine 0.3.75 → 0.3.77
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 +76 -1
- package/lib/admin-daily.js +5 -1
- package/lib/admin-jev-images.js +105 -0
- package/lib/admin.js +44 -5
- package/lib/commands.js +21 -3
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +11 -9
- package/sdk/LUMINE_ADMIN.md +72 -3
package/lib/admin-arena.js
CHANGED
|
@@ -13,8 +13,13 @@ 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
|
+
// picture-audit [--items <ids> | --cursor <id>] [--limit 1-20] [--renders <dir>]
|
|
17
|
+
// advisory JEV image review list of live picture
|
|
18
|
+
// items; never retires or edits. --renders holds
|
|
19
|
+
// <itemId>.png renders of picture-option items
|
|
20
|
+
// (panels are drawn by the web, not stored)
|
|
16
21
|
export const ARENA_USAGE =
|
|
17
|
-
"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] [--json]";
|
|
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]";
|
|
18
23
|
const MAX_ITEMS = 300;
|
|
19
24
|
|
|
20
25
|
function validationError(message) {
|
|
@@ -62,7 +67,60 @@ function readAdditiveItems(file) {
|
|
|
62
67
|
return items;
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
const PICTURE_AUDIT_MAX = 20;
|
|
71
|
+
const RENDER_MAX_BYTES = 3 * 1024 * 1024;
|
|
72
|
+
// The API's JSON body limit is 8 MB; keep a margin for base64 framing.
|
|
73
|
+
const RENDERS_TOTAL_MAX_BYTES = 5 * 1024 * 1024;
|
|
74
|
+
|
|
75
|
+
function readRenders(dir, itemIds) {
|
|
76
|
+
const folder = path.resolve(dir);
|
|
77
|
+
let names;
|
|
78
|
+
try {
|
|
79
|
+
names = fs.readdirSync(folder).filter((name) => /^\d{1,10}\.(png|jpe?g)$/i.test(name));
|
|
80
|
+
} catch {
|
|
81
|
+
throw validationError(`Cannot read the renders folder ${folder}.`);
|
|
82
|
+
}
|
|
83
|
+
const renders = {};
|
|
84
|
+
let total = 0;
|
|
85
|
+
for (const name of names) {
|
|
86
|
+
const id = name.replace(/\..*$/, "");
|
|
87
|
+
if (itemIds && !itemIds.includes(Number(id))) continue;
|
|
88
|
+
const bytes = fs.readFileSync(path.join(folder, name));
|
|
89
|
+
if (!bytes.length || bytes.length > RENDER_MAX_BYTES) throw validationError(`${name} must be a PNG or JPEG of at most 3 MB.`);
|
|
90
|
+
total += bytes.length;
|
|
91
|
+
if (total > RENDERS_TOTAL_MAX_BYTES) throw validationError("Renders for one call must total under 5 MB; audit fewer --items at a time.");
|
|
92
|
+
renders[id] = bytes.toString("base64");
|
|
93
|
+
}
|
|
94
|
+
if (Object.keys(renders).length > PICTURE_AUDIT_MAX) throw validationError(`Send renders for at most ${PICTURE_AUDIT_MAX} items per call (use --items).`);
|
|
95
|
+
return renders;
|
|
96
|
+
}
|
|
97
|
+
|
|
65
98
|
export function buildArenaOperation({ action, options }) {
|
|
99
|
+
if (action === "picture-audit") {
|
|
100
|
+
const limit = Number(options.adminLimit ?? 10);
|
|
101
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > PICTURE_AUDIT_MAX) throw validationError(`Use --limit from 1 to ${PICTURE_AUDIT_MAX}.`);
|
|
102
|
+
let itemIds = null;
|
|
103
|
+
if (options.adminArenaItems) {
|
|
104
|
+
itemIds = options.adminArenaItems.split(",").map((value) => Number(value.trim()));
|
|
105
|
+
if (!itemIds.length || itemIds.length > PICTURE_AUDIT_MAX || itemIds.some((id) => !Number.isSafeInteger(id) || id <= 0)) {
|
|
106
|
+
throw validationError(`--items is 1-${PICTURE_AUDIT_MAX} comma-separated item ids.`);
|
|
107
|
+
}
|
|
108
|
+
if (options.adminCursor) throw validationError("Use --items or --cursor, not both.");
|
|
109
|
+
}
|
|
110
|
+
const cursor = Number(options.adminCursor || 0);
|
|
111
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0) throw validationError("Use a non-negative --cursor.");
|
|
112
|
+
const body = { limit, ...(itemIds ? { itemIds } : { cursor }) };
|
|
113
|
+
if (options.adminArenaRenders) body.renders = readRenders(options.adminArenaRenders, itemIds);
|
|
114
|
+
return {
|
|
115
|
+
name: "arena.picture-audit",
|
|
116
|
+
method: "POST",
|
|
117
|
+
path: "/cli/admin/arena/picture-audit",
|
|
118
|
+
body,
|
|
119
|
+
mutates: false,
|
|
120
|
+
requiresRun: false,
|
|
121
|
+
timeoutMs: 600000,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
66
124
|
if (action === "paper-queue") {
|
|
67
125
|
const cursor = Number(options.adminCursor || 0);
|
|
68
126
|
const limit = Number(options.adminLimit ?? 50);
|
|
@@ -112,6 +170,13 @@ export function buildArenaOperation({ action, options }) {
|
|
|
112
170
|
}
|
|
113
171
|
if (action === "import") {
|
|
114
172
|
const additive = Boolean(options.adminArenaAdditive);
|
|
173
|
+
if (!options.adminFile && !options.adminDir) {
|
|
174
|
+
throw validationError(
|
|
175
|
+
additive
|
|
176
|
+
? "--additive needs --file <reviewed-items.json> or --dir <folder>; it never reads the current folder."
|
|
177
|
+
: ARENA_USAGE,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
115
180
|
if ((options.adminFile && options.adminDir) || (options.adminFile && !additive)) {
|
|
116
181
|
throw validationError("Use --dir for a complete paper, or --additive with exactly one of --file or --dir for a refill.");
|
|
117
182
|
}
|
|
@@ -167,6 +232,16 @@ export function formatArenaResult({ operation, data }) {
|
|
|
167
232
|
data?.nextCursor ? `Continue: lumine admin arena today-queue --cursor ${data.nextCursor} --json` : "End of Today’s Q queue.",
|
|
168
233
|
];
|
|
169
234
|
if (operation.name === "arena.today.member") return [JSON.stringify(data, null, 2)];
|
|
235
|
+
if (operation.name === "arena.picture-audit") {
|
|
236
|
+
const items = data?.items || [];
|
|
237
|
+
return [
|
|
238
|
+
`Picture audit (advisory; nothing retired or edited): ${items.length} items, ${data?.review?.length || 0} to review, ${data?.needsRender?.length || 0} need a render.`,
|
|
239
|
+
...items.map((row) => `#${row.itemId} ${row.examFamily} ${row.itemType}${row.number ? ` no ${row.number}` : ""} [${row.kind || "-"}]: ${
|
|
240
|
+
row.status === "audited" ? `${row.verdict === "flag" ? "REVIEW" : "ok"}${row.cached ? " (saved)" : ""}${row.notes?.length ? ` · ${row.notes.join("; ")}` : ""}` : `${row.status} (${row.reason})`}`),
|
|
241
|
+
data?.nextCursor ? `Continue: lumine admin arena picture-audit --cursor ${data.nextCursor} --output <file> --json` : "End of live picture items.",
|
|
242
|
+
data?.notice || "",
|
|
243
|
+
].filter(Boolean);
|
|
244
|
+
}
|
|
170
245
|
if (operation.name === "arena.today.preview" || operation.name === "arena.today.import") return [
|
|
171
246
|
`${data?.dryRun ? "Dry run" : "Saved"}: ${data?.questions?.length || 0} reviewed personal questions.${data?.dryRun ? " Add --confirm to make new questions available." : ""}`,
|
|
172
247
|
];
|
package/lib/admin-daily.js
CHANGED
|
@@ -188,7 +188,7 @@ export async function dailyRunGather(options, { run = runAdmin, now, clock } = {
|
|
|
188
188
|
collection.invalidate(steps.at(-1).name);
|
|
189
189
|
Object.assign(steps.at(-1), { ok: false, error });
|
|
190
190
|
};
|
|
191
|
-
const evidenceNames = /^(ai-costs|media-costs|reward-activity|reward-diagnostics|jev-review|owner-wake|bot-output)/;
|
|
191
|
+
const evidenceNames = /^(ai-costs|media-costs|reward-activity|reward-diagnostics|jev-review|jev-images|owner-wake|bot-output)/;
|
|
192
192
|
const step = (name, args, file) => {
|
|
193
193
|
const res = collection.collect(name, args, file, evidenceNames.test(name) ? "evidence" : "priority", run);
|
|
194
194
|
steps.push({
|
|
@@ -283,6 +283,10 @@ export async function dailyRunGather(options, { run = runAdmin, now, clock } = {
|
|
|
283
283
|
since: jevUntil - 86400, until: jevUntil, step, failLast });
|
|
284
284
|
writeAdminJsonFile(path.join(dir, "jev-review-summary.json"), diagnosticCollectionSummary(jevReview), { privateFile: true });
|
|
285
285
|
steps.push({ name: "jev-review-summary", file: "jev-review-summary.json", ok: true });
|
|
286
|
+
// JEV image checks (advisory): per-area health, cost, agreement with final
|
|
287
|
+
// decisions, disagreements and escalations for the report's "JEV images"
|
|
288
|
+
// section. A failed step is unavailable evidence, not a quiet day.
|
|
289
|
+
step("jev-images", ["jev-images", "report", "--days", "1"], "jev-images.json");
|
|
286
290
|
// One pending queue covers every Build approval type. Exhaust its cursor;
|
|
287
291
|
// an empty/malformed receipt must not look like an empty queue.
|
|
288
292
|
const buildReviews = { complete: false, pages: 0, items: 0 };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// `lumine admin jev-images report|switch|decide` (twinkle-api
|
|
2
|
+
// controllers/cli/admin/jevImages.ts): JEV image checks are advisory only.
|
|
3
|
+
// report [--days 1..30] per-area health, cost, agreement,
|
|
4
|
+
// disagreements and escalations
|
|
5
|
+
// switch --area <area|all> --mode on|off [--daily-cap <n>] owner only;
|
|
6
|
+
// takes effect within 30 s, no deploy
|
|
7
|
+
// decide --area <area> --key <subject> --decision <decision> record a
|
|
8
|
+
// reviewer's final decision
|
|
9
|
+
export const JEV_IMAGES_AREAS = ["arena_paper", "arena_picture", "study_record", "thumbnail", "sdk_decide"];
|
|
10
|
+
export const JEV_IMAGES_USAGE =
|
|
11
|
+
"Usage: lumine admin jev-images report [--days 1..30] | switch --area <" +
|
|
12
|
+
["all", ...JEV_IMAGES_AREAS].join("|") +
|
|
13
|
+
"> --mode on|off [--daily-cap <0-5000>] | decide --area <area> --key <subject key, e.g. arenaItem:10176> --decision <decision> [--json]";
|
|
14
|
+
|
|
15
|
+
function invalid(message) {
|
|
16
|
+
const error = new Error(message);
|
|
17
|
+
error.code = "CLI_ADMIN_CLI_VALIDATION";
|
|
18
|
+
return error;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildJevImagesOperation({ action, options = {} }) {
|
|
22
|
+
if (!action || action === "report") {
|
|
23
|
+
const days = options.adminDays === "" || options.adminDays == null ? 1 : Number(options.adminDays);
|
|
24
|
+
if (!Number.isInteger(days) || days < 1 || days > 30) throw invalid("--days must be 1 to 30.");
|
|
25
|
+
return {
|
|
26
|
+
name: "jev-images.report",
|
|
27
|
+
method: "GET",
|
|
28
|
+
path: `/cli/admin/jev-images/report?days=${days}`,
|
|
29
|
+
body: undefined,
|
|
30
|
+
mutates: false,
|
|
31
|
+
requiresRun: false,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (action === "switch") {
|
|
35
|
+
const area = String(options.adminJevArea || "");
|
|
36
|
+
if (area !== "all" && !JEV_IMAGES_AREAS.includes(area)) throw invalid(JEV_IMAGES_USAGE);
|
|
37
|
+
const mode = String(options.adminMode || "");
|
|
38
|
+
if (mode !== "on" && mode !== "off") throw invalid("--mode is on or off.");
|
|
39
|
+
const body = { area, mode };
|
|
40
|
+
if (options.adminJevDailyCap !== "" && options.adminJevDailyCap != null) {
|
|
41
|
+
const cap = Number(options.adminJevDailyCap);
|
|
42
|
+
if (area === "all" || !Number.isInteger(cap) || cap < 0 || cap > 5000) {
|
|
43
|
+
throw invalid("--daily-cap is set per area, 0 to 5000.");
|
|
44
|
+
}
|
|
45
|
+
body.dailyCap = cap;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
name: "jev-images.switch",
|
|
49
|
+
method: "POST",
|
|
50
|
+
path: "/cli/admin/jev-images/switch",
|
|
51
|
+
body,
|
|
52
|
+
mutates: true,
|
|
53
|
+
requiresRun: false,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (action === "decide") {
|
|
57
|
+
const area = String(options.adminJevArea || "");
|
|
58
|
+
if (!JEV_IMAGES_AREAS.includes(area)) throw invalid(JEV_IMAGES_USAGE);
|
|
59
|
+
const subjectKey = String(options.adminJevKey || "");
|
|
60
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]{0,30}:[0-9][A-Za-z0-9:_-]{0,60}$/.test(subjectKey)) {
|
|
61
|
+
throw invalid("--key is a subject key such as arenaItem:10176 or arenaPaper:12.");
|
|
62
|
+
}
|
|
63
|
+
const decision = String(options.adminDecision || "");
|
|
64
|
+
if (!/^[a-z_]{2,24}$/.test(decision)) throw invalid("--decision is required (e.g. keep, fix, retired).");
|
|
65
|
+
return {
|
|
66
|
+
name: "jev-images.decide",
|
|
67
|
+
method: "POST",
|
|
68
|
+
path: "/cli/admin/jev-images/decide",
|
|
69
|
+
body: { area, subjectKey, decision },
|
|
70
|
+
mutates: true,
|
|
71
|
+
requiresRun: false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
throw invalid(JEV_IMAGES_USAGE);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const usd = (value) => `$${Number(value || 0).toFixed(4)}`;
|
|
78
|
+
|
|
79
|
+
export function formatJevImagesResult({ operation, data = {} }) {
|
|
80
|
+
if (operation.name === "jev-images.switch") {
|
|
81
|
+
return [`JEV images ${data.area}: ${data.mode}${data.dailyCap !== undefined ? `, daily cap ${data.dailyCap}` : ""} (all servers within ${data.effectiveWithinSeconds ?? 30} s).`];
|
|
82
|
+
}
|
|
83
|
+
if (operation.name === "jev-images.decide") {
|
|
84
|
+
return [`Recorded ${data.decision} for ${data.subjectKey} (${data.area}) on ${data.updated ?? 0} advisory row(s).`];
|
|
85
|
+
}
|
|
86
|
+
const lines = [
|
|
87
|
+
`JEV images (${data.model || "?"}, advisory only) · last ${data.window?.days ?? "?"} day(s) · coverage ${data.coverage?.status || "unknown"} · ${data.totals?.checks ?? 0} checks · ${usd(data.totals?.costUsd)}`,
|
|
88
|
+
];
|
|
89
|
+
for (const [area, a] of Object.entries(data.areas || {})) {
|
|
90
|
+
const s = a.setting || {};
|
|
91
|
+
lines.push(
|
|
92
|
+
`${area}: ${s.mode} (${s.source}, cap ${s.dailyCap}, used today ${s.usedToday ?? "?"}) · ${a.checks} checks, ${a.ok} ok, ${a.errors} errors, ${a.timeouts} timeouts, ${a.droppedRetries} dropped-image retries · p95 ${a.latencyMs?.p95 ?? "-"} ms · ${a.tokens?.input ?? 0} tokens ${usd(a.costUsd)} · agreement ${a.agreementRate === null ? "-" : `${a.agreed}/${a.scored}`} · ${a.flaggedUndecided} flagged awaiting a decision`,
|
|
93
|
+
);
|
|
94
|
+
for (const d of a.disagreements || []) {
|
|
95
|
+
lines.push(` disagreement #${d.eventId} ${d.subjectKey}: advisory ${d.advisory}, final ${d.finalDecision}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if ((data.escalations || []).length) {
|
|
99
|
+
lines.push("Escalations:");
|
|
100
|
+
for (const e of data.escalations) lines.push(` ${e.area} ${e.code}: ${e.detail}`);
|
|
101
|
+
} else {
|
|
102
|
+
lines.push("Escalations: none.");
|
|
103
|
+
}
|
|
104
|
+
return lines;
|
|
105
|
+
}
|
package/lib/admin.js
CHANGED
|
@@ -50,6 +50,7 @@ import { buildCraftOperation, formatCraftResult } from "./admin-craft.js";
|
|
|
50
50
|
import { buildNonogramOperation, formatNonogramResult } from './admin-nonogram.js';
|
|
51
51
|
import { buildArenaOperation, formatArenaResult } from "./admin-arena.js";
|
|
52
52
|
import { buildJevRoutingReviewOperation, formatJevRoutingReview } from "./admin-jev-review.js";
|
|
53
|
+
import { buildJevImagesOperation, formatJevImagesResult } from "./admin-jev-images.js";
|
|
53
54
|
import { summarizeGameDiagnostics } from "./game-diagnostic-summary.js";
|
|
54
55
|
import {
|
|
55
56
|
buildSlidingBlockOperation,
|
|
@@ -377,8 +378,10 @@ function buildApprovalOperation({ action, target, options }) {
|
|
|
377
378
|
}
|
|
378
379
|
const id = parseApprovalId(target);
|
|
379
380
|
if (action === "show" || action === "get") {
|
|
381
|
+
// An Arena paper's advisory JEV pre-screen can take a while on first view.
|
|
380
382
|
return readOperation("approvals.show", `/cli/admin/approvals/${id}`, {
|
|
381
383
|
requiresRun: false,
|
|
384
|
+
timeoutMs: 180000,
|
|
382
385
|
});
|
|
383
386
|
}
|
|
384
387
|
// An Arena paper's page replaced by the reviewer's blurred copy (names,
|
|
@@ -2714,6 +2717,10 @@ export function parseAdminOperation(options) {
|
|
|
2714
2717
|
if (namespace === "jev-routing") {
|
|
2715
2718
|
return buildJevRoutingReviewOperation({ action, target, extra, options });
|
|
2716
2719
|
}
|
|
2720
|
+
// JEV image checks: report, owner switch, final decisions (admin-jev-images.js).
|
|
2721
|
+
if (namespace === "jev-images") {
|
|
2722
|
+
return buildJevImagesOperation({ action, options });
|
|
2723
|
+
}
|
|
2717
2724
|
// Who holds which management level (admin-roles.js); read-only, no run.
|
|
2718
2725
|
if (namespace === "roles") {
|
|
2719
2726
|
return buildRolesOperation(options);
|
|
@@ -3063,9 +3070,15 @@ export function parseAdminOperation(options) {
|
|
|
3063
3070
|
const raw = readAdminJsonFile(target, "<reviewed-definitions.json>", { maxBytes: 512 * 1024 });
|
|
3064
3071
|
const items = Array.isArray(raw) ? raw : raw?.items;
|
|
3065
3072
|
if (!Array.isArray(items) || !items.length || items.length > 100) throw cliValidationError("Review 1 to 100 Korean definitions, keeping exported word IDs, revisions and source hashes.");
|
|
3066
|
-
|
|
3073
|
+
// A preview (dry run) unless --confirm, like arena import and today-import.
|
|
3074
|
+
if (options.adminConfirm && options.adminConfirm !== "true") {
|
|
3075
|
+
throw cliValidationError("Pass --confirm on its own, without a value.");
|
|
3076
|
+
}
|
|
3077
|
+
const confirm = options.adminConfirm === "true";
|
|
3078
|
+
if (confirm && options.adminDryRun) throw cliValidationError("Choose --confirm or --dry-run, not both.");
|
|
3079
|
+
return writeOperation("dictionary.korean-review", "POST", "/cli/admin/dictionary/korean/review", { items, dryRun: !confirm }, { requiresRun: false, mutates: confirm, timeoutMs: 120000 });
|
|
3067
3080
|
}
|
|
3068
|
-
throw cliValidationError("Use: lumine admin dictionary korean-status | korean-pending [--cursor <time:id>] [--limit <1-100>] | korean-review <reviewed-definitions.json> [--
|
|
3081
|
+
throw cliValidationError("Use: lumine admin dictionary korean-status | korean-pending [--cursor <time:id>] [--limit <1-100>] | korean-review <reviewed-definitions.json> [--confirm]");
|
|
3069
3082
|
}
|
|
3070
3083
|
if (namespace === "word-games") {
|
|
3071
3084
|
if (action === "status") return readOperation("word-games.status", "/cli/admin/word-games/status", { requiresRun: false });
|
|
@@ -3173,7 +3186,7 @@ export function parseAdminOperation(options) {
|
|
|
3173
3186
|
// as the website. Check-ins provide evidence; every meetup completion
|
|
3174
3187
|
// waits for Mikey's decision (2026-10-22).
|
|
3175
3188
|
const usage =
|
|
3176
|
-
"Usage: lumine admin meetup list [--status review|active|completed|all] | show <crewId> | review-completion <crewId> --decision approve|reject --fingerprint <review fingerprint> | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | set-branch <crewId> <userId> --branch <name> [--note <text>] | branches [list|show|approve|reject|merge] | emails [list|send <id>
|
|
3189
|
+
"Usage: lumine admin meetup list [--status review|active|completed|all] | show <crewId> | review-completion <crewId> --decision approve|reject --fingerprint <review fingerprint> | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | set-branch <crewId> <userId> --branch <name> [--note <text>] | branches [list|show|approve|reject|merge] | emails [list|send <id> --fingerprint <revision>|discard <id>|review on|off] | friends [status|on|off] | funnel [--days <1-90>] | story ... | announce-checkins [--send] [--as zero|ciel].";
|
|
3177
3190
|
const note = String(options.note || "").trim();
|
|
3178
3191
|
if (note.length > 1000) {
|
|
3179
3192
|
throw cliValidationError("--note must be at most 1000 characters.");
|
|
@@ -3279,11 +3292,18 @@ export function parseAdminOperation(options) {
|
|
|
3279
3292
|
}
|
|
3280
3293
|
if (sub === "send" || sub === "discard") {
|
|
3281
3294
|
const id = parseRequiredInteger(extra, "Email ID", 1);
|
|
3282
|
-
|
|
3295
|
+
// A send names the exact draft the admin reviewed, so a draft that
|
|
3296
|
+
// changed after review is never sent unseen.
|
|
3297
|
+
const revision = sub === "send" ? String(options.adminFingerprint || "").trim().toLowerCase() : "";
|
|
3298
|
+
if (sub === "send" && !revision) {
|
|
3299
|
+
throw cliValidationError(
|
|
3300
|
+
"Sending a parent email needs --fingerprint <revision>. Run lumine admin meetup emails list, review the draft, then send it with the fingerprint printed beside it.",
|
|
3301
|
+
);
|
|
3302
|
+
}
|
|
3283
3303
|
if (revision && !/^[0-9a-f]{64}$/.test(revision)) throw cliValidationError("Use --fingerprint from the reviewed email in meetup emails list.");
|
|
3284
3304
|
return writeOperation("meetup.emails", "POST", `/cli/admin/meetup-quest/emails/${id}/${sub}`, revision ? { revision } : {}, { requiresRun: false });
|
|
3285
3305
|
}
|
|
3286
|
-
throw cliValidationError("lumine admin meetup emails [list | send <id> | discard <id> | review on|off]");
|
|
3306
|
+
throw cliValidationError("lumine admin meetup emails [list | send <id> --fingerprint <revision> | discard <id> | review on|off]");
|
|
3287
3307
|
}
|
|
3288
3308
|
if (action === "friends") {
|
|
3289
3309
|
const sub = target || "status";
|
|
@@ -6902,6 +6922,20 @@ function arenaItemSummary(item) {
|
|
|
6902
6922
|
return "";
|
|
6903
6923
|
}
|
|
6904
6924
|
|
|
6925
|
+
// JEV image pre-screen notes (advisory only; the decision stays the reviewer's).
|
|
6926
|
+
function printPaperPrescreen(prescreen) {
|
|
6927
|
+
if (!prescreen) return;
|
|
6928
|
+
console.log(`Pre-screen (advisory, JEV images): ${prescreen.status}${prescreen.reason ? ` (${prescreen.reason})` : ""}`);
|
|
6929
|
+
for (const page of prescreen.pages || []) {
|
|
6930
|
+
console.log(page.status === "ok"
|
|
6931
|
+
? ` page ${page.page}: ${page.verdict === "flag" ? "LOOK CLOSELY" : "looks like an exam page"}${page.cached ? " (saved)" : ""} · ${(page.notes || []).join("; ")}`
|
|
6932
|
+
: ` page ${page.page}: no pre-screen (${page.reason})`);
|
|
6933
|
+
}
|
|
6934
|
+
if (prescreen.blankLooking?.length) console.log(` blank-looking pages: ${prescreen.blankLooking.join(", ")}`);
|
|
6935
|
+
for (const pair of prescreen.duplicatePages || []) console.log(` pages ${pair.pages.join(" and ")}: ${pair.kind === "same_file" ? "the same file" : "look nearly identical"}`);
|
|
6936
|
+
if (prescreen.notice) console.log(` ${prescreen.notice}`);
|
|
6937
|
+
}
|
|
6938
|
+
|
|
6905
6939
|
function printArenaEvidence(arena) {
|
|
6906
6940
|
if (!arena) return;
|
|
6907
6941
|
if (arena.kind === "scoreReport") {
|
|
@@ -6917,6 +6951,7 @@ function printArenaEvidence(arena) {
|
|
|
6917
6951
|
console.log(arena.duplicate ? `Duplicate: ${arena.duplicate.reason} of paper #${arena.duplicate.duplicateOf}` : "Duplicate: none found");
|
|
6918
6952
|
console.log(`Rarity: ${arena.rarity || "-"} · reward ${arena.reward?.xp?.toLocaleString?.("en-US") ?? 0} XP + ${arena.reward?.coins ?? 0} coins${arena.reward?.onApprove ? " on approval" : arena.reward?.paid ? " (paid)" : ""}`);
|
|
6919
6953
|
if (arena.reviewNote) console.log(`Review note: ${arena.reviewNote}`);
|
|
6954
|
+
printPaperPrescreen(arena.prescreen);
|
|
6920
6955
|
}
|
|
6921
6956
|
if (arena.check) console.log(`Check: ${arena.check}`);
|
|
6922
6957
|
}
|
|
@@ -7660,6 +7695,10 @@ function printAdminResult({ operation, result }) {
|
|
|
7660
7695
|
for (const line of formatJevRoutingReview(data)) console.log(line);
|
|
7661
7696
|
return;
|
|
7662
7697
|
}
|
|
7698
|
+
if (operation.name.startsWith("jev-images.")) {
|
|
7699
|
+
for (const line of formatJevImagesResult({ operation, data })) console.log(line);
|
|
7700
|
+
return;
|
|
7701
|
+
}
|
|
7663
7702
|
if (operation.name === "roles.read") {
|
|
7664
7703
|
printRoles(data);
|
|
7665
7704
|
return;
|
package/lib/commands.js
CHANGED
|
@@ -2545,6 +2545,17 @@ export function parseArgs(args) {
|
|
|
2545
2545
|
? `${raw.branch},${inlineValue}`
|
|
2546
2546
|
: inlineValue;
|
|
2547
2547
|
} else if (booleanFlags.has(camelKey)) {
|
|
2548
|
+
// A switch never takes a spaced value, so `--live false` would turn the
|
|
2549
|
+
// switch on and leave "false" behind. Refuse it rather than guess.
|
|
2550
|
+
const nextValue = rest[i + 1];
|
|
2551
|
+
if (
|
|
2552
|
+
nextValue !== undefined &&
|
|
2553
|
+
["false", "no", "off", "0"].includes(nextValue.trim().toLowerCase())
|
|
2554
|
+
) {
|
|
2555
|
+
throw new Error(
|
|
2556
|
+
`--${key} is a switch and does not take "${nextValue}" after a space. Leave --${key} out (or write --${key}=false) to turn it off.`,
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2548
2559
|
raw[camelKey] = true;
|
|
2549
2560
|
} else if (
|
|
2550
2561
|
optionalValueFlags.has(camelKey) &&
|
|
@@ -2683,7 +2694,7 @@ export function parseArgs(args) {
|
|
|
2683
2694
|
adminSinceRun: Boolean(raw.sinceRun),
|
|
2684
2695
|
adminSince: raw.since ? String(raw.since) : "",
|
|
2685
2696
|
adminUntil: raw.until ? String(raw.until) : "",
|
|
2686
|
-
adminIncludeSynthetic:
|
|
2697
|
+
adminIncludeSynthetic: parseBoolean(raw.includeSynthetic, false),
|
|
2687
2698
|
adminPath: raw.path ? String(raw.path) : "",
|
|
2688
2699
|
adminPerf: Boolean(raw.perf),
|
|
2689
2700
|
adminWake: Boolean(raw.wake),
|
|
@@ -2708,6 +2719,12 @@ export function parseArgs(args) {
|
|
|
2708
2719
|
adminArenaAdditive: parseBoolean(raw.additive, false),
|
|
2709
2720
|
adminArenaItem: raw.item ? String(raw.item) : "",
|
|
2710
2721
|
adminArenaTo: raw.to ? String(raw.to) : "",
|
|
2722
|
+
adminArenaRenders: raw.renders ? String(raw.renders) : "",
|
|
2723
|
+
adminArenaItems: raw.items ? String(raw.items) : "",
|
|
2724
|
+
// lumine admin jev-images (admin-jev-images.js)
|
|
2725
|
+
adminJevArea: raw.area ? String(raw.area) : "",
|
|
2726
|
+
adminJevKey: raw.key ? String(raw.key) : "",
|
|
2727
|
+
adminJevDailyCap: raw.dailyCap === undefined ? "" : String(raw.dailyCap),
|
|
2711
2728
|
adminClaimFile: raw.claim ? String(raw.claim) : "",
|
|
2712
2729
|
adminScaffoldFile: raw.scaffold ? String(raw.scaffold) : "",
|
|
2713
2730
|
adminTargetFile: raw.targetFile ? String(raw.targetFile) : "",
|
|
@@ -3319,13 +3336,13 @@ export function printHelp() {
|
|
|
3319
3336
|
lumine admin teachers audit [--limit <1-100>] [--cursor <c>] [--all] | revoke <userId> --reason <text> (approved teachers, a page at a time or --all: new flags first, then changed since your review; revoke removes the status cleanly)
|
|
3320
3337
|
lumine admin teachers branches suggest | networks | confirm <networkId> --branch <name> [--branch <name>] [--note <text>] | unconfirm <networkId> | set <userId|username> --branch <name> [--note <text>] | clear <userId|username> | apply | list [--branch <name>|none] (which branch each teacher works at, confirmed by you through the academy networks they use: suggest finds the networks, confirm names their branch (repeat --branch for a building branches share), apply gives teachers the branch most of their actions there point at and lists the unclear ones; set wins over networks)
|
|
3321
3338
|
lumine admin teachers review <userId> --decision legit|revoke --note <text> [--fingerprint <fp>, required for revoke] | review --all-flagged --expect <id[:fp],...> --note <text> (record your review of the facts shown; --all-flagged records only the --expect teachers; nothing is recorded if they changed since. A reviewed teacher comes back when its flags or linked accounts change; accounts sharing a network count only when they change a network flag)
|
|
3322
|
-
lumine admin dictionary korean-status | korean-pending [--cursor <time:id>] [--limit <1-100>] | korean-review <reviewed-definitions.json> [--
|
|
3339
|
+
lumine admin dictionary korean-status | korean-pending [--cursor <time:id>] [--limit <1-100>] | korean-review <reviewed-definitions.json> [--confirm] (shared Korean word-bank definitions; daily review, no paid generation; a preview unless --confirm)
|
|
3323
3340
|
lumine admin english import <verdicts.json> --pass a|b | adjudicate <file> | add-questions <file> | relevel <file> | mastery <userId> --reason <why> | backfill-distractors | summary | review [--limit <n>] | accept|retire <questionId> | retag <questionId> --skill <code> (English skill tags and bank upkeep for Grammarbles; content written by Claude Code agents)
|
|
3324
3341
|
lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] [--json]
|
|
3325
3342
|
lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
|
|
3326
3343
|
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)
|
|
3327
3344
|
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)
|
|
3328
|
-
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 (Arena's mock-exam bank
|
|
3345
|
+
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 is a validation-only dry run without --confirm; upload audio first with twinkle-api scripts/arena-upload-media.ts)
|
|
3329
3346
|
lumine admin nonogram status | add --file <pictures.json> | retire <key> | restore <key> [--confirm] (private reviewed pictures; changes are dry runs without --confirm)
|
|
3330
3347
|
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)
|
|
3331
3348
|
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>]
|
|
@@ -3383,6 +3400,7 @@ export function printHelp() {
|
|
|
3383
3400
|
lumine admin reward-bank report <buildId> [--days 1] [--json] (question-bank telemetry: generation outcomes, learners, payouts, ladder, questions to review)
|
|
3384
3401
|
lumine admin owner-trace [--since 2h|30m|3d|<iso>] [--type route,feed-open,scroll-anchor,content-scroll,error,rejection,session,nav-timing,slow-request,stall,page-load,scroll-perf,scroll-frame] [--perf] [--wake] [--path <substring>] [--limit 300] [--json] (--wake: sessions, 20 s wake profiles, bounded viewport geometry and dropped-event counts after loads or returns; network tags identify the upload network; what the website did on Mikey's own devices: routes with PUSH/POP/REPLACE, Home feed card opens, scroll-anchor restores, content scroll positions, window errors, and performance: tap-to-page timing, API requests over 1 s, main-thread stalls over 200 ms, page load, scroll frame pacing and slow-frame attribution (Long Animation Frame scripts on Chrome, feed card renders on Safari); --perf shows only those; kept 30 days)
|
|
3385
3402
|
lumine admin roles [--min-level 1] [--limit 50] [--json] (every user type with its management level and who holds it; admin pages need level 3, achievement points never pass level 2)
|
|
3403
|
+
lumine admin jev-images report [--days 1..30] | switch --area <all|arena_paper|arena_picture|study_record|thumbnail|sdk_decide> --mode on|off [--daily-cap <0-5000>] | decide --area <area> --key <subject key> --decision <decision> [--json] (JEV image checks: advisory only; per-area health, cost, agreement with final decisions and escalations for the daily run; switch is owner-only and needs no deploy)
|
|
3386
3404
|
lumine admin jev-routing review --since <unix|ISO> --until <unix|ISO> [--limit 1..20] [--cursor <cursor> | --all --checkpoint <file> [--resume]] [--include-synthetic] [--json] (bounded frozen evidence for paired disagreements and Auto fallbacks; collection is not review)
|
|
3387
3405
|
lumine admin comment draft <target> [--type subject|comment|build|aiStory|dailyReflection] [--file <comment.md>] [--review-receipt <review.json>|--reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--identity zero|ciel|auto] [--json]
|
|
3388
3406
|
lumine admin comment reply comment:<id> [--file <reply.md>] [--reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--identity zero|ciel|auto] [--json]
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Build SDK Index
|
|
2
2
|
|
|
3
|
-
Version: 1.
|
|
4
|
-
Updated: 2026-
|
|
5
|
-
Generated: 2026-
|
|
3
|
+
Version: 1.76.0
|
|
4
|
+
Updated: 2026-11-01
|
|
5
|
+
Generated: 2026-11-01T11:22:03.891Z
|
|
6
6
|
|
|
7
7
|
## Platform rules
|
|
8
8
|
Each rule below is a way an app breaks once it runs inside Twinkle's sandboxed frame, even when the code itself is correct. They outrank every other guideline: follow them in every change, including small ones, and fix any break of them you see in code you touch.
|
|
@@ -32,6 +32,7 @@ Features every app needs whenever they apply. When you build a new app or make a
|
|
|
32
32
|
- Reward is part of completion: a reward-paying run must stay pending until awarded:true and its canonical receipt (or getReceipt status awarded). Do not show final solved/completed status, advance rewarded progress, or discard finished proof before that. Preserve the exact challenge/proof and acknowledged progress in a durable per-viewer outbox; reconcile getReceipt after a lost reply and retry the same challenge, never inventing a payment. Explain explicit practice, already collected, exhausted approved limits, preview and disclosed zero-value retry outcomes. Use the start response as a conservative elapsed-time reference, and test delayed start, network failure, lost claim reply, reload, day/viewer change and duplicate recovery.
|
|
33
33
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
34
34
|
- Widgets call SDK methods; the parent proxies to the API.
|
|
35
|
+
- When an SDK request never got a reply from Twinkle's server (the device went offline, the network dropped, or the connection was cut first), its rejected error has error.details.isTransportError === true. Its error.status is 500 for older apps, so check this flag before treating a 500 as a server failure. The request may or may not have been applied: retry a write only with the same operationId or requestId, and only after a later user action or decision point, never in a loop. Errors that came back from the server never carry the flag.
|
|
35
36
|
- Data API methods require scoped tokens handled by the parent; some namespaces include write methods.
|
|
36
37
|
- Use Twinkle.privateDb for LOW-frequency durable private per-user state such as preferences, drafts, settings, inventory checkpoints, and saved progress. It is NOT for high-frequency or per-frame/per-tick writes; the server rate-limits writes and returns 429.
|
|
37
38
|
- Match storage to update frequency: privateDb and sharedDb are for LOW-frequency durable state that changes on a user action. NEVER write per-frame/per-tick state to them (camera or cursor position, animation, live movement, presence, autosave every frame/tick). Keep live state in client memory, broadcast realtime/presence via Twinkle.world, and flush only occasional durable snapshots (on an interval or on exit, never per frame). The server enforces per-key write rate limits and returns 429 on excess; never retry-loop a 429.
|
|
@@ -553,7 +554,7 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
|
|
|
553
554
|
- Template values describe types, not answers to copy. All object keys are required and extra keys are rejected, including inside nested objects; {} means an exactly empty object, not a free-form dictionary. A non-empty array uses only its first item as the template for every returned item, with no fixed length; mixed examples do not declare a union. [] allows arbitrary JSON items. A null template accepts any JSON primitive (string, number, boolean or null), but the key is still required. Use 'number' for fractional numbers; 0 and other integer examples require integers. State content requirements in prompt or instructions. GPT routes retain descriptive string examples as field guidance, not enforced enum values.
|
|
554
555
|
- GPT models prefer native JSON Schema output for compatible shapes. Shapes outside the strict provider subset, such as [] with arbitrary items or deeply nested structures, use prompt-based JSON generation with the same final validation. A provider rejection specifically of the native schema triggers one prompt-based fallback; auth, quota, safety and unrelated request failures do not trigger that fallback. Validation retries preserve the original task and instructions. The returned object/result shape and streaming callbacks are unchanged.
|
|
555
556
|
- mode is accepted as an alias for thinkingMode, and mid is accepted as an alias for medium.
|
|
556
|
-
- Omit model to use the normal Lite/Medium/High routing. Low and Medium use gpt-6-luna; explicit gpt-6-luna accepts Low, Medium or High thinking. Existing apps naming claude-haiku-5-5 now run on gpt-6-luna at the requested thinking level. Explicit claude-sonnet-5-5
|
|
557
|
+
- Omit model to use the normal Lite/Medium/High routing. Low and Medium use gpt-6-luna; explicit gpt-6-luna accepts Low, Medium or High thinking. Existing apps naming claude-haiku-5-5 now run on gpt-6-luna at the requested thinking level. Explicit gpt-6.1-sol requires thinkingMode: 'high' (it reasons at xhigh). Explicit claude-sonnet-5-5 or claude-opus-5-5 think at the requested level (low, medium or high; Opus 5.5's own default is medium). A Claude model's thinking shares one reply with the object, so if a large object fails with code build_ai_output_too_long (cut off at the length limit), offer the player a retry at a lower thinkingMode or a smaller request; a cut-off reply is not charged to the player's AI Energy. Unknown model IDs reject instead of silently falling back. Retired IDs still work and run on their replacement: claude-sonnet-5 runs claude-sonnet-5-5; gpt-6-sol and gpt-5.6-sol run gpt-6.1-sol; gpt-6-astra, claude-opus-5 and claude-fable-5-1 run claude-opus-5-5.
|
|
557
558
|
- thinkingMode low uses GPT-6 Luna with low reasoning and consumes the viewer's AI Energy from confirmed provider usage.
|
|
558
559
|
- thinkingMode medium uses GPT-6 Luna with medium reasoning and consumes normal AI Energy.
|
|
559
560
|
- thinkingMode high without model uses Claude Sonnet 5.5 with high adaptive thinking and consumes high AI Energy. Explicit model: 'gpt-6.1-sol' selects GPT-6.1 Sol with xhigh reasoning at the same High AI Energy tier; model: 'claude-sonnet-5-5' names Sonnet 5.5 directly.
|
|
@@ -1403,17 +1404,18 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
1403
1404
|
- For oldest-first chronological reads, pass order: 'asc' or order: 'oldest'. sort and direction are accepted as aliases.
|
|
1404
1405
|
- Supports cursor-based pagination. Newest-first cursors page toward older entries; oldest-first cursors page toward newer entries.
|
|
1405
1406
|
- async queryEntries(topicName, { limit, pageSize, cursor, order, authorUserId, fields, where, countBy, includeAuthor } = {}) | scopes: sharedDb:read
|
|
1406
|
-
- Returns: { entries?, groups?, cursor, hasMore, queryVersion: 1 }
|
|
1407
|
+
- Returns: { entries?, groups?, cursor, hasMore, queryVersion: 1, truncated? }
|
|
1407
1408
|
- Read selected shared data fields with server-side creator filtering, scalar matching and grouped counts.
|
|
1408
1409
|
- Returns the same paginated entries as getEntries, plus queryVersion: 1.
|
|
1409
1410
|
- authorUserId filters to one entry creator on the server. It does not change read or write permissions.
|
|
1410
1411
|
- fields selects at most 24 top-level data field names; missing selected fields are null. Omit fields for complete data; [] returns only row metadata.
|
|
1411
1412
|
- where: { field, equals } filters one top-level data field by exact JSON scalar equality (string, number, boolean or null).
|
|
1412
1413
|
- Alternatively where: { field, truthy: true } requires a present JSON value other than null, false, zero or an empty string.
|
|
1413
|
-
- countBy groups matching entries by one top-level data field and returns { groups: [{ value, count }], cursor, hasMore, queryVersion: 1 } instead of entries. This avoids transferring documents just to count comments. Groups page in first-entry order; use the returned cursor with the same query. Per-viewer hidden entries are excluded before counting.
|
|
1414
|
+
- countBy groups matching entries by one top-level data field and returns { groups: [{ value, count }], cursor, hasMore, queryVersion: 1 } instead of entries. This avoids transferring documents just to count comments. Groups page in first-entry order; use the returned cursor with the same query. Per-viewer hidden entries are excluded before counting. Counts cover at most the newest 4000 entries of the topic (or of authorUserId's entries in it); when the topic is larger the result has truncated: true, so keep a running tally field instead of recounting a huge topic.
|
|
1414
1415
|
- includeAuthor: false skips username/profile-photo enrichment for count/index reads; id, topicId, userId and timestamps remain available.
|
|
1415
1416
|
- Field names use letters, digits and underscores, starting with a letter or underscore, at most 64 characters. No SQL or nested paths.
|
|
1416
|
-
- Keep the same filters/fields/order when passing a cursor. Reads retain app/topic boundaries, published guest opt-ins and per-viewer content visibility. Signed-in queries read canonical writer state.
|
|
1417
|
+
- Keep the same filters/fields/order when passing a cursor. Reads retain app/topic boundaries, published guest opt-ins and per-viewer content visibility. Signed-in entry queries read canonical writer state; grouped counts may trail a just-saved entry by a moment.
|
|
1418
|
+
- queryEntries calls are limited to 60 a minute per viewer (per IP for guests) and 600 a minute per app; a busy app gets HTTP 429 and should retry later rather than loop.
|
|
1417
1419
|
- Use metadata lists for browsing and getEntriesByIds only when a member opens a full recording/document.
|
|
1418
1420
|
- async loadMoreEntries(topicName, { limit, pageSize, cursor, order, sort, direction } = {}) | scopes: sharedDb:read
|
|
1419
1421
|
- Returns: { entries: [{ id, topicId, userId, username, profilePicUrl, data, createdAt, updatedAt }], cursor?, hasMore }
|
|
@@ -1572,14 +1574,14 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1572
1574
|
- Daily reflections are Daily Question answers shown in the Twinkle feed.
|
|
1573
1575
|
- Only currently shared public reflections are returned. The response field is the stored shared response version, which may be raw or polished depending on what the author explicitly shared.
|
|
1574
1576
|
- submittedAt and sharedAt are Unix timestamps (seconds). Use new Date(value * 1000) to convert to JS Date.
|
|
1575
|
-
- dayType is 'word' (Word Day), 'picture' (Picture Day) or null (an ordinary reflection). On a Picture Day the member described a picture instead of reflecting: pictureUrl is the picture they chose (null when they finished with words only), pictureStyle the style they asked for, and response is their description. Show the picture with the description, not the stock question.
|
|
1577
|
+
- dayType is 'word' (Word Day), 'advice' (Advice Day: a Word Day that asks for a piece of advice; response is the advice, or whatever they chose to write instead), 'picture' (Picture Day) or null (an ordinary reflection). On a Picture Day the member described a picture instead of reflecting: pictureUrl is the picture they chose (null when they finished with words only), pictureStyle the style they asked for, and response is their description. Show the picture with the description, not the stock question.
|
|
1576
1578
|
- async getDailyReflectionsByUser(userId, { cursor, lastId, limit } = {}) | scopes: dailyReflections:read
|
|
1577
1579
|
- Returns: { reflections: [{ id, userId, response, questionId, submittedAt, sharedAt, username, profilePicUrl, question, dayType, pictureUrl, pictureStyle }], cursor? }
|
|
1578
1580
|
- Read one user's currently public Daily Reflection shares. response is the exact text the author chose to share publicly, never an unshared raw/private answer.
|
|
1579
1581
|
- Daily reflections are Daily Question answers shown in the Twinkle feed.
|
|
1580
1582
|
- Only currently shared public reflections are returned. The response field is the stored shared response version, which may be raw or polished depending on what the author explicitly shared.
|
|
1581
1583
|
- submittedAt and sharedAt are Unix timestamps (seconds). Use new Date(value * 1000) to convert to JS Date.
|
|
1582
|
-
- dayType is 'word' (Word Day), 'picture' (Picture Day) or null (an ordinary reflection). On a Picture Day the member described a picture instead of reflecting: pictureUrl is the picture they chose (null when they finished with words only), pictureStyle the style they asked for, and response is their description. Show the picture with the description, not the stock question.
|
|
1584
|
+
- dayType is 'word' (Word Day), 'advice' (Advice Day: a Word Day that asks for a piece of advice; response is the advice, or whatever they chose to write instead), 'picture' (Picture Day) or null (an ordinary reflection). On a Picture Day the member described a picture instead of reflecting: pictureUrl is the picture they chose (null when they finished with words only), pictureStyle the style they asked for, and response is their description. Show the picture with the description, not the stock question.
|
|
1583
1585
|
|
|
1584
1586
|
### Twinkle.privateDb
|
|
1585
1587
|
- async get(key) | scopes: privateDb:read
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -1328,7 +1328,7 @@ Birthdate, teacher-signup (`mentor`) and meetup-achievement requests, each with
|
|
|
1328
1328
|
|
|
1329
1329
|
```bash
|
|
1330
1330
|
lumine admin approvals list [--status pending|approved|rejected|all] [--type dob|mentor|meetup]
|
|
1331
|
-
lumine admin approvals show <id>
|
|
1331
|
+
lumine admin approvals show <id> # an Arena paper also gets advisory JEV pre-screen notes (see JEV images)
|
|
1332
1332
|
lumine admin approvals approve|reject <id> [--reason <text>]
|
|
1333
1333
|
lumine admin teachers audit [--limit 1-100] [--cursor <c>] [--all]
|
|
1334
1334
|
lumine admin teachers revoke <userId> --reason <text>
|
|
@@ -3176,6 +3176,9 @@ unrelated release authority. Keep release evidence and private diagnostics out
|
|
|
3176
3176
|
of the public article. Follow the repository's daily website-code update
|
|
3177
3177
|
procedure and the existing dated-revision contract for an approved correction.
|
|
3178
3178
|
|
|
3179
|
+
**Plain words (Mikey, 2026-10-30):** write the article for 10-15-year-olds with no technical background. Describe what members can now do or what feels better in everyday words; avoid developer terms and internal names (API, SDK, frame rate, memory, cache, deploy, version numbers, counterproposal and the like). The same applies to the newspaper's front-page piece about it. A published update Subject cannot be edited through the CLI, so check the wording before `--confirm`.
|
|
3180
|
+
Model to follow (Mikey, 2026-10-30): the Subject 37274 form in `agent-guides/daily-update-post-template.md`: short headline, one-line greeting from the duty bot, the biggest change first in bold with a link, a short bold-labelled paragraph for each other change, and a closing question.
|
|
3181
|
+
|
|
3179
3182
|
Use the scoped posting command after composing and reviewing the article:
|
|
3180
3183
|
|
|
3181
3184
|
```bash
|
|
@@ -3997,6 +4000,72 @@ input snapshots as described below.
|
|
|
3997
4000
|
See `twinkle-api/JEV_PILOT.md` for configuration,
|
|
3998
4001
|
the synthetic evaluation step and release checks.
|
|
3999
4002
|
|
|
4003
|
+
### JEV images (standing duty, every full daily review; added 2026-11-01)
|
|
4004
|
+
|
|
4005
|
+
```bash
|
|
4006
|
+
lumine admin jev-images report --json # last 24 h; --days 1..30
|
|
4007
|
+
lumine admin jev-images switch --area all --mode off # owner kill switch, no deploy
|
|
4008
|
+
lumine admin jev-images switch --area arena_picture --mode on --daily-cap 100
|
|
4009
|
+
lumine admin jev-images decide --area arena_picture --key arenaItem:10176 --decision keep|fix|retired
|
|
4010
|
+
lumine admin arena picture-audit [--items <id,...> | --cursor <id>] [--limit 1-20] [--renders <dir>] --output <review-list.json> --json
|
|
4011
|
+
```
|
|
4012
|
+
|
|
4013
|
+
JEV image checks (Mikey, November 1) send pictures to JEV (`jev-1.13.0`,
|
|
4014
|
+
pinned) with the `TypeSafe-Danger-Zone-Images` opt-in. The vendor marks images
|
|
4015
|
+
experimental. **Every answer is advisory:** never the only gate, never a
|
|
4016
|
+
child-safety decision and never a reason to pay or withhold a reward (standing
|
|
4017
|
+
ruling 11-01). Areas: `arena_paper` (paper-upload pre-screen notes in
|
|
4018
|
+
`approvals show`), `arena_picture` (picture-bank audit). `study_record`,
|
|
4019
|
+
`thumbnail` and `sdk_decide` are reserved for later steps and stay off.
|
|
4020
|
+
|
|
4021
|
+
Each check carries a small coloured square and asks its colour. A wrong or
|
|
4022
|
+
unsure colour, or an answer from a different model, means the pictures were
|
|
4023
|
+
dropped. The check is retried once (`droppedRetries`), then returns
|
|
4024
|
+
`images_dropped`. An unavailable check (off, capped, timeout, provider error,
|
|
4025
|
+
dropped images, tables missing) leaves the caller exactly as it was before.
|
|
4026
|
+
|
|
4027
|
+
`daily-run gather` saves the report as `jev-images.json`. Give the full report a
|
|
4028
|
+
**JEV images** section:
|
|
4029
|
+
- per area: mode and source, daily cap and today's use, checks, errors,
|
|
4030
|
+
timeouts, dropped-image retries and give-ups, latency p50/p95, tokens and cost;
|
|
4031
|
+
- agreement between advisories and final decisions (`agreed/scored`), every
|
|
4032
|
+
listed disagreement and the flagged items still awaiting a decision;
|
|
4033
|
+
- every `escalations` entry. Thresholds (`thresholds` in the JSON): error rate
|
|
4034
|
+
above 20% or timeouts above 10% (at least 5 sent checks), dropped-image retries
|
|
4035
|
+
above 5% of requests or any give-up, daily cost above $0.10 and 3x the previous
|
|
4036
|
+
7-day average, daily cost above $2, agreement below 80% over at least 10
|
|
4037
|
+
decided items, and any check skipped at the cap.
|
|
4038
|
+
|
|
4039
|
+
`coverage.status: store_unavailable` means the tables are not migrated: report
|
|
4040
|
+
"not deployed", not a quiet day. A failed gather step is unavailable evidence.
|
|
4041
|
+
JEV image spend is also in the AI ledger (`surface: jev_images`), so never add
|
|
4042
|
+
it to the AI cost total a second time. Agreement is not accuracy: a
|
|
4043
|
+
disagreement can be JEV's mistake or the reviewer's, so read the item before
|
|
4044
|
+
saying which.
|
|
4045
|
+
|
|
4046
|
+
**Paper pre-screen.** For a pending `arenaPaper`, `approvals show <id>` adds a
|
|
4047
|
+
`prescreen` block: per page, whether it looks like a test page, legibility, a
|
|
4048
|
+
guess at the exam family and printed year, whether it fits its tags, plus
|
|
4049
|
+
blank-looking and duplicate pages (plain image arithmetic). Results are saved
|
|
4050
|
+
per page, so a second `show` makes no new call (`(saved)`). Add
|
|
4051
|
+
`?prescreen=off` to the API read to skip it. The approval criterion is
|
|
4052
|
+
unchanged (a real exam paper matching its tags, ruling 10-29): read every page
|
|
4053
|
+
yourself. Approve/reject records the final decision for agreement.
|
|
4054
|
+
|
|
4055
|
+
**Picture-bank audit.** `arena picture-audit` reads live items with a picture
|
|
4056
|
+
and asks fixed questions. For 수능 listening #4 it asks which labelled object
|
|
4057
|
+
mismatches the script, whether the keyed object is drawn as briefed, whether
|
|
4058
|
+
the ① to ⑤ labels are present, and whether there is stray text. For picture
|
|
4059
|
+
choices it asks which picture matches the heard sentence and whether every
|
|
4060
|
+
picture is readable. For other single pictures it asks whether the picture is
|
|
4061
|
+
readable and whether there is stray text. Picture choices are drawn by the web,
|
|
4062
|
+
so pass `--renders <dir>` containing `<itemId>.png` renders of the item as
|
|
4063
|
+
printed, for example from an Arena render script. Without one, the item is
|
|
4064
|
+
listed under `needsRender`. The command never retires or edits an item. Read
|
|
4065
|
+
each `review` item yourself. Record `keep`, `fix` or `retired` with
|
|
4066
|
+
`jev-images decide`. Retiring an item through `arena status` records `retired`
|
|
4067
|
+
automatically. Fixing or retiring live items needs the usual authorization.
|
|
4068
|
+
|
|
4000
4069
|
### Spending by calendar month
|
|
4001
4070
|
|
|
4002
4071
|
```bash
|
|
@@ -5568,7 +5637,7 @@ Bridge Builder qualifies from QR check-ins: 7 different people, or one meetup a
|
|
|
5568
5637
|
- The private note and the change history are admin-only; nobody is messaged.
|
|
5569
5638
|
- It refuses members who left and crews that are not active.
|
|
5570
5639
|
- `lumine admin meetup funnel [--days <1-90>]` shows views, crews by stage, and check-ins: pairs, how many were on one network or at an academy, and refusals by reason.
|
|
5571
|
-
- `lumine admin meetup emails` lists parent emails held for review. Kind `witness` is a child asking a parent to be the grown-up.
|
|
5640
|
+
- `lumine admin meetup emails` lists parent emails held for review. Kind `witness` is a child asking a parent to be the grown-up. `lumine admin meetup emails send <id> --fingerprint <revision>` sends one; the fingerprint is the reviewed draft's revision that `emails list` prints, and a send without it is refused. If the draft changed after review, list and review it again. `emails discard <id>` drops one.
|
|
5572
5641
|
- `lumine admin meetup announce-checkins [--send] [--as zero|ciel]` is about the one-time announcement of the check-in rework in each crew chat. It posts itself when the website with check-ins is out. The messages are pre-written; without `--send` this only shows them. `--send` retries a crew whose post failed. Each crew gets it once.
|
|
5573
5642
|
- **Held unlocks:** every physical-meetup qualification gets a `meetup` approval item (`lumine admin approvals list --type meetup`). `approvals show <id> --json` includes `meetupEvidence` for all participants. After Mikey decides, use `approvals approve|reject <id> --fingerprint <meetupEvidence.fingerprint>`. Proximity never bypasses review.
|
|
5574
5643
|
- **Crew completions:** `meetup show <crewId> --json` includes the same owner-only scan packet. After Mikey decides, use `meetup review-completion <crewId> --decision approve|reject --fingerprint <meetupEvidence.fingerprint>`. The decision fails if the shown evidence or proposed attendance changed. New attendees and tier upgrades need a new decision.
|
|
@@ -5621,6 +5690,6 @@ Every full management run includes this dedicated review, requested by Mikey Oct
|
|
|
5621
5690
|
|
|
5622
5691
|
- `lumine admin dictionary korean-status --json`: current generated/reviewed/pending coverage, oldest pending time, outstanding jobs and recent failures.
|
|
5623
5692
|
- `lumine admin dictionary korean-pending --limit 100 [--cursor <time:id>] --json`: full text, English reference meanings, exact IDs/revisions/source hashes. Continue through `nextCursor` until null. Read and assess every entry; collection alone is not review.
|
|
5624
|
-
- `lumine admin dictionary korean-review <reviewed.json> --
|
|
5693
|
+
- `lumine admin dictionary korean-review <reviewed.json> --json` previews (validation only, nothing written); then add `--confirm` to apply the same reviewed payload within authorized content-maintenance scope. Accepts an array or `{ "items": [...] }`, 1–100 entries, each `{ "wordId": 123, "word": "example", "expectedRevision": 1, "expectedSourceHash": "<exported SHA-256>", "koreanDefinition": "<checked Korean meaning>", "reason": "<what was checked or corrected>" }`. Keep exact exported identity/version fields; supply a reason even when accepting unchanged text. All entries commit together with a mutation audit. Conflicts require re-export and re-review, never blind retry with new revisions.
|
|
5625
5694
|
|
|
5626
5695
|
Assess common senses, part of speech and inflections, precision and natural Korean for learners. Use your own language judgment and source checks where needed; no paid batch-generation calls. Corrections update the shared word-bank field for all apps; successful reviews mark it reviewed and advance its revision. Future paid generation never overwrites existing text. The final run report includes reviewed/corrected/remaining counts, oldest waiting age, generation/Battery recovery failures, pagination coverage and carryovers. Re-read status after applying. Missing tables/endpoints mean the release is unavailable, not an empty healthy queue.
|