@stage5/lumine 0.3.76 → 0.3.78
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 +69 -1
- package/lib/admin-daily.js +31 -2
- package/lib/admin-jev-images.js +158 -0
- package/lib/admin.js +26 -0
- package/lib/commands.js +14 -1
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +11 -10
- package/sdk/LUMINE_ADMIN.md +104 -1
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);
|
|
@@ -174,6 +232,16 @@ export function formatArenaResult({ operation, data }) {
|
|
|
174
232
|
data?.nextCursor ? `Continue: lumine admin arena today-queue --cursor ${data.nextCursor} --json` : "End of Today’s Q queue.",
|
|
175
233
|
];
|
|
176
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
|
+
}
|
|
177
245
|
if (operation.name === "arena.today.preview" || operation.name === "arena.today.import") return [
|
|
178
246
|
`${data?.dryRun ? "Dry run" : "Saved"}: ${data?.questions?.length || 0} reviewed personal questions.${data?.dryRun ? " Add --confirm to make new questions available." : ""}`,
|
|
179
247
|
];
|
package/lib/admin-daily.js
CHANGED
|
@@ -10,6 +10,7 @@ import { createDailyReviewState, dailyRunProgress } from "./admin-duties.js";
|
|
|
10
10
|
import { summarizeOwnerViewport, formatOwnerViewportReport } from "./admin-owner-viewport.js";
|
|
11
11
|
import { createDailyCollection } from "./admin-daily-collection.js";
|
|
12
12
|
import { collectDiagnosticPages, diagnosticCollectionSummary } from "./admin-diagnostic-collection.js";
|
|
13
|
+
import { JEV_THUMBNAIL_MAX_TARGETS } from "./admin-jev-images.js";
|
|
13
14
|
export { dailyRunProgress };
|
|
14
15
|
|
|
15
16
|
// Multi-step conveniences for the full daily management run. Each one runs
|
|
@@ -188,7 +189,7 @@ export async function dailyRunGather(options, { run = runAdmin, now, clock } = {
|
|
|
188
189
|
collection.invalidate(steps.at(-1).name);
|
|
189
190
|
Object.assign(steps.at(-1), { ok: false, error });
|
|
190
191
|
};
|
|
191
|
-
const evidenceNames = /^(ai-costs|media-costs|reward-activity|reward-diagnostics|jev-review|owner-wake|bot-output)/;
|
|
192
|
+
const evidenceNames = /^(ai-costs|media-costs|reward-activity|reward-diagnostics|jev-review|jev-images|owner-wake|bot-output)/;
|
|
192
193
|
const step = (name, args, file) => {
|
|
193
194
|
const res = collection.collect(name, args, file, evidenceNames.test(name) ? "evidence" : "priority", run);
|
|
194
195
|
steps.push({
|
|
@@ -283,6 +284,10 @@ export async function dailyRunGather(options, { run = runAdmin, now, clock } = {
|
|
|
283
284
|
since: jevUntil - 86400, until: jevUntil, step, failLast });
|
|
284
285
|
writeAdminJsonFile(path.join(dir, "jev-review-summary.json"), diagnosticCollectionSummary(jevReview), { privateFile: true });
|
|
285
286
|
steps.push({ name: "jev-review-summary", file: "jev-review-summary.json", ok: true });
|
|
287
|
+
// JEV image checks (advisory): per-area health, cost, agreement with final
|
|
288
|
+
// decisions, disagreements and escalations for the report's "JEV images"
|
|
289
|
+
// section. A failed step is unavailable evidence, not a quiet day.
|
|
290
|
+
step("jev-images", ["jev-images", "report", "--days", "1"], "jev-images.json");
|
|
286
291
|
// One pending queue covers every Build approval type. Exhaust its cursor;
|
|
287
292
|
// an empty/malformed receipt must not look like an empty queue.
|
|
288
293
|
const buildReviews = { complete: false, pages: 0, items: 0 };
|
|
@@ -459,6 +464,25 @@ export async function dailyRunGather(options, { run = runAdmin, now, clock } = {
|
|
|
459
464
|
|
|
460
465
|
// Recent, provably never-Featured Subjects with the context the editorial
|
|
461
466
|
// ranking needs (comment count, operator view state), newest first.
|
|
467
|
+
export function featuredThumbnailChecks(eligible, run = runAdmin) {
|
|
468
|
+
const ids = eligible.slice(0, JEV_THUMBNAIL_MAX_TARGETS).map((c) => `subject:${c.id}`);
|
|
469
|
+
if (!ids.length) return { summary: { status: "none" } };
|
|
470
|
+
const res = run(["jev-images", "thumbnails", "--targets", ids.join(",")], { timeoutMs: 200000 });
|
|
471
|
+
const data = res.ok ? res.json?.data : null;
|
|
472
|
+
if (!data || !Array.isArray(data.items)) {
|
|
473
|
+
for (const c of eligible) c.thumbnailCheck = { status: "unavailable", reason: "request_failed" };
|
|
474
|
+
return { summary: { status: "unavailable", reason: "request_failed", advisory: true } };
|
|
475
|
+
}
|
|
476
|
+
const byTarget = new Map(data.items.map((item) => [item.target, item]));
|
|
477
|
+
for (const c of eligible) {
|
|
478
|
+
const item = byTarget.get(`subject:${c.id}`);
|
|
479
|
+
c.thumbnailCheck = item
|
|
480
|
+
? { status: item.status, ...(item.kind ? { kind: item.kind } : {}), ...(item.status === "ok" ? { verdict: item.verdict, notes: item.notes || [], cached: Boolean(item.cached) } : { reason: item.reason }) }
|
|
481
|
+
: { status: "unavailable", reason: "not_checked" };
|
|
482
|
+
}
|
|
483
|
+
return { summary: { status: data.status, flagged: data.flagged || [], advisory: true, notice: data.notice } };
|
|
484
|
+
}
|
|
485
|
+
|
|
462
486
|
export async function featuredCandidates(options) {
|
|
463
487
|
const days = Math.max(1, Math.min(30, Number(options.adminDays || 7)));
|
|
464
488
|
const after = options.adminAfter || bangkokCutoff(days);
|
|
@@ -524,6 +548,10 @@ export async function featuredCandidates(options) {
|
|
|
524
548
|
operatorViewed: Boolean(s.operatorViewed?.viewed),
|
|
525
549
|
});
|
|
526
550
|
}
|
|
551
|
+
// Advisory JEV thumbnail notes (never a ranking or eligibility input). A
|
|
552
|
+
// failed or switched-off check leaves the candidate list exactly as before.
|
|
553
|
+
const eligible = candidates.filter((c) => c.neverFeatured === true);
|
|
554
|
+
const thumbnailChecks = featuredThumbnailChecks(eligible);
|
|
527
555
|
return {
|
|
528
556
|
ok: true,
|
|
529
557
|
status: "success",
|
|
@@ -531,7 +559,8 @@ export async function featuredCandidates(options) {
|
|
|
531
559
|
postedAfter: after,
|
|
532
560
|
coverage: history.json.data.coverage || null,
|
|
533
561
|
boardCount: onBoard.size,
|
|
534
|
-
eligible
|
|
562
|
+
eligible,
|
|
563
|
+
thumbnailChecks: thumbnailChecks.summary,
|
|
535
564
|
excluded: candidates
|
|
536
565
|
.filter((c) => c.neverFeatured !== true)
|
|
537
566
|
.map((c) => ({ id: c.id, title: c.title, neverFeatured: c.neverFeatured })),
|
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
// thumbnails --targets subject:<id>,build:<id>,... | --featured [--saved-only]
|
|
10
|
+
// advisory thumbnail notes for
|
|
11
|
+
// Featured review (area thumbnail)
|
|
12
|
+
export const JEV_IMAGES_AREAS = ["arena_paper", "arena_picture", "study_record", "thumbnail", "sdk_decide"];
|
|
13
|
+
export const JEV_IMAGES_USAGE =
|
|
14
|
+
"Usage: lumine admin jev-images report [--days 1..30] | switch --area <" +
|
|
15
|
+
["all", ...JEV_IMAGES_AREAS].join("|") +
|
|
16
|
+
"> --mode on|off [--daily-cap <0-5000>] [--build <id>, sdk_decide only, no --daily-cap] | decide --area <area> --key <subject key, e.g. arenaItem:10176> --decision <decision> | thumbnails --targets <subject:<id>,build:<id>,...> | --featured [--saved-only] [--json]";
|
|
17
|
+
export const JEV_THUMBNAIL_MAX_TARGETS = 40;
|
|
18
|
+
|
|
19
|
+
function invalid(message) {
|
|
20
|
+
const error = new Error(message);
|
|
21
|
+
error.code = "CLI_ADMIN_CLI_VALIDATION";
|
|
22
|
+
return error;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildJevImagesOperation({ action, options = {} }) {
|
|
26
|
+
if (!action || action === "report") {
|
|
27
|
+
const days = options.adminDays === "" || options.adminDays == null ? 1 : Number(options.adminDays);
|
|
28
|
+
if (!Number.isInteger(days) || days < 1 || days > 30) throw invalid("--days must be 1 to 30.");
|
|
29
|
+
return {
|
|
30
|
+
name: "jev-images.report",
|
|
31
|
+
method: "GET",
|
|
32
|
+
path: `/cli/admin/jev-images/report?days=${days}`,
|
|
33
|
+
body: undefined,
|
|
34
|
+
mutates: false,
|
|
35
|
+
requiresRun: false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (action === "switch") {
|
|
39
|
+
const area = String(options.adminJevArea || "");
|
|
40
|
+
if (area !== "all" && !JEV_IMAGES_AREAS.includes(area)) throw invalid(JEV_IMAGES_USAGE);
|
|
41
|
+
const mode = String(options.adminMode || "");
|
|
42
|
+
if (mode !== "on" && mode !== "off") throw invalid("--mode is on or off.");
|
|
43
|
+
const body = { area, mode };
|
|
44
|
+
if (options.adminJevDailyCap !== "" && options.adminJevDailyCap != null) {
|
|
45
|
+
const cap = Number(options.adminJevDailyCap);
|
|
46
|
+
if (area === "all" || !Number.isInteger(cap) || cap < 0 || cap > 5000) {
|
|
47
|
+
throw invalid("--daily-cap is set per area, 0 to 5000.");
|
|
48
|
+
}
|
|
49
|
+
body.dailyCap = cap;
|
|
50
|
+
}
|
|
51
|
+
// One app's switch for sdk_decide (Twinkle.ai.decide with images). An app
|
|
52
|
+
// has no daily cap of its own; bursts are limited by the server.
|
|
53
|
+
if (options.adminBuild !== "" && options.adminBuild != null) {
|
|
54
|
+
const buildId = Number(options.adminBuild);
|
|
55
|
+
if (area !== "sdk_decide" || !Number.isSafeInteger(buildId) || buildId <= 0) {
|
|
56
|
+
throw invalid("--build is set only with --area sdk_decide and a build id.");
|
|
57
|
+
}
|
|
58
|
+
if (body.dailyCap !== undefined) throw invalid("--build sets one app's switch only (--mode on|off); an app has no daily cap of its own.");
|
|
59
|
+
body.buildId = buildId;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
name: "jev-images.switch",
|
|
63
|
+
method: "POST",
|
|
64
|
+
path: "/cli/admin/jev-images/switch",
|
|
65
|
+
body,
|
|
66
|
+
mutates: true,
|
|
67
|
+
requiresRun: false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (action === "decide") {
|
|
71
|
+
const area = String(options.adminJevArea || "");
|
|
72
|
+
if (!JEV_IMAGES_AREAS.includes(area)) throw invalid(JEV_IMAGES_USAGE);
|
|
73
|
+
const subjectKey = String(options.adminJevKey || "");
|
|
74
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]{0,30}:[0-9][A-Za-z0-9:_-]{0,60}$/.test(subjectKey)) {
|
|
75
|
+
throw invalid("--key is a subject key such as arenaItem:10176 or arenaPaper:12.");
|
|
76
|
+
}
|
|
77
|
+
const decision = String(options.adminDecision || "");
|
|
78
|
+
if (!/^[a-z_]{2,24}$/.test(decision)) throw invalid("--decision is required (e.g. keep, fix, retired).");
|
|
79
|
+
return {
|
|
80
|
+
name: "jev-images.decide",
|
|
81
|
+
method: "POST",
|
|
82
|
+
path: "/cli/admin/jev-images/decide",
|
|
83
|
+
body: { area, subjectKey, decision },
|
|
84
|
+
mutates: true,
|
|
85
|
+
requiresRun: false,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (action === "thumbnails") {
|
|
89
|
+
const featured = options.adminFeatured === true;
|
|
90
|
+
const raw = String(options.adminTargets || "").trim();
|
|
91
|
+
if (featured === Boolean(raw)) throw invalid("Use --targets subject:<id>,build:<id>,... or --featured (the current board), not both.");
|
|
92
|
+
const body = featured ? { featured: true } : { targets: parseThumbnailTargets(raw) };
|
|
93
|
+
// --saved-only: earlier answers only, no provider call.
|
|
94
|
+
if (options.adminSavedOnly === true) body.run = false;
|
|
95
|
+
return {
|
|
96
|
+
name: "jev-images.thumbnails",
|
|
97
|
+
method: "POST",
|
|
98
|
+
path: "/cli/admin/jev-images/thumbnails",
|
|
99
|
+
body,
|
|
100
|
+
mutates: false,
|
|
101
|
+
requiresRun: false,
|
|
102
|
+
timeoutMs: 180000,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
throw invalid(JEV_IMAGES_USAGE);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function parseThumbnailTargets(raw) {
|
|
109
|
+
const targets = [...new Set(String(raw).split(",").map((value) => value.trim()).filter(Boolean))];
|
|
110
|
+
if (!targets.length || targets.length > JEV_THUMBNAIL_MAX_TARGETS || targets.some((value) => !/^(subject|build):[1-9][0-9]{0,14}$/.test(value))) {
|
|
111
|
+
throw invalid(`--targets is 1-${JEV_THUMBNAIL_MAX_TARGETS} of subject:<id> or build:<id>, comma-separated.`);
|
|
112
|
+
}
|
|
113
|
+
return targets;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function thumbnailLine(item) {
|
|
117
|
+
if (!item) return "no thumbnail check";
|
|
118
|
+
if (item.status !== "ok") return `no thumbnail check (${item.reason || "unavailable"})`;
|
|
119
|
+
return `${item.verdict === "flag" ? "LOOK AT THE THUMBNAIL" : "thumbnail looks fine"}${item.cached ? " (saved)" : ""}${item.notes?.length ? ` · ${item.notes.join("; ")}` : ""}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const usd = (value) => `$${Number(value || 0).toFixed(4)}`;
|
|
123
|
+
|
|
124
|
+
export function formatJevImagesResult({ operation, data = {} }) {
|
|
125
|
+
if (operation.name === "jev-images.switch") {
|
|
126
|
+
return [`JEV images ${data.area}${data.buildId ? ` for build ${data.buildId}` : ""}: ${data.mode}${data.dailyCap !== undefined ? `, daily cap ${data.dailyCap}` : ""} (all servers within ${data.effectiveWithinSeconds ?? 30} s).`];
|
|
127
|
+
}
|
|
128
|
+
if (operation.name === "jev-images.decide") {
|
|
129
|
+
return [`Recorded ${data.decision} for ${data.subjectKey} (${data.area}) on ${data.updated ?? 0} advisory row(s).`];
|
|
130
|
+
}
|
|
131
|
+
if (operation.name === "jev-images.thumbnails") {
|
|
132
|
+
const lines = [`Thumbnail notes (advisory, JEV images): ${data.status || "unavailable"}${data.featuredBoard ? " · current Featured board" : ""} · ${(data.flagged || []).length} to look at`];
|
|
133
|
+
for (const item of data.items || []) {
|
|
134
|
+
lines.push(` ${item.target}${item.kind ? ` (${item.kind})` : ""} ${JSON.stringify(item.title || "")}: ${thumbnailLine(item)}`);
|
|
135
|
+
}
|
|
136
|
+
if (data.notice) lines.push(` ${data.notice}`);
|
|
137
|
+
return lines;
|
|
138
|
+
}
|
|
139
|
+
const lines = [
|
|
140
|
+
`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)}`,
|
|
141
|
+
];
|
|
142
|
+
for (const [area, a] of Object.entries(data.areas || {})) {
|
|
143
|
+
const s = a.setting || {};
|
|
144
|
+
lines.push(
|
|
145
|
+
`${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`,
|
|
146
|
+
);
|
|
147
|
+
for (const d of a.disagreements || []) {
|
|
148
|
+
lines.push(` disagreement #${d.eventId} ${d.subjectKey}: advisory ${d.advisory}, final ${d.finalDecision}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if ((data.escalations || []).length) {
|
|
152
|
+
lines.push("Escalations:");
|
|
153
|
+
for (const e of data.escalations) lines.push(` ${e.area} ${e.code}: ${e.detail}`);
|
|
154
|
+
} else {
|
|
155
|
+
lines.push("Escalations: none.");
|
|
156
|
+
}
|
|
157
|
+
return lines;
|
|
158
|
+
}
|
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);
|
|
@@ -6915,6 +6922,20 @@ function arenaItemSummary(item) {
|
|
|
6915
6922
|
return "";
|
|
6916
6923
|
}
|
|
6917
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
|
+
|
|
6918
6939
|
function printArenaEvidence(arena) {
|
|
6919
6940
|
if (!arena) return;
|
|
6920
6941
|
if (arena.kind === "scoreReport") {
|
|
@@ -6930,6 +6951,7 @@ function printArenaEvidence(arena) {
|
|
|
6930
6951
|
console.log(arena.duplicate ? `Duplicate: ${arena.duplicate.reason} of paper #${arena.duplicate.duplicateOf}` : "Duplicate: none found");
|
|
6931
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)" : ""}`);
|
|
6932
6953
|
if (arena.reviewNote) console.log(`Review note: ${arena.reviewNote}`);
|
|
6954
|
+
printPaperPrescreen(arena.prescreen);
|
|
6933
6955
|
}
|
|
6934
6956
|
if (arena.check) console.log(`Check: ${arena.check}`);
|
|
6935
6957
|
}
|
|
@@ -7673,6 +7695,10 @@ function printAdminResult({ operation, result }) {
|
|
|
7673
7695
|
for (const line of formatJevRoutingReview(data)) console.log(line);
|
|
7674
7696
|
return;
|
|
7675
7697
|
}
|
|
7698
|
+
if (operation.name.startsWith("jev-images.")) {
|
|
7699
|
+
for (const line of formatJevImagesResult({ operation, data })) console.log(line);
|
|
7700
|
+
return;
|
|
7701
|
+
}
|
|
7676
7702
|
if (operation.name === "roles.read") {
|
|
7677
7703
|
printRoles(data);
|
|
7678
7704
|
return;
|
package/lib/commands.js
CHANGED
|
@@ -2472,6 +2472,9 @@ export function parseArgs(args) {
|
|
|
2472
2472
|
const raw = {};
|
|
2473
2473
|
const positional = [];
|
|
2474
2474
|
const booleanFlags = new Set([
|
|
2475
|
+
// lumine admin jev-images thumbnails --featured [--saved-only]
|
|
2476
|
+
"featured",
|
|
2477
|
+
"savedOnly",
|
|
2475
2478
|
// lumine admin arena import --live
|
|
2476
2479
|
"live",
|
|
2477
2480
|
"additive",
|
|
@@ -2719,6 +2722,15 @@ export function parseArgs(args) {
|
|
|
2719
2722
|
adminArenaAdditive: parseBoolean(raw.additive, false),
|
|
2720
2723
|
adminArenaItem: raw.item ? String(raw.item) : "",
|
|
2721
2724
|
adminArenaTo: raw.to ? String(raw.to) : "",
|
|
2725
|
+
adminArenaRenders: raw.renders ? String(raw.renders) : "",
|
|
2726
|
+
adminArenaItems: raw.items ? String(raw.items) : "",
|
|
2727
|
+
// lumine admin jev-images (admin-jev-images.js)
|
|
2728
|
+
adminJevArea: raw.area ? String(raw.area) : "",
|
|
2729
|
+
adminJevKey: raw.key ? String(raw.key) : "",
|
|
2730
|
+
adminJevDailyCap: raw.dailyCap === undefined ? "" : String(raw.dailyCap),
|
|
2731
|
+
adminTargets: raw.targets ? String(raw.targets) : "",
|
|
2732
|
+
adminFeatured: parseBoolean(raw.featured, false),
|
|
2733
|
+
adminSavedOnly: parseBoolean(raw.savedOnly, false),
|
|
2722
2734
|
adminClaimFile: raw.claim ? String(raw.claim) : "",
|
|
2723
2735
|
adminScaffoldFile: raw.scaffold ? String(raw.scaffold) : "",
|
|
2724
2736
|
adminTargetFile: raw.targetFile ? String(raw.targetFile) : "",
|
|
@@ -3336,7 +3348,7 @@ export function printHelp() {
|
|
|
3336
3348
|
lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
|
|
3337
3349
|
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)
|
|
3338
3350
|
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)
|
|
3339
|
-
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
|
|
3351
|
+
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)
|
|
3340
3352
|
lumine admin nonogram status | add --file <pictures.json> | retire <key> | restore <key> [--confirm] (private reviewed pictures; changes are dry runs without --confirm)
|
|
3341
3353
|
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)
|
|
3342
3354
|
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>]
|
|
@@ -3394,6 +3406,7 @@ export function printHelp() {
|
|
|
3394
3406
|
lumine admin reward-bank report <buildId> [--days 1] [--json] (question-bank telemetry: generation outcomes, learners, payouts, ladder, questions to review)
|
|
3395
3407
|
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)
|
|
3396
3408
|
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)
|
|
3409
|
+
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>] [--build <id>, sdk_decide only, no --daily-cap] | decide --area <area> --key <subject key> --decision <decision> | thumbnails --targets <subject:<id>,build:<id>,...> | --featured [--saved-only] [--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; thumbnails adds notes for Featured review and never replaces or unfeatures anything)
|
|
3397
3410
|
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)
|
|
3398
3411
|
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]
|
|
3399
3412
|
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.77.0
|
|
4
|
+
Updated: 2026-11-01
|
|
5
|
+
Generated: 2026-11-01T12:15:43.879Z
|
|
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.
|
|
@@ -71,7 +71,7 @@ Features every app needs whenever they apply. When you build a new app or make a
|
|
|
71
71
|
|
|
72
72
|
## AI decision design
|
|
73
73
|
- Twinkle.ai.decide runs JEV, a model for narrow decisions over supplied text or structured state, and it is free for viewers: decisions are covered by Twinkle and never use their AI Energy. Whenever an app makes a judgment that ordinary code can only approximate, consider JEV first. Proven patterns: an opponent or boss coach that reads the player's habits (favourite lane, repeated trick, cheese move) and picks a counter-plan from a fixed list, as in Red Square's boss and Open Football's opponent coach; NPCs that understand what a player types and choose an action; classifying or scoring user submissions; ranking supplied candidates; and picking the next exercise or difficulty from evidence about the learner. Any app with an AI opponent, NPC, or adaptive difficulty should decide explicitly whether JEV improves it. These are examples to reason from, not a keyword checklist or a requirement to add AI to every app.
|
|
74
|
-
- Choose the mechanism by the work: use ordinary code for known rules, arithmetic, physics, legal moves, storage, and permissions; use ai.decide for contextual choices, yes/no probabilities, and scores; use ai.chat or ai.generateObject for generated dialogue, explanations, open-ended content, and multi-step reasoning. JEV does not generate text, browse the web,
|
|
74
|
+
- Choose the mechanism by the work: use ordinary code for known rules, arithmetic, physics, legal moves, storage, and permissions; use ai.decide for contextual choices, yes/no probabilities, and scores; use ai.chat or ai.generateObject for generated dialogue, explanations, open-ended content, and multi-step reasoning. JEV does not generate text, browse the web, or supply missing app data. Looking at pictures is a preview (decide with images, see its notes): use it only when the creator asks for it, and keep the feature working when it is unavailable. Combine these tools when it benefits the actual request, without silently turning every decision into a more expensive text-model call.
|
|
75
75
|
- Before adopting JEV, identify the concrete decision, the context available at that moment, the allowed outcomes, and why the added latency is worthwhile. Use your model judgment to make that choice. Do not substitute regex or keyword matching for understanding human-language intent. Explain the user-visible benefit simply; the creator does not need to select a provider, enter an API key, or design question schemas.
|
|
76
76
|
- Send related independent questions about the same state together in one ai.decide request (up to 32). Write complete instructions and distinct option or level descriptions. Include an other/uncertain option when the choices might not cover the input. A second call is useful only when it needs newly obtained context or choices that depend on the first result; answers in one batch do not see each other.
|
|
77
77
|
- Keep code in control. Choice returns an option and its probability distribution; score returns a position on the declared levels; noul is the probability of yes, with 0.5 meaning uncertainty rather than medium strength. Confidence describes the distribution and is not proof of correctness. Test the app's criteria and thresholds on clear, ambiguous, and out-of-scope examples, and provide a suitable uncertain outcome. JEV decisions never authorize XP, Coins, purchases, access, or other server-owned state; use the appropriate existing SDK and server approval paths.
|
|
@@ -531,10 +531,10 @@ renderBattery(policy?.energyPercent, policy?.energySegmentsRemaining);
|
|
|
531
531
|
- Use this for in-app AI replies instead of creating or fetching app-local endpoints such as /api/chat.
|
|
532
532
|
- Example: const chatHistory = conversation.slice(-12).map((entry) => ({ role: entry.role === 'assistant' ? 'assistant' : 'user', content: entry.text }));
|
|
533
533
|
const result = await Twinkle.ai.chat({ message, history: chatHistory, systemPrompt: 'You are a cheerful pirate helper who answers in one sentence.', onText: (text, meta) => renderReply(text), onStatus: (status) => setThinking(status === 'thinking') });
|
|
534
|
-
- async decide({ state, questions }) | scopes: none
|
|
535
|
-
- Returns: { answers, model, provider, aiUsagePolicy }
|
|
534
|
+
- async decide({ state, questions, images }) | scopes: none
|
|
535
|
+
- Returns: { answers, model, provider, aiUsagePolicy, imageCount? }
|
|
536
536
|
- Use JEV for fast contextual choices, yes/no probabilities, and scores inside a running app. Send several independent questions about the same state together.
|
|
537
|
-
- Read the AI decision design guidance when deciding whether this capability benefits the app. This method evaluates supplied context; it does not generate prose, browse,
|
|
537
|
+
- Read the AI decision design guidance when deciding whether this capability benefits the app. This method evaluates supplied context; it does not generate prose, browse, run tools, grant access, or award XP/Coins. It looks at pictures only through the preview images option below.
|
|
538
538
|
- state is non-empty text, a JSON object, or a JSON array. questions maps your question IDs to { type, instructions, criteria? }. Instructions may be non-empty text or structured JSON. Every question sees the same state and answers independently; put the complete question in instructions, not just its ID.
|
|
539
539
|
- Choice: { type: 'choice', instructions, criteria: { optionName: description, other: description } }. Supply 2–255 named options. Each description is text, structured JSON, or null. Returns { type: 'choice', choice, probabilities, confidence } under answers[questionId].
|
|
540
540
|
- Score: { type: 'score', instructions, criteria: [lowestLevelDescription, ..., highestLevelDescription] }. Supply 2–10 concrete ordered descriptions. Returns { type: 'score', score, probabilities, confidence, legend }. Levels start at 0; score may be fractional and is a probability-weighted position between 0 and criteria.length - 1.
|
|
@@ -542,6 +542,7 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
|
|
|
542
542
|
- A request accepts 1–32 questions, at most 64 KB of JSON and 20 levels of nesting. Question IDs and option names must be non-empty, at most 128 characters, and cannot be __proto__, constructor, or prototype. Descriptions may use structured JSON; state must contain only data the app is allowed to access.
|
|
543
543
|
- Requires a signed-in viewer and uses the normal runtime AI rate limits. Twinkle selects and calls JEV on its server; never put provider credentials or an external endpoint in app code. Decisions are covered by Twinkle: they never use the viewer's AI Energy, and a viewer with empty Energy still gets answers. The response includes the viewer's current canonical aiUsagePolicy, unchanged by the call.
|
|
544
544
|
- Errors include invalid_ai_decision (bad input), ai_decision_unavailable, ai_decision_timeout, ai_decision_rate_limited, ai_decision_invalid_response, and ai_usage_unavailable, plus the existing auth, access, Energy, and rate-limit errors. Invalid answers are rejected. An answer that fails validation is retried once on the server within the same deadline; there is no other provider retry, fabricated answer, or paid LLM fallback. Do not automatically retry a rejected request.
|
|
545
|
+
- Preview: images (not generally available yet). Pass images: { name: picture } with 1-4 entries to let JEV look at pictures. Names use lowercase letters, digits and underscores (e.g. drawing); each name becomes a key of state, so state must then be a JSON object without that key, and instructions refer to the picture by name (e.g. 'Does `drawing` show a cat?'). A picture is this app's own upload URL (from Twinkle.files, at most 5 MB each), a canvas, or a PNG/JPEG/WebP Blob of at most 3 MB (5 MB of snapshots per decision). All pictures of one decision together must be at most 10 MB; a larger one is refused with 'Image too large: max 5 MB each, 10 MB per decision. Resize the picture first.' so resize big pictures before asking. Image decisions are covered by Twinkle like text decisions. Only some run at once (one per player, several per app, and a generous ceiling per minute for the app), so wait for one answer before asking again and do not call decide in a tight loop. Until Twinkle turns them on for the app, and whenever a picture cannot be checked or too many decisions are running, decide throws ai_decision_unavailable with a reason (disabled, app_disabled, rate_limited, capped, images_dropped, timeout, ...). There is never a text-only answer that ignores the pictures. Use images only when the creator asks for it, keep the feature usable without it (for example skip the AI check and let the player continue), and never use the answer for safety, access, XP or Coins.
|
|
545
546
|
- Call on meaningful app events with bounded frequency, batch questions, and keep rendering and deterministic game rules in local code. Ignore a result if the app state or turn changed while it was pending. Test uncertain inputs and choose a fallback appropriate to the experience; confidence is not a correctness guarantee.
|
|
546
547
|
- Example: const { answers } = await Twinkle.ai.decide({ state: { playerRequest, availableActions }, questions: { action: { type: 'choice', instructions: 'Which available companion action best fits playerRequest? Use wait when unclear.', criteria: { follow: 'Follow the player', guard: 'Stay and keep watch', wait: 'Do nothing until clarified' } }, needsClarification: { type: 'noul', instructions: 'Is playerRequest too ambiguous to act on?' } } }); const action = answers.needsClarification.noul > 0.5 ? 'wait' : answers.action.choice;
|
|
547
548
|
- async generateObject({ prompt, expectedStructure, images, thinkingMode, mode, model, instructions, systemPrompt, webSearch, requestId, onText, onStatus, onReasoning } = {}) | scopes: none
|
|
@@ -554,7 +555,7 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
|
|
|
554
555
|
- 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.
|
|
555
556
|
- 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.
|
|
556
557
|
- mode is accepted as an alias for thinkingMode, and mid is accepted as an alias for medium.
|
|
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 claude-sonnet-5-5
|
|
558
|
+
- 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.
|
|
558
559
|
- thinkingMode low uses GPT-6 Luna with low reasoning and consumes the viewer's AI Energy from confirmed provider usage.
|
|
559
560
|
- thinkingMode medium uses GPT-6 Luna with medium reasoning and consumes normal AI Energy.
|
|
560
561
|
- 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.
|
|
@@ -1574,14 +1575,14 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1574
1575
|
- Daily reflections are Daily Question answers shown in the Twinkle feed.
|
|
1575
1576
|
- 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.
|
|
1576
1577
|
- submittedAt and sharedAt are Unix timestamps (seconds). Use new Date(value * 1000) to convert to JS Date.
|
|
1577
|
-
- 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.
|
|
1578
|
+
- 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.
|
|
1578
1579
|
- async getDailyReflectionsByUser(userId, { cursor, lastId, limit } = {}) | scopes: dailyReflections:read
|
|
1579
1580
|
- Returns: { reflections: [{ id, userId, response, questionId, submittedAt, sharedAt, username, profilePicUrl, question, dayType, pictureUrl, pictureStyle }], cursor? }
|
|
1580
1581
|
- 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.
|
|
1581
1582
|
- Daily reflections are Daily Question answers shown in the Twinkle feed.
|
|
1582
1583
|
- 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.
|
|
1583
1584
|
- submittedAt and sharedAt are Unix timestamps (seconds). Use new Date(value * 1000) to convert to JS Date.
|
|
1584
|
-
- 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.
|
|
1585
|
+
- 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.
|
|
1585
1586
|
|
|
1586
1587
|
### Twinkle.privateDb
|
|
1587
1588
|
- 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>
|
|
@@ -4000,6 +4000,109 @@ input snapshots as described below.
|
|
|
4000
4000
|
See `twinkle-api/JEV_PILOT.md` for configuration,
|
|
4001
4001
|
the synthetic evaluation step and release checks.
|
|
4002
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
|
+
lumine admin jev-images thumbnails --targets subject:<id>,build:<id>,... | --featured [--saved-only] --json
|
|
4012
|
+
lumine admin jev-images decide --area thumbnail --key subject:37265 --decision keep|replace
|
|
4013
|
+
lumine admin jev-images switch --area sdk_decide --build <id> --mode on|off # one app's image decisions (no per-app cap)
|
|
4014
|
+
```
|
|
4015
|
+
|
|
4016
|
+
JEV image checks (Mikey, November 1) send pictures to JEV (`jev-1.13.0`,
|
|
4017
|
+
pinned) with the `TypeSafe-Danger-Zone-Images` opt-in. The vendor marks images
|
|
4018
|
+
experimental. **Every answer is advisory:** never the only gate, never a
|
|
4019
|
+
child-safety decision and never a reason to pay or withhold a reward (standing
|
|
4020
|
+
ruling 11-01). Areas: `arena_paper` (paper-upload pre-screen notes in
|
|
4021
|
+
`approvals show`), `arena_picture` (picture-bank audit), `thumbnail` (Featured
|
|
4022
|
+
thumbnail notes, on by default, cap 2000/day) and `sdk_decide`
|
|
4023
|
+
(`Twinkle.ai.decide` with images, a preview, **off** until Mikey turns it on).
|
|
4024
|
+
`study_record` has no caller: study-record-v1 records are text only, so there
|
|
4025
|
+
are no photos to check, and the area stays off.
|
|
4026
|
+
|
|
4027
|
+
Each check carries a small coloured square and asks its colour. A wrong or
|
|
4028
|
+
unsure colour, or an answer from a different model, means the pictures were
|
|
4029
|
+
dropped. The check is retried once (`droppedRetries`), then returns
|
|
4030
|
+
`images_dropped`. An unavailable check (off, capped, timeout, provider error,
|
|
4031
|
+
dropped images, tables missing) leaves the caller exactly as it was before.
|
|
4032
|
+
|
|
4033
|
+
`daily-run gather` saves the report as `jev-images.json`. Give the full report a
|
|
4034
|
+
**JEV images** section:
|
|
4035
|
+
- per area: mode and source, daily cap and today's use, checks, errors,
|
|
4036
|
+
timeouts, dropped-image retries and give-ups, latency p50/p95, tokens and cost;
|
|
4037
|
+
- agreement between advisories and final decisions (`agreed/scored`), every
|
|
4038
|
+
listed disagreement and the flagged items still awaiting a decision;
|
|
4039
|
+
- every `escalations` entry. Thresholds (`thresholds` in the JSON): error rate
|
|
4040
|
+
above 20% or timeouts above 10% (at least 5 sent checks), dropped-image retries
|
|
4041
|
+
above 5% of requests or any give-up, daily cost above $0.10 and 3x the previous
|
|
4042
|
+
7-day average, daily cost above $2, agreement below 80% over at least 10
|
|
4043
|
+
decided items, and any check skipped at the cap.
|
|
4044
|
+
|
|
4045
|
+
`coverage.status: store_unavailable` means the tables are not migrated: report
|
|
4046
|
+
"not deployed", not a quiet day. A failed gather step is unavailable evidence.
|
|
4047
|
+
JEV image spend is also in the AI ledger (`surface: jev_images`), so never add
|
|
4048
|
+
it to the AI cost total a second time. Agreement is not accuracy: a
|
|
4049
|
+
disagreement can be JEV's mistake or the reviewer's, so read the item before
|
|
4050
|
+
saying which.
|
|
4051
|
+
|
|
4052
|
+
**Paper pre-screen.** For a pending `arenaPaper`, `approvals show <id>` adds a
|
|
4053
|
+
`prescreen` block: per page, whether it looks like a test page, legibility, a
|
|
4054
|
+
guess at the exam family and printed year, whether it fits its tags, plus
|
|
4055
|
+
blank-looking and duplicate pages (plain image arithmetic). Results are saved
|
|
4056
|
+
per page, so a second `show` makes no new call (`(saved)`). Add
|
|
4057
|
+
`?prescreen=off` to the API read to skip it. The approval criterion is
|
|
4058
|
+
unchanged (a real exam paper matching its tags, ruling 10-29): read every page
|
|
4059
|
+
yourself. Approve/reject records the final decision for agreement.
|
|
4060
|
+
|
|
4061
|
+
**Picture-bank audit.** `arena picture-audit` reads live items with a picture
|
|
4062
|
+
and asks fixed questions. For 수능 listening #4 it asks which labelled object
|
|
4063
|
+
mismatches the script, whether the keyed object is drawn as briefed, whether
|
|
4064
|
+
the ① to ⑤ labels are present, and whether there is stray text. For picture
|
|
4065
|
+
choices it asks which picture matches the heard sentence and whether every
|
|
4066
|
+
picture is readable. For other single pictures it asks whether the picture is
|
|
4067
|
+
readable and whether there is stray text. Picture choices are drawn by the web,
|
|
4068
|
+
so pass `--renders <dir>` containing `<itemId>.png` renders of the item as
|
|
4069
|
+
printed, for example from an Arena render script. Without one, the item is
|
|
4070
|
+
listed under `needsRender`. The command never retires or edits an item. Read
|
|
4071
|
+
each `review` item yourself. Record `keep`, `fix` or `retired` with
|
|
4072
|
+
`jev-images decide`. Retiring an item through `arena status` records `retired`
|
|
4073
|
+
automatically. Fixing or retiring live items needs the usual authorization.
|
|
4074
|
+
|
|
4075
|
+
**Featured thumbnails.** `featured candidates` adds a `thumbnailCheck` to each
|
|
4076
|
+
eligible candidate and a `thumbnailChecks` summary; `jev-images thumbnails
|
|
4077
|
+
--featured` does the same for the current board. Each card's picture (the post's
|
|
4078
|
+
own picture, or the cover of the app it is about) gets notes: blank, error or
|
|
4079
|
+
loading screen, placeholder, mostly empty at card size, garbled or interface
|
|
4080
|
+
text, a watermark, and for an app whether it shows the app. Only
|
|
4081
|
+
Twinkle-hosted pictures are read; link previews and videos say
|
|
4082
|
+
`external_thumbnail`, and a secret attachment is never looked at. Answers are
|
|
4083
|
+
saved per picture, so a re-run makes no new call. The notes are never a ranking
|
|
4084
|
+
or eligibility input and nothing is replaced or unfeatured because of them.
|
|
4085
|
+
When you look at a flagged picture, record `keep` or `replace` with
|
|
4086
|
+
`jev-images decide --area thumbnail --key subject:<id>` (or `build:<id>`).
|
|
4087
|
+
Pilot (11-01, app covers only): 43/45 on a held-out set, every good cover
|
|
4088
|
+
passed, 2 of 4 bad ones caught. A pass does not mean the picture is fine.
|
|
4089
|
+
|
|
4090
|
+
**Image decisions in apps (preview).** `Twinkle.ai.decide({ state, questions,
|
|
4091
|
+
images })` lets an app ask about up to four pictures (its own uploads or a
|
|
4092
|
+
canvas). Pictures are bounded: an upload at most 5 MB, a snapshot at most 3 MB
|
|
4093
|
+
(5 MB of snapshots), and 10 MB for all pictures of one decision; a larger call
|
|
4094
|
+
is refused with a plain "Image too large" message before it is fully read. It
|
|
4095
|
+
is free for viewers like text decisions. There are no per-app or
|
|
4096
|
+
per-viewer daily caps (a decision costs a fraction of a cent); the area's daily
|
|
4097
|
+
cap and the cost alarm in the daily report are the spending guard. Bursts are
|
|
4098
|
+
limited instead: one image decision in flight per viewer, eight per app, and
|
|
4099
|
+
600 a minute per app (per API instance, so up to about twice that across the
|
|
4100
|
+
cluster; a class of 30 deciding every ~20 s is about 90 a minute). A throttled call is refused at once as `ai_decision_unavailable` with
|
|
4101
|
+
reason `rate_limited`. It never falls back to a text-only answer: when off,
|
|
4102
|
+
throttled or failing it returns `ai_decision_unavailable` with a reason.
|
|
4103
|
+
`switch --area sdk_decide --build <id> --mode off` switches one app off (or
|
|
4104
|
+
`on` to undo it). The report shows its use under `sdk_decide`.
|
|
4105
|
+
|
|
4003
4106
|
### Spending by calendar month
|
|
4004
4107
|
|
|
4005
4108
|
```bash
|