@stage5/lumine 0.3.66 → 0.3.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/admin-arena.js +48 -1
- package/lib/admin-daily.js +4 -1
- package/lib/admin-owner-trace.js +3 -0
- package/lib/admin-owner-viewport.js +93 -1
- package/lib/admin.js +14 -0
- package/lib/commands.js +1 -0
- package/lib/sdk.js +3 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +17 -2
- package/sdk/LUMINE_ADMIN.md +31 -0
package/lib/admin-arena.js
CHANGED
|
@@ -14,7 +14,7 @@ import path from "node:path";
|
|
|
14
14
|
// re-importing a folder retires that paper's earlier items
|
|
15
15
|
// status --item <id> --to live|retired|checked
|
|
16
16
|
export const ARENA_USAGE =
|
|
17
|
-
"Usage: lumine admin arena queue | import --dir <paper folder> [--audio audio/<exam>/<paper>] [--live] [--confirm] | status --item <id> --to live|retired|checked [--confirm] [--json]";
|
|
17
|
+
"Usage: lumine admin arena queue | marking [--cursor <id>] [--limit <1-200>] | import --dir <paper folder> [--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]";
|
|
18
18
|
const MAX_ITEMS = 300;
|
|
19
19
|
|
|
20
20
|
function validationError(message) {
|
|
@@ -50,6 +50,37 @@ function readPaper(dir) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export function buildArenaOperation({ action, options }) {
|
|
53
|
+
if (action === "today-queue") {
|
|
54
|
+
const cursor = Number(options.adminCursor || 0);
|
|
55
|
+
const limit = Number(options.adminLimit ?? 50);
|
|
56
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0 || !Number.isInteger(limit) || limit < 1 || limit > 100) throw validationError("Use a non-negative --cursor and --limit from 1 to 100.");
|
|
57
|
+
return { name: "arena.today.queue", method: "GET", path: `/cli/admin/arena/today/queue?cursor=${cursor}&limit=${limit}`, mutates: false, requiresRun: false };
|
|
58
|
+
}
|
|
59
|
+
if (action === "today-member") {
|
|
60
|
+
const id = Number(options.adminUser);
|
|
61
|
+
if (!Number.isSafeInteger(id) || id <= 0) throw validationError("Use today-member --user <member id>.");
|
|
62
|
+
return { name: "arena.today.member", method: "GET", path: `/cli/admin/arena/today/member/${id}`, mutates: false, requiresRun: false };
|
|
63
|
+
}
|
|
64
|
+
if (action === "today-import") {
|
|
65
|
+
if (!options.adminFile) throw validationError("Use today-import --file <reviewed-questions.json> [--confirm].");
|
|
66
|
+
const file = path.resolve(options.adminFile);
|
|
67
|
+
if (fs.statSync(file).size > 1000000) throw validationError("The import file must be under 1 MB.");
|
|
68
|
+
const payload = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
69
|
+
const confirm = confirmed(options);
|
|
70
|
+
return { name: confirm ? "arena.today.import" : "arena.today.preview", method: "POST", path: "/cli/admin/arena/today/import", body: { ...payload, dryRun: !confirm }, mutates: confirm, requiresRun: false };
|
|
71
|
+
}
|
|
72
|
+
if (action === "marking") {
|
|
73
|
+
const cursor = Number(options.adminCursor || 0);
|
|
74
|
+
const limit = Number(options.adminLimit ?? 100);
|
|
75
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0 || !Number.isInteger(limit) || limit < 1 || limit > 200) {
|
|
76
|
+
throw validationError("Use a non-negative --cursor and --limit from 1 to 200.");
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
name: "arena.marking", method: "GET",
|
|
80
|
+
path: `/cli/admin/arena/marking?cursor=${cursor}&limit=${limit}`,
|
|
81
|
+
body: undefined, mutates: false, requiresRun: false,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
53
84
|
if (!action || action === "queue") {
|
|
54
85
|
return {
|
|
55
86
|
name: "arena.queue",
|
|
@@ -102,6 +133,22 @@ export function buildArenaOperation({ action, options }) {
|
|
|
102
133
|
}
|
|
103
134
|
|
|
104
135
|
export function formatArenaResult({ operation, data }) {
|
|
136
|
+
if (operation.name === "arena.today.queue") return [
|
|
137
|
+
...(data?.members || []).map(m => `#${m.userId} ${m.username}: ${m.ready} questions ready${m.refillDue ? `; refill to ${m.target}` : ""} (preferences v${m.revision})`),
|
|
138
|
+
data?.nextCursor ? `Continue: lumine admin arena today-queue --cursor ${data.nextCursor} --json` : "End of Today’s Q queue.",
|
|
139
|
+
];
|
|
140
|
+
if (operation.name === "arena.today.member") return [JSON.stringify(data, null, 2)];
|
|
141
|
+
if (operation.name === "arena.today.preview" || operation.name === "arena.today.import") return [
|
|
142
|
+
`${data?.dryRun ? "Dry run" : "Saved"}: ${data?.questions?.length || 0} reviewed personal questions.${data?.dryRun ? " Add --confirm to make new questions available." : ""}`,
|
|
143
|
+
];
|
|
144
|
+
if (operation.name === "arena.marking") {
|
|
145
|
+
const entries = data?.entries || [];
|
|
146
|
+
return [
|
|
147
|
+
`Checked ${data?.scannedBattles ?? 0} completed battles; ${entries.length} need follow-up on this page.`,
|
|
148
|
+
...entries.map((row) => `Battle ${row.battleId}, member ${row.userId}, ${row.examKey}: ${row.reasons.join(", ")} (${row.pendingAnswers} answers pending; ${row.xpEarned}/${row.xpExpected} XP and ${row.coinsEarned}/${row.coinsExpected} Coins paid)`),
|
|
149
|
+
...(data?.nextCursor ? [`Continue: lumine admin arena marking --cursor ${data.nextCursor} --json`] : ["End of marking/reward census."]),
|
|
150
|
+
];
|
|
151
|
+
}
|
|
105
152
|
if (operation.name === "arena.queue") {
|
|
106
153
|
const types = data?.types || [];
|
|
107
154
|
if (!types.length) return ["No live Arena items yet."];
|
package/lib/admin-daily.js
CHANGED
|
@@ -264,7 +264,10 @@ export async function dailyRunGather(options, { run = runAdmin } = {}) {
|
|
|
264
264
|
...(!wake.ok ? ["owner-wake"] : []),
|
|
265
265
|
...(!wakeSessions.ok ? ["owner-wake-sessions"] : []),
|
|
266
266
|
];
|
|
267
|
-
if (ownerWake.collectionFailures.length)
|
|
267
|
+
if (ownerWake.collectionFailures.length) {
|
|
268
|
+
ownerWake.coverage = ownerWake.usableSamples ? "partial" : "unavailable";
|
|
269
|
+
ownerWake.detail.coverage = ownerWake.detail.samples ? "partial" : "unavailable";
|
|
270
|
+
}
|
|
268
271
|
writeAdminJsonFile(path.join(dir, "owner-wake-summary.json"), ownerWake, { privateFile: true });
|
|
269
272
|
fs.writeFileSync(path.join(dir, "owner-wake-summary.md"), formatOwnerViewportReport(ownerWake), { mode: 0o600 });
|
|
270
273
|
steps.push({ name: "owner-wake-summary", file: "owner-wake-summary.json", ok: true });
|
package/lib/admin-owner-trace.js
CHANGED
|
@@ -201,6 +201,9 @@ function describeViewport(data) {
|
|
|
201
201
|
`nav bottom ${nav.bottom ?? "?"} (${nav.position || "absent"})`,
|
|
202
202
|
`inset ${data.inset ?? "?"} focus ${focus.tag || "none"}${focus.type ? `:${focus.type}` : ""}`,
|
|
203
203
|
`scroll ${data.root?.scrollTop ?? "?"}`,
|
|
204
|
+
isPlainObject(data.signals) ? `signals ${Object.entries(data.signals).map(([name, count]) => `${name}:${count}`).join(",")}` : "",
|
|
205
|
+
isPlainObject(data.layout) ? `detail ${data.layout.omitted || `v${data.layout.v}; body top ${data.layout.body?.top ?? "?"}; ancestor effects ${Array.isArray(data.layout.ancestors) ? data.layout.ancestors.length : "?"}`}` : "",
|
|
206
|
+
isPlainObject(data.activity) ? `changes ${data.activity.changes ?? "?"}/${data.activity.limit ?? "?"}; skipped events ${data.activity.skippedEvents ?? "?"}` : "",
|
|
204
207
|
data._trimmed ? `trimmed ${JSON.stringify(data._trimmed)}` : "",
|
|
205
208
|
].filter(Boolean).join(" · ");
|
|
206
209
|
}
|
|
@@ -33,7 +33,9 @@ export function summarizeOwnerViewport(data, sessionData = data) {
|
|
|
33
33
|
}
|
|
34
34
|
cycle.samples += 1;
|
|
35
35
|
if (d.phase === "background") cycle.backgroundSamples += 1;
|
|
36
|
-
|
|
36
|
+
// Activity samples can occur minutes later; only a scheduled baseline
|
|
37
|
+
// establishes that the report actually observed the 20-second follow-up.
|
|
38
|
+
if (d.phase === "settle") cycle.lastScheduledMs = Math.max(cycle.lastScheduledMs, Number(d.scheduledMs) || 0);
|
|
37
39
|
if (d.phase !== "settle" || d.scheduledMs < 1000 || d.visibility !== "visible") continue;
|
|
38
40
|
cycle.settledSamples += 1;
|
|
39
41
|
const nav = d.nav;
|
|
@@ -81,6 +83,7 @@ export function summarizeOwnerViewport(data, sessionData = data) {
|
|
|
81
83
|
truncated: Boolean(data?.truncated), sessionHeadersTruncated: Boolean(sessionData?.truncated),
|
|
82
84
|
viewportEvents, usableSamples, unavailableSamples: unavailable, droppedEvents: dropped,
|
|
83
85
|
wakeProfiles: events.filter(e => e.type === "wake").length,
|
|
86
|
+
detail: summarizeViewportDetail(events, usableSamples, partial),
|
|
84
87
|
clients, clientsWithoutGeometry,
|
|
85
88
|
cycles: rows, cyclesMissingHeaders: missingHeaders,
|
|
86
89
|
cyclesReaching20s: rows.filter(c => c.lastScheduledMs >= 20_000).length,
|
|
@@ -92,6 +95,92 @@ export function summarizeOwnerViewport(data, sessionData = data) {
|
|
|
92
95
|
};
|
|
93
96
|
}
|
|
94
97
|
|
|
98
|
+
function summarizeViewportDetail(events, usableSamples, partial) {
|
|
99
|
+
const rows = events.filter(e => e.type === "wake-viewport" && e.data?.v === 1 &&
|
|
100
|
+
!e.data.error && !e.data._trimmed && e.data.window && e.data.root && e.data.focus);
|
|
101
|
+
const detailed = rows.filter(e => e.data.layout?.v === 2 && !e.data.layout.omitted);
|
|
102
|
+
const omitted = rows.filter(e => e.data.layout?.omitted).length;
|
|
103
|
+
const limitedAncestors = detailed.filter(e => e.data.layout.ancestorLimit).length;
|
|
104
|
+
const capped = new Set();
|
|
105
|
+
const groups = new Map();
|
|
106
|
+
const examples = [];
|
|
107
|
+
let firstTouchPairs = 0, firstTouchChanges = 0, boundaryChanges = 0, ancestorSamples = 0;
|
|
108
|
+
const addExample = (event, kind, extra) => {
|
|
109
|
+
if (examples.filter(example => example.kind === kind).length < 3) examples.push({ kind, sessionId: event.sessionId,
|
|
110
|
+
cycle: event.data.cycle, seq: event.seq, clientTime: event.clientTime, path: event.path, ...extra });
|
|
111
|
+
};
|
|
112
|
+
for (const event of rows) {
|
|
113
|
+
const d = event.data, key = `${event.sessionId}:${d.cycle}`;
|
|
114
|
+
const limit = Number(d.activity?.limit);
|
|
115
|
+
if ((limit > 0 && Number(d.activity?.changes) >= limit) || Number(d.activity?.skippedEvents) > 0) capped.add(key);
|
|
116
|
+
if (d.layout?.v !== 2 || d.layout.omitted) continue;
|
|
117
|
+
if (Array.isArray(d.layout.ancestors) && d.layout.ancestors.length) {
|
|
118
|
+
ancestorSamples++;
|
|
119
|
+
addExample(event, "ancestor-layout-effects", { ancestors: d.layout.ancestors });
|
|
120
|
+
}
|
|
121
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
122
|
+
groups.get(key).push(event);
|
|
123
|
+
}
|
|
124
|
+
for (const group of groups.values()) {
|
|
125
|
+
// Within one client session sequence order survives wall-clock changes.
|
|
126
|
+
group.sort((a, b) => a.seq - b.seq || a.clientTime - b.clientTime);
|
|
127
|
+
let previous = null, firstTouch = null;
|
|
128
|
+
for (const event of group) {
|
|
129
|
+
const d = event.data;
|
|
130
|
+
if (d.signals?.["first-touch"]) firstTouch = event;
|
|
131
|
+
if (d.signals?.["after-first-touch"] && firstTouch) {
|
|
132
|
+
const delta = compareBoundaries(firstTouch, event);
|
|
133
|
+
// A tap can navigate or focus a keyboard; neither is a same-page repair.
|
|
134
|
+
if (delta && d.elapsedMs - firstTouch.data.elapsedMs <= 2000) {
|
|
135
|
+
firstTouchPairs++;
|
|
136
|
+
if (delta.maximum >= 40) {
|
|
137
|
+
firstTouchChanges++;
|
|
138
|
+
addExample(event, "first-touch-boundary-change", { beforeSeq: firstTouch.seq, ...delta });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
firstTouch = null;
|
|
142
|
+
}
|
|
143
|
+
if (previous) {
|
|
144
|
+
const delta = compareBoundaries(previous, event);
|
|
145
|
+
if (delta && delta.maximum >= 40) {
|
|
146
|
+
boundaryChanges++;
|
|
147
|
+
addExample(event, "boundary-change", { beforeSeq: previous.seq, signals: d.signals, ...delta });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
previous = event;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
coverage: !detailed.length ? "unavailable" : partial || limitedAncestors || detailed.length < usableSamples ? "partial" : "observed",
|
|
155
|
+
samples: detailed.length, omittedSamples: omitted, limitedAncestorSamples: limitedAncestors, activityCappedCycles: capped.size,
|
|
156
|
+
firstTouchPairs, firstTouchChanges, boundaryChanges, ancestorSamples, examples,
|
|
157
|
+
note: "Boundary changes (>=40px) and ancestor CSS effects are comparison evidence, not confirmed gaps or repairs. A first touch can naturally move browser controls. Unchanged DOM measurements cannot rule out a visible rendering fault. Activity capture is bounded; no samples or a reached cap cannot establish healthy behavior.",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function compareBoundaries(before, after) {
|
|
162
|
+
const a = before.data, b = after.data;
|
|
163
|
+
if (![a.elapsedMs, b.elapsedMs, a.window.width, b.window.width].every(Number.isFinite) ||
|
|
164
|
+
before.path !== after.path || a.window.width !== b.window.width ||
|
|
165
|
+
a.layout.orientation !== b.layout.orientation || b.elapsedMs <= a.elapsedMs) return null;
|
|
166
|
+
for (const d of [a, b]) {
|
|
167
|
+
if (d.visibility !== "visible" || d.inset !== 0 || d.focus.editable ||
|
|
168
|
+
["input", "textarea", "iframe"].includes(d.focus.tag) ||
|
|
169
|
+
!Number.isFinite(d.visual?.scale) || Math.abs(d.visual.scale - 1) > 0.01 ||
|
|
170
|
+
d.nav?.position !== "fixed" || parseFloat(d.nav.bottomStyle) !== 0 ||
|
|
171
|
+
d.nav.height <= 0 || d.nav.display === "none" || d.nav.visibility === "hidden") return null;
|
|
172
|
+
if (![d.window.height, d.root.height, d.visual.height, d.visual.offsetTop, d.nav.bottom].every(Number.isFinite)) return null;
|
|
173
|
+
}
|
|
174
|
+
const changes = {
|
|
175
|
+
window: b.window.height - a.window.height,
|
|
176
|
+
root: b.root.height - a.root.height,
|
|
177
|
+
visual: b.visual.height - a.visual.height,
|
|
178
|
+
visualTop: b.visual.offsetTop - a.visual.offsetTop,
|
|
179
|
+
navBottom: b.nav.bottom - a.nav.bottom,
|
|
180
|
+
};
|
|
181
|
+
return { changes, maximum: Math.max(...Object.values(changes).map(Math.abs)) };
|
|
182
|
+
}
|
|
183
|
+
|
|
95
184
|
export function formatOwnerViewportReport(summary) {
|
|
96
185
|
const lines = [
|
|
97
186
|
"# Owner wake viewport follow-up", "",
|
|
@@ -100,6 +189,9 @@ export function formatOwnerViewportReport(summary) {
|
|
|
100
189
|
`Collection failures: ${summary.collectionFailures?.join(", ") || "none"}.`, "",
|
|
101
190
|
`Large-gap candidates: ${summary.largeGapCycles} cycles; ${summary.repeatedLargeGapCycles} had settled bottom-nav gaps >=100px at least 500ms apart in actual time, no focused input, no app keyboard inset and scale 1.`, "",
|
|
102
191
|
`Missing/trimmed geometry: ${summary.unavailableSamples}; clients without usable geometry: ${summary.clientsWithoutGeometry}; reported dropped events: ${summary.droppedEvents}; cycles without session context: ${summary.cyclesMissingHeaders}; truncated trace: ${summary.truncated}; truncated session history: ${summary.sessionHeadersTruncated}.`, "",
|
|
192
|
+
`Expanded diagnostics: **${summary.detail?.coverage || "unavailable"}**; ${summary.detail?.samples || 0} detailed samples, ${summary.detail?.omittedSamples || 0} payload-budget omissions, ${summary.detail?.limitedAncestorSamples || 0} bounded ancestor scans, ${summary.detail?.activityCappedCycles || 0} cycles reached the activity cap.`, "",
|
|
193
|
+
`Comparison leads: ${summary.detail?.boundaryChanges || 0} boundary changes >=40px; ${summary.detail?.firstTouchPairs || 0} comparable first-touch pairs (${summary.detail?.firstTouchChanges || 0} changed >=40px); ${summary.detail?.ancestorSamples || 0} samples with ancestor layout effects. These are not diagnoses. See detail.examples in owner-wake-summary.json and the corresponding raw sequence numbers.`, "",
|
|
194
|
+
summary.detail?.note || "Expanded telemetry has not been observed in this window.", "",
|
|
103
195
|
summary.note, "",
|
|
104
196
|
"Read owner-wake.json and owner-wake-sessions.json alongside this summary. Compare affected and ordinary cycles within the same client/browser version. Distinguish viewport height changes, viewport panning and nav displacement; correlate the wake performance profile and its upload network tag. Record the evidence and next action in the daily report even when coverage is unavailable.", "",
|
|
105
197
|
"| Client | Platform | Cycle | Samples | Settled | Large gap | Max root gap | Max visual gap |",
|
package/lib/admin.js
CHANGED
|
@@ -3013,6 +3013,20 @@ export function parseAdminOperation(options) {
|
|
|
3013
3013
|
// Mikey's decisions, any time; each comes with its evidence (approvals.ts).
|
|
3014
3014
|
return buildApprovalOperation({ action, target, options });
|
|
3015
3015
|
}
|
|
3016
|
+
if (namespace === "dictionary") {
|
|
3017
|
+
if (action === "korean-status") return readOperation("dictionary.korean-status", "/cli/admin/dictionary/korean/status", { requiresRun: false });
|
|
3018
|
+
if (action === "korean-pending") return readOperation("dictionary.korean-pending", withQuery("/cli/admin/dictionary/korean/pending", {
|
|
3019
|
+
cursor: options.adminCursor || undefined,
|
|
3020
|
+
limit: parseRequiredInteger(options.adminLimit || 30, "--limit", 1, 100),
|
|
3021
|
+
}), { requiresRun: false });
|
|
3022
|
+
if (action === "korean-review" && target) {
|
|
3023
|
+
const raw = readAdminJsonFile(target, "<reviewed-definitions.json>", { maxBytes: 512 * 1024 });
|
|
3024
|
+
const items = Array.isArray(raw) ? raw : raw?.items;
|
|
3025
|
+
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.");
|
|
3026
|
+
return writeOperation("dictionary.korean-review", "POST", "/cli/admin/dictionary/korean/review", { items, dryRun: Boolean(options.adminDryRun) }, { requiresRun: false, mutates: !options.adminDryRun, timeoutMs: 120000 });
|
|
3027
|
+
}
|
|
3028
|
+
throw cliValidationError("Use: lumine admin dictionary korean-status | korean-pending [--cursor <time:id>] [--limit <1-100>] | korean-review <reviewed-definitions.json> [--dry-run]");
|
|
3029
|
+
}
|
|
3016
3030
|
if (namespace === "word-games") {
|
|
3017
3031
|
if (action === "status") return readOperation("word-games.status", "/cli/admin/word-games/status", { requiresRun: false });
|
|
3018
3032
|
if (action === "candidates") return readOperation("word-games.candidates", withQuery("/cli/admin/word-games/candidates", {
|
package/lib/commands.js
CHANGED
|
@@ -3310,6 +3310,7 @@ export function printHelp() {
|
|
|
3310
3310
|
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)
|
|
3311
3311
|
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)
|
|
3312
3312
|
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)
|
|
3313
|
+
lumine admin dictionary korean-status | korean-pending [--cursor <time:id>] [--limit <1-100>] | korean-review <reviewed-definitions.json> [--dry-run] (shared Korean word-bank definitions; daily review, no paid generation)
|
|
3313
3314
|
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)
|
|
3314
3315
|
lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] [--json]
|
|
3315
3316
|
lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
|
package/lib/sdk.js
CHANGED
|
@@ -41,6 +41,9 @@ export const SDK_CLI_METHODS = {
|
|
|
41
41
|
"wordMaster.reportIssue": { path: "api/content/word-master/telemetry", scopes: ["content:write"], write: true },
|
|
42
42
|
"wordMaster.getDailyWords": { path: "api/content/word-master/words", scopes: ["content:read"] },
|
|
43
43
|
"wordMaster.getWord": { path: "api/content/word-master/details", scopes: ["content:write"], write: true },
|
|
44
|
+
"wordMaster.lookupWord": { path: "api/content/word-master/dictionary", scopes: ["content:read"] },
|
|
45
|
+
"wordMaster.generateKoreanDefinition": { path: "api/content/word-master/korean-definition", scopes: ["content:write"], write: true },
|
|
46
|
+
"wordMaster.getKoreanDefinition": { path: "api/content/word-master/korean-definition-status", scopes: ["content:write"], write: true },
|
|
44
47
|
"wordMaster.generateEntry": { path: "api/content/word-master/generate-entry", scopes: ["content:write"], write: true },
|
|
45
48
|
"wordMaster.getEntry": { path: "api/content/word-master/entry-status", scopes: ["content:read"], write: true },
|
|
46
49
|
"wordMaster.encounterWords": { path: "api/content/word-master/encounter", scopes: ["content:write"], write: true },
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Build SDK Index
|
|
2
2
|
|
|
3
3
|
Version: 1.73.0
|
|
4
|
-
Updated: 2026-10-
|
|
5
|
-
Generated: 2026-10-
|
|
4
|
+
Updated: 2026-10-27
|
|
5
|
+
Generated: 2026-10-26T22:37:17.385Z
|
|
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.
|
|
@@ -828,6 +828,21 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
828
828
|
- Returns: { word: { id, word, definition, example, wordLevel, canGenerate }, mode }
|
|
829
829
|
- Open an existing Word Master word’s reviewed definition and example, and add it to the viewer’s book.
|
|
830
830
|
- Pass wordId or the exact word. Reuses reviewed existing definitions and suitable public AI Card examples. Blank definition/example means missing. Adds this encounter to the book but awards nothing; this is a write for CLI/capture permission purposes. No viewer AI Battery is used.
|
|
831
|
+
- async lookupWord({ wordId, word }) | scopes: content:read
|
|
832
|
+
- Returns: { word: { id, word, wordLevel, definitions: [{ partOfSpeech, definition }], koreanDefinition, koreanDefinitionRevision, koreanDefinitionStatus } }
|
|
833
|
+
- Read an exact word from the shared dictionary without spending Battery or changing the viewer’s book.
|
|
834
|
+
- Pass wordId or an exact English headword (1–64 characters; internal spaces, apostrophes and hyphens supported). Only existing, uncensored word-bank entries are eligible; an unknown spelling returns dictionary_word_not_found (404). This does not create words or perform reverse Korean search.
|
|
835
|
+
- koreanDefinitionStatus is missing, generated (awaiting daily review), or reviewed. definitions contains up to 12 existing English meanings. Reads never generate or charge. Always use current canonical text: daily corrections replace earlier generated text.
|
|
836
|
+
- async generateKoreanDefinition({ wordId, requestId, useAiBattery }) | scopes: content:write
|
|
837
|
+
- Returns: { entry?: { id, wordId, requestId, status, failureCode }, word?, pending?, alreadyComplete?, aiUsagePolicy? }
|
|
838
|
+
- Fill a missing shared Korean definition using the searching member’s AI Battery.
|
|
839
|
+
- Before the intentional search, clearly disclose that a missing Korean definition uses the viewer’s AI Battery; pass useAiBattery:true. Do not generate on page load, resume, keystrokes or background refresh. Cached content costs no Battery. This is shared canonical dictionary content in BOTH preview and live, across all apps; it grants no XP, Coins, mastery or completion receipt.
|
|
840
|
+
- Persist a stable 16–64 character requestId before calling. One reservation per word prevents concurrent duplicate generations. pending:true means another request owns that reservation: use free lookupWord checks, then allow an explicit retry if it remains missing. Never invent a payment or definition from local state.
|
|
841
|
+
- entry.status is pending, generated, ready, failed or superseded. Recover unknown transport outcomes with getKoreanDefinition and the SAME requestId; retrying generate with that same ID is idempotent. A confirmed failed job may be deliberately retried with a new ID. Only successful saved definitions charge Battery, atomically with publication. At most 20 generation attempts per member per UTC day and one active generation per member.
|
|
842
|
+
- async getKoreanDefinition({ requestId }) | scopes: content:write
|
|
843
|
+
- Returns: { entry: { id, wordId, requestId, status, failureCode } | null, word?, aiUsagePolicy? }
|
|
844
|
+
- Recover a Korean-definition request after a lost response without another model call.
|
|
845
|
+
- Scoped to this build and viewer. A generated result is saved and charged once; status never launches another model call. entry:null means no job was found: an explicit retry must reuse the saved requestId. Return values include the current shared definition, including subsequent reviewer corrections.
|
|
831
846
|
- async generateEntry({ wordId, requestId, useAiBattery }) | scopes: content:write
|
|
832
847
|
- Returns: { entry?, word?, alreadyComplete?, aiUsagePolicy, mode }
|
|
833
848
|
- Use viewer AI Battery to fill a missing meaning/example once, with a server-verified contribution receipt.
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -1653,6 +1653,27 @@ control contents. Normal wakes are retained for comparison. `--perf` also
|
|
|
1653
1653
|
includes the geometry rows. The session's `viewportTrace: 1` marks support;
|
|
1654
1654
|
older tabs do not acquire it until they load a version containing the change.
|
|
1655
1655
|
|
|
1656
|
+
The October 26 expanded recorder keeps that v1 event compatible and adds
|
|
1657
|
+
`layout.v: 2` (`viewportDetail: 2` on the session): body/root bounds, nav padding,
|
|
1658
|
+
orientation and bounded ancestor CSS-effect flags. Mobile load/return cycles
|
|
1659
|
+
also capture one first-touch/300ms pair and at most four debounced
|
|
1660
|
+
resize/scroll/orientation bursts. `signals` and `activity` retain event counts,
|
|
1661
|
+
the cap and skipped/pending events; optional context can explicitly report
|
|
1662
|
+
`omitted: "payload-budget"`. No page content, screenshot or automatic correction
|
|
1663
|
+
is included. Check `work/mobile-bottom-gap-20261026/telemetry/STATUS.md` for
|
|
1664
|
+
actual release status; use the workspace CLI for this expanded summary until
|
|
1665
|
+
an npm release containing it is recorded there.
|
|
1666
|
+
|
|
1667
|
+
The daily summary's `detail` reports expanded coverage, capped/omitted samples,
|
|
1668
|
+
boundary changes, comparable first-touch pairs and ancestor effects, with raw
|
|
1669
|
+
sequence numbers for follow-up. These are comparison leads, never proof of a
|
|
1670
|
+
rendering defect or repair. Normal toolbar movement changes heights too. A
|
|
1671
|
+
normal DOM rectangle cannot disprove a visible compositor-only gap, and reads
|
|
1672
|
+
may flush pending layout. Review new evidence and keep the original roughly
|
|
1673
|
+
108px gap open until its cause and fix are established. Follow the owner-wake
|
|
1674
|
+
section in `agent-guides/lumine-admin.md`; do not restart the stopped physical
|
|
1675
|
+
phone/Simulator investigation without Mikey's instruction.
|
|
1676
|
+
|
|
1656
1677
|
`daily-run gather` additionally saves `owner-wake.json`, the retained session
|
|
1657
1678
|
context in `owner-wake-sessions.json`, and `owner-wake-summary.json` / `.md`.
|
|
1658
1679
|
Include the coverage and follow-up in the daily report. A >=100px settled gap
|
|
@@ -5544,3 +5565,13 @@ Every full daily website-management run adds reviewed Word Master words across a
|
|
|
5544
5565
|
## Word Search monthly champions
|
|
5545
5566
|
|
|
5546
5567
|
Word Search rankings come from server-verified solves, but every board comes from public inputs, so a script playing at a human-looking pace can still rank. A month's champion prize (word-search-champion-v1) opens only after review. From 07:00 UTC on the 2nd, run `lumine admin word-games champions --build <id>`. It lists the last three final months: the current candidate, whether the win is confirmed or collected, and each top-three player's best solves (score, seconds, words, gaps between words, gap variation (near 0 means machine-even spacing), misses, pauses, hints, best streak, days played). Confirm a believable human with `lumine admin word-games confirm <YYYY-MM> <userId> --build <id> --reason "<evidence>"`. Disqualify a scripted winner with `disqualify`; the next eligible player becomes the candidate, so review them too. Removed and fully banned accounts are skipped automatically. Both writes are audited, and neither works after the prize is collected. Until confirmation the winner's game shows that the win is being checked.
|
|
5568
|
+
|
|
5569
|
+
## Shared Korean dictionary definitions
|
|
5570
|
+
|
|
5571
|
+
Every full management run includes this dedicated review, requested by Mikey October 27. A missing Korean definition is generated on an intentional member search using **that member's AI Battery**; saved definitions are shared and free to reuse. Preview and live apps use the same canonical word bank. These admin commands never invoke a paid model, charge a member, or grant rewards.
|
|
5572
|
+
|
|
5573
|
+
- `lumine admin dictionary korean-status --json`: current generated/reviewed/pending coverage, oldest pending time, outstanding jobs and recent failures.
|
|
5574
|
+
- `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.
|
|
5575
|
+
- `lumine admin dictionary korean-review <reviewed.json> --dry-run --json`, then omit `--dry-run` 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.
|
|
5576
|
+
|
|
5577
|
+
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.
|