@stage5/lumine 0.3.37 → 0.3.39
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-craft.js +36 -5
- package/lib/admin.js +21 -4
- package/lib/commands.js +2 -1
- package/lib/sdk.js +17 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +103 -7
- package/sdk/LUMINE_ADMIN.md +17 -1
package/lib/admin-craft.js
CHANGED
|
@@ -3,11 +3,12 @@ import { readAdminJsonFile } from "./admin-news.js";
|
|
|
3
3
|
// `lumine admin craft seed|stats|revise`: our agents curate a Twinkle.craft
|
|
4
4
|
// universe (Infinity Craft), Math Lab style. seed writes pairs we wrote (no
|
|
5
5
|
// AI call), stats shows sizes, the frontier, today's target and the newest
|
|
6
|
-
// AI answers to audit, revise corrects an item's teaching text
|
|
6
|
+
// AI answers to audit, revise corrects an item's teaching text or one pair's
|
|
7
|
+
// why (the line a player sees when they make that pair). No daily run
|
|
7
8
|
// needed. seed and revise are dry runs unless --confirm is given.
|
|
8
9
|
|
|
9
10
|
export const CRAFT_USAGE =
|
|
10
|
-
"Usage: lumine admin craft seed --build <id> --file <pairs.json> [--confirm] | stats --build <id> [--limit <n>] | revise --build <id> --item <id|name> [--why <text>] [--emoji <emoji>] [--category <field>] [--confirm] | retire --build <id> --item <id|name> --reason <text> [--confirm] | unfizzle --build <id> [--since <iso>] [--pair <a+b>] [--confirm] [--json].";
|
|
11
|
+
"Usage: lumine admin craft seed --build <id> --file <pairs.json> [--confirm] | stats --build <id> [--limit <n>] | 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] [--json].";
|
|
11
12
|
const CRAFT_SEED_MAX = 500;
|
|
12
13
|
|
|
13
14
|
function validationError(message) {
|
|
@@ -82,10 +83,31 @@ export function buildCraftOperation({ action, options }) {
|
|
|
82
83
|
};
|
|
83
84
|
}
|
|
84
85
|
if (action === "revise") {
|
|
86
|
+
const pair = textFlag(options.adminCraftPair, "--pair");
|
|
85
87
|
const item = textFlag(options.adminCraftItem, "--item");
|
|
86
|
-
if (!item) throw validationError(`Pass --item <id|name>. ${CRAFT_USAGE}`);
|
|
87
|
-
const body = { item: item.trim() };
|
|
88
88
|
const why = textFlag(options.adminCraftWhy, "--why");
|
|
89
|
+
const confirm = confirmed(options);
|
|
90
|
+
if (pair !== undefined) {
|
|
91
|
+
// A pair's own why; its result never changes.
|
|
92
|
+
if (item !== undefined || options.adminCraftEmoji != null || options.adminCraftCategory != null) {
|
|
93
|
+
throw validationError("--pair revises only the pair's --why: leave out --item, --emoji and --category.");
|
|
94
|
+
}
|
|
95
|
+
const ref = pair.trim();
|
|
96
|
+
if (!/^\d+$/.test(ref) && ref.split("+").filter((name) => name.trim()).length !== 2) {
|
|
97
|
+
throw validationError('--pair must be a pair id or two item names joined by +, e.g. "Earth + Smoke".');
|
|
98
|
+
}
|
|
99
|
+
if (why === undefined) throw validationError("Pass --why <text> with --pair.");
|
|
100
|
+
return {
|
|
101
|
+
name: confirm ? "craft.revise" : "craft.revise.preview",
|
|
102
|
+
method: "POST",
|
|
103
|
+
path: `${base}/revise`,
|
|
104
|
+
body: { pair: ref, why, dryRun: !confirm },
|
|
105
|
+
mutates: confirm,
|
|
106
|
+
requiresRun: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (!item) throw validationError(`Pass --item <id|name> or --pair <pairId|a+b>. ${CRAFT_USAGE}`);
|
|
110
|
+
const body = { item: item.trim() };
|
|
89
111
|
const emoji = textFlag(options.adminCraftEmoji, "--emoji");
|
|
90
112
|
const category = textFlag(options.adminCraftCategory, "--category");
|
|
91
113
|
if (why !== undefined) body.why = why;
|
|
@@ -94,7 +116,6 @@ export function buildCraftOperation({ action, options }) {
|
|
|
94
116
|
if (Object.keys(body).length === 1) {
|
|
95
117
|
throw validationError("Pass at least one of --why, --emoji or --category (the name never changes).");
|
|
96
118
|
}
|
|
97
|
-
const confirm = confirmed(options);
|
|
98
119
|
return {
|
|
99
120
|
name: confirm ? "craft.revise" : "craft.revise.preview",
|
|
100
121
|
method: "POST",
|
|
@@ -162,6 +183,16 @@ export function formatCraftResult({ operation, data }) {
|
|
|
162
183
|
if (data.dryRun) lines.push("Nothing was written. Add --confirm to seed.");
|
|
163
184
|
return lines;
|
|
164
185
|
}
|
|
186
|
+
if (name.startsWith("craft.revise") && data.pair) {
|
|
187
|
+
const { pair } = data;
|
|
188
|
+
lines.push(
|
|
189
|
+
`${data.dryRun ? "DRY RUN: " : ""}pair #${pair.id} ${pair.a} + ${pair.b} = ${pair.result.emoji} ${pair.result.name} (#${pair.result.id})`,
|
|
190
|
+
);
|
|
191
|
+
lines.push(` before: ${data.before.why ?? "(no explanation)"}`);
|
|
192
|
+
lines.push(` after: ${data.after.why}`);
|
|
193
|
+
if (data.dryRun) lines.push("Nothing was written. Add --confirm to revise.");
|
|
194
|
+
return lines;
|
|
195
|
+
}
|
|
165
196
|
if (name.startsWith("craft.revise")) {
|
|
166
197
|
const show = (value) => `${value.emoji} ${value.category} · ${value.why ?? "(no explanation)"}`;
|
|
167
198
|
lines.push(`${data.dryRun ? "DRY RUN: " : ""}${data.item.name} (#${data.item.id})`);
|
package/lib/admin.js
CHANGED
|
@@ -179,6 +179,7 @@ const REVIEW_REQUEST_TYPES = [
|
|
|
179
179
|
"project-limit",
|
|
180
180
|
"storage-limit",
|
|
181
181
|
"cardcraft",
|
|
182
|
+
"soundtrack",
|
|
182
183
|
];
|
|
183
184
|
const REVIEW_REQUEST_TYPE_ALIASES = {
|
|
184
185
|
rewards: "rewards",
|
|
@@ -192,6 +193,8 @@ const REVIEW_REQUEST_TYPE_ALIASES = {
|
|
|
192
193
|
cardcraft: "cardcraft",
|
|
193
194
|
"card-craft": "cardcraft",
|
|
194
195
|
crafting: "cardcraft",
|
|
196
|
+
soundtrack: "soundtrack",
|
|
197
|
+
"song-tag": "soundtrack",
|
|
195
198
|
};
|
|
196
199
|
const REVIEW_REQUEST_LIST_STATUSES = [
|
|
197
200
|
"queue",
|
|
@@ -206,9 +209,10 @@ const REVIEW_REQUEST_LABELS = {
|
|
|
206
209
|
"project-limit": "Project room",
|
|
207
210
|
"storage-limit": "Lumine file storage",
|
|
208
211
|
cardcraft: "AI Card crafting",
|
|
212
|
+
soundtrack: "Song app tag",
|
|
209
213
|
};
|
|
210
214
|
const REVIEW_REQUEST_USAGE =
|
|
211
|
-
"Usage: lumine admin review list [--type rewards|project|storage|cardcraft] [--status queue|pending|approved|rejected|all] [--cursor <c>] | show <type:id> [--dir <path>] | approve <type:id> [--config <rules.json>] [--size <500MB|1GB|2GB|n MB>] [--reason <text>] | reject <type:id> [--reason <text>] | revoke <type:id> --reason <text> | propose rewards:<id> --dir <edited-snapshot> --config <rules.json> [--reason <text>] [--title <new app name>] [--thumbnail <image|workspace>]. A bare id works with --type.";
|
|
215
|
+
"Usage: lumine admin review list [--type rewards|project|storage|cardcraft|soundtrack] [--status queue|pending|approved|rejected|all] [--cursor <c>] | show <type:id> [--dir <path>] | approve <type:id> [--config <rules.json>] [--size <500MB|1GB|2GB|n MB>] [--reason <text>] | reject <type:id> [--reason <text>] | revoke <type:id> --reason <text> | propose rewards:<id> --dir <edited-snapshot> --config <rules.json> [--reason <text>] [--title <new app name>] [--thumbnail <image|workspace>]. A bare id works with --type.";
|
|
212
216
|
|
|
213
217
|
function parseReviewRequestType(value) {
|
|
214
218
|
const key = String(value || "")
|
|
@@ -217,7 +221,7 @@ function parseReviewRequestType(value) {
|
|
|
217
221
|
const type = REVIEW_REQUEST_TYPE_ALIASES[key];
|
|
218
222
|
if (!type) {
|
|
219
223
|
throw cliValidationError(
|
|
220
|
-
`Unknown request type "${key}". Use rewards, project, storage or
|
|
224
|
+
`Unknown request type "${key}". Use rewards, project, storage, cardcraft or soundtrack.`,
|
|
221
225
|
);
|
|
222
226
|
}
|
|
223
227
|
return type;
|
|
@@ -882,13 +886,13 @@ function buildReviewRequestOperation({
|
|
|
882
886
|
if (!REWARD_REVIEW_DECISIONS.includes(action)) {
|
|
883
887
|
return null;
|
|
884
888
|
}
|
|
885
|
-
if (action === "revoke" && !["rewards", "cardcraft"].includes(ref.type)) {
|
|
889
|
+
if (action === "revoke" && !["rewards", "cardcraft", "soundtrack"].includes(ref.type)) {
|
|
886
890
|
throw cliValidationError(
|
|
887
891
|
`${REVIEW_REQUEST_LABELS[ref.type]} approvals are not revoked; change the limit instead${ref.type === "storage-limit" ? " (lumine admin storage grant)" : ""}.`,
|
|
888
892
|
);
|
|
889
893
|
}
|
|
890
894
|
if (
|
|
891
|
-
["rewards", "cardcraft"].includes(ref.type) &&
|
|
895
|
+
["rewards", "cardcraft", "soundtrack"].includes(ref.type) &&
|
|
892
896
|
action !== "approve" &&
|
|
893
897
|
!reason
|
|
894
898
|
) {
|
|
@@ -2907,6 +2911,19 @@ export function parseAdminOperation(options) {
|
|
|
2907
2911
|
// Mikey's decisions, any time; each comes with its evidence (approvals.ts).
|
|
2908
2912
|
return buildApprovalOperation({ action, target, options });
|
|
2909
2913
|
}
|
|
2914
|
+
if (namespace === "word-games") {
|
|
2915
|
+
if (action === "status") return readOperation("word-games.status", "/cli/admin/word-games/status", { requiresRun: false });
|
|
2916
|
+
if (action === "candidates") return readOperation("word-games.candidates", withQuery("/cli/admin/word-games/candidates", {
|
|
2917
|
+
level: parseRequiredInteger(target, "word level", 1, 5), after: options.adminAfter || 0, limit: options.adminLimit || 50,
|
|
2918
|
+
}), { requiresRun: false });
|
|
2919
|
+
if (action === "import" && target) {
|
|
2920
|
+
const raw = readAdminJsonFile(target, "<reviewed-words.json>", { maxBytes: 256 * 1024 });
|
|
2921
|
+
const items = Array.isArray(raw) ? raw : raw?.items;
|
|
2922
|
+
if (!Array.isArray(items) || !items.length || items.length > 100) throw cliValidationError("Import 1 to 100 reviewed words, with definitions and examples.");
|
|
2923
|
+
return writeOperation("word-games.import", "POST", "/cli/admin/word-games/import", { items, dryRun: Boolean(options.adminDryRun) }, { requiresRun: false, mutates: !options.adminDryRun, timeoutMs: 120000 });
|
|
2924
|
+
}
|
|
2925
|
+
throw cliValidationError("Use: lumine admin word-games status | candidates <1-5> [--after <id>] [--limit <n>] | import <reviewed-words.json> [--dry-run]");
|
|
2926
|
+
}
|
|
2910
2927
|
if (namespace === "english") {
|
|
2911
2928
|
return buildEnglishOperation({ action, target, options });
|
|
2912
2929
|
}
|
package/lib/commands.js
CHANGED
|
@@ -2807,6 +2807,7 @@ export function parseArgs(args) {
|
|
|
2807
2807
|
rewardPreflight: Boolean(raw.preflight),
|
|
2808
2808
|
rewardApprovalFile: raw.approvalFile ? String(raw.approvalFile) : "",
|
|
2809
2809
|
// admin craft revise --item <id|name> [--why] [--emoji] [--category]
|
|
2810
|
+
// or --pair <pairId|a+b> --why
|
|
2810
2811
|
// (kept raw: a flag given without a text value is refused in
|
|
2811
2812
|
// admin-craft.js rather than sent as the string "true")
|
|
2812
2813
|
adminCraftItem: raw.item,
|
|
@@ -3252,7 +3253,7 @@ export function printHelp() {
|
|
|
3252
3253
|
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)
|
|
3253
3254
|
lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] [--json]
|
|
3254
3255
|
lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
|
|
3255
|
-
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] | 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)
|
|
3256
|
+
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)
|
|
3256
3257
|
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>]
|
|
3257
3258
|
lumine admin meetup list [--status review|active|completed|all] | approve-crew <crewId> | approve-grownup <crewId> | request-changes <crewId> --note <text> | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | parent-reply <crewId> <memberUserId> --note <text> | funnel [--days <1-90>] | branches [list [--status pending|official|rejected|all] | show <id> | approve <id> | reject <id> [--note <text>] | merge <id> --into <officialId>] | show <crewId> | approve-plan <crewId> [--note <text>] | send-back <crewId> --note <text> | approve <crewId> --attended <userId,userId,...> [--note <text>] | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | set-branch <crewId> <userId> --branch "<name>" [--note <text>] | slot <crewId> --confirm <slotIndex> | resend-email <crewId> (Bridge Builder meetup quest)
|
|
3258
3259
|
lumine admin meetup story list | show <crewId> | approve <crewId> [--note <text>] [--no-announce] | send-back <crewId> --note <text> | unpublish <crewId> --note <text> (Bridge Builder story pages: publishes only what every tagged member's parent allowed)
|
package/lib/sdk.js
CHANGED
|
@@ -13,6 +13,11 @@ import { findLocalProjectMetadata } from "./workspace.js";
|
|
|
13
13
|
// TWINKLE_BUILD_SDK.md. Methods that mutate data require --allow-write.
|
|
14
14
|
// Anything not listed can be called with --path api/<endpoint>.
|
|
15
15
|
export const SDK_CLI_METHODS = {
|
|
16
|
+
"soundtracks.searchSources": { path: "api/soundtracks/sources", scopes: ["sharedDb:read"] },
|
|
17
|
+
"soundtracks.getTags": { path: "api/soundtracks/tags", scopes: ["sharedDb:read"] },
|
|
18
|
+
"soundtracks.getRecommendations": { path: "api/soundtracks/recommendations", scopes: ["sharedDb:read"] },
|
|
19
|
+
"soundtracks.requestTag": { path: "api/soundtracks/request-tag", scopes: ["sharedDb:write"], write: true },
|
|
20
|
+
"soundtracks.removeTag": { path: "api/soundtracks/remove-tag", scopes: ["sharedDb:write"], write: true },
|
|
16
21
|
"network.getHome": { path: "api/network/home", scopes: ["sharedDb:read"] },
|
|
17
22
|
"network.getFeed": { path: "api/network/feed", scopes: ["sharedDb:read"] },
|
|
18
23
|
"network.getThread": { path: "api/network/thread", scopes: ["sharedDb:read"] },
|
|
@@ -33,6 +38,18 @@ export const SDK_CLI_METHODS = {
|
|
|
33
38
|
"aiCards.list": { path: "api/content/ai-cards/list", scopes: ["content:read"] },
|
|
34
39
|
"aiCards.search": { path: "api/content/ai-cards/search", scopes: ["content:read"] },
|
|
35
40
|
"aiCards.get": { path: "api/content/ai-card", scopes: ["content:read"] },
|
|
41
|
+
"wordMaster.reportIssue": { path: "api/content/word-master/telemetry", scopes: ["content:write"], write: true },
|
|
42
|
+
"wordMaster.getDailyWords": { path: "api/content/word-master/words", scopes: ["content:read"] },
|
|
43
|
+
"wordMaster.getWord": { path: "api/content/word-master/details", scopes: ["content:write"], write: true },
|
|
44
|
+
"wordMaster.generateEntry": { path: "api/content/word-master/generate-entry", scopes: ["content:write"], write: true },
|
|
45
|
+
"wordMaster.getEntry": { path: "api/content/word-master/entry-status", scopes: ["content:read"], write: true },
|
|
46
|
+
"wordMaster.encounterWords": { path: "api/content/word-master/encounter", scopes: ["content:write"], write: true },
|
|
47
|
+
"wordMaster.getWordBook": { path: "api/content/word-master/book", scopes: ["content:read"] },
|
|
48
|
+
"wordMaster.createQuiz": { path: "api/content/word-master/quiz", scopes: ["content:write"], write: true },
|
|
49
|
+
"wordMaster.getQuiz": { path: "api/content/word-master/quiz-status", scopes: ["content:read"], write: true },
|
|
50
|
+
"wordMaster.answerQuiz": { path: "api/content/word-master/answer", scopes: ["content:write"], write: true },
|
|
51
|
+
"wordMaster.getItems": { path: "api/content/word-master/items", scopes: ["content:read"] },
|
|
52
|
+
"wordMaster.useItem": { path: "api/content/word-master/use-item", scopes: ["content:write"], write: true },
|
|
36
53
|
"grammarbles.listQuestions": { path: "api/content/grammarbles/questions", scopes: ["content:read"] },
|
|
37
54
|
"grammarbles.getMyQuestionHistory": { path: "api/content/grammarbles/history", scopes: ["content:read"] },
|
|
38
55
|
"minecraft.getWorlds": { path: "api/minecraft/worlds", scopes: ["content:read"] },
|
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-10-
|
|
5
|
-
Generated: 2026-10-
|
|
3
|
+
Version: 1.67.0
|
|
4
|
+
Updated: 2026-10-16
|
|
5
|
+
Generated: 2026-10-16T15:37:18.409Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -36,7 +36,10 @@ Generated: 2026-10-12T17:14:00.482Z
|
|
|
36
36
|
- The creator's agent designs the rewards. Declare the economy in a project file `rewards.json` at the root: budgets (userDailyXP, userDailyCoins, optional userDailyClaims; there is no app-wide daily or lifetime budget, only what one learner can earn per day; an award larger than the learner's remaining room today pays what fits, XP and Coins clamped separately; a capped award still counts as claimed, including a one-time maxLifetimeClaims reward, and its receipt carries cappedFrom) and rules [{ id, title, howTo?, category? (section header, up to 40 characters), series? (key for repeat rules that share category and amounts), xp, coins, verifier: 'numeric-quiz' | 'completion' | 'generated-quiz', maxAttempts?, retry?: { xpPercent, coinsPercent, paidAttempts? }, minSeconds? (completion), progression?: 'dated' | 'until-earned' (quiz), bank? (generated-quiz): { steps: [percent, ...] rising to 100, startStep?, instructions, topics: [{ key, title, description }] } }]. generated-quiz rules need no question sheet. Wire the matching Twinkle.rewards calls with those literal rule ids. Questions and answer keys NEVER go in project files (published source is readable by every player): quiz rules get them from the private question sheet uploaded with `lumine rewards sheet <file.json>` ({ rules: { <ruleId>: { questions?, sets? } } }); `lumine rewards check` validates both together. A review request freezes the code and proposes rewards.json merged with the sheet; the administrator reads the code, checks the amounts and whether the app is exploitable, may change any amount, and approves. Creators are kids and teens: show approval status and one Send for review action; do not ask them to fill in technical forms. Every code update that retains rewards needs a new approval before publishing. Removing the SDK automatically clears its gate. Apps read amounts, tries and sets from getStatus, never from their own file.
|
|
37
37
|
- Verifiers: 'numeric-quiz' pays for server-checked numeric answers (retry share, attempt limits, dated sets or until-earned sets that stay up until somebody earns them, after-answer guides). 'completion' pays when the app reports an activity finished — a cleared stage, a finished round — at least minSeconds after start({ ruleId }); the server checks only the elapsed time, once per learner per site day (UTC midnight), and the budgets. Call start when the activity begins and claim({ challengeId }) with no answers when it ends; keep completion amounts and userDailyXP small enough that a player scripting the calls would not matter, because nothing else is verified. 'generated-quiz' pays from a question bank the server fills itself: for one declared topic at the learner's step of the ladder, the server writes a question with GPT-6.1 Sol (billed to that learner's AI Energy), solves it again independently and keeps it only when both answers agree; every learner who has not seen a question gets it before anything new is written. `xp`/`coins` are the top step's amounts and each step pays its `steps` percent of them (then the usual retry share). Two first-try answers in a row climb a step; two misses in a row step down. Learners may earn many bank questions a day, bounded by userDailyXP/userDailyCoins. The reviewer approves the ladder, amounts, topics and instructions, not each question; learner reports and solve rates hide bad questions. Use the rewards bank methods (getBankStatus, startBankQuestion, generateBankQuestion, claimBankQuestion, reportBankQuestion); start and claim refuse bank rules. 'archive-rematch' is the daily rematch: one retired until-earned puzzle of the app a day, chosen by the server (celebrated, high-rated solves first; never repeated within 60 days) and answered from its frozen approved sheet. Declare at most one, with no questions, sets, progression, series, minSeconds or completionProof, e.g. { "id": "rematch-daily", "title": "Daily rematch", "xp": 20000, "coins": 1000, "verifier": "archive-rematch", "retry": { "xpPercent": 50, "coinsPercent": 0, "paidAttempts": 3 }, "maxAttempts": null }. Only players who never met that puzzle (no official receipt, wrong answer or challenge while it was live, no Vault practice) may answer it; its receipt does not use userDailyClaims but counts toward userDailyXP/userDailyCoins. Use getRematch and claimRematch; start and claim refuse it.
|
|
38
38
|
- Numeric quiz answers are verified on the server; client scores, privateDb state, timers and completion booleans are not verified reward evidence. Limits reset at UTC midnight. Rules are earned once per viewer per UTC day; attempt limits and retry payouts come from the approved rule. Challenges expire at the UTC day boundary. Budgets apply across release changes.
|
|
39
|
-
- Optional reward rule controls: maxLifetimeClaims caps one learner’s receipts for that rule across every day and release (a receipt capped by the daily budget still counts, so a one-time reward claimed near the cap pays only what fits); completionProof: classic-tower-v1 requires a server-simulated Classic Tower finish in addition to minSeconds. These are server-enforced controls. Existing completion rules without completionProof still verify elapsed time only. Registered proof profiles also include breadface-v1, breadface-v2 and breadface-v3 (server-simulated Breadface inputs; each is one reviewed release's exact physics, chosen by the reviewer) study-record-v1 (a private study record reviewed by JEV, billed to the learner’s AI Energy), and Groove Lab's groove-lab-song-v1 (a song the learner published today, checked on the server for length, notes and originality) and groove-lab-heard-v1 (three established accounts finished the learner's songs today), groove-lab-listen-v1 (the learner listened to most of someone else's published song: start binds the song, the server clock covers three quarters of it up to two minutes, and the listener's own groove-lab-listens row for it was written today; each song pays a listener once a day across listening rules) and groove-lab-own-listen-v1 (the same for the learner's own song; own listens never count toward groove-lab-heard-v1), Ashen Vigil's vigil-guest-coplay-v1 (a new guest spent 10 minutes with the learner in a private world room, measured by the world relay; each guest pays once) and minecraft-first-link-v1 (the learner is the first Twinkle account ever linked to that Minecraft player; each player pays once), and writing-words-v1 with minWords (counted words the learner wrote today as chapters in their own books registered in the app's shared books topic; each chapter is read once by a small model for genuine writing and language, English words count fully and other languages half, and the same text counts once), reading-quiz-v1 (the learner passed a server-graded three-question quiz on a chapter of someone else's registered book after its reading time; each chapter pays a reader once, one attempt per chapter per day) and reading-readers-v1 with minReaders (that many established accounts passed a quiz on the learner's chapters today). Twinkle.craft apps also have craft-target-v1 (the learner's run of merges reaches today's target in this app's universe; every merge is a known pair and every result is one the learner made through Twinkle.craft.merge), craft-par-v1 (the same run reaches it in at most par merges) and craft-discovery-v1 (the oldest World First of the learner's that no rule has paid yet; each World First pays once ever; optional minWordLevel basic|elementary|intermediate|advanced|epic makes tiers: either every craft-discovery-v1 rule has one or none does; each World First pays under exactly one tier, the highest configured tier at or below its Word Master level, so a series per tier can pay rarer words more and the order of claims never matters; an item whose level is still pending pays under no tier yet). Profiles are platform-owned; an app cannot invent a verifier or authorize its own reward.
|
|
39
|
+
- Optional reward rule controls: maxLifetimeClaims caps one learner’s receipts for that rule across every day and release (a receipt capped by the daily budget still counts, so a one-time reward claimed near the cap pays only what fits); completionProof: classic-tower-v1 requires a server-simulated Classic Tower finish in addition to minSeconds. These are server-enforced controls. Existing completion rules without completionProof still verify elapsed time only. Registered proof profiles also include breadface-v1, breadface-v2 and breadface-v3 (server-simulated Breadface inputs; each is one reviewed release's exact physics, chosen by the reviewer) study-record-v1 (a private study record reviewed by JEV, billed to the learner’s AI Energy), and Groove Lab's groove-lab-song-v1 (a song the learner published today, checked on the server for length, notes and originality) and groove-lab-heard-v1 (three established accounts finished the learner's songs today), groove-lab-listen-v1 (the learner listened to most of someone else's published song: start binds the song, the server clock covers three quarters of it up to two minutes, and the listener's own groove-lab-listens row for it was written today; each song pays a listener once a day across listening rules) and groove-lab-own-listen-v1 (the same for the learner's own song; own listens never count toward groove-lab-heard-v1), Ashen Vigil's vigil-guest-coplay-v1 (a new guest spent 10 minutes with the learner in a private world room, measured by the world relay; each guest pays once) and minecraft-first-link-v1 (the learner is the first Twinkle account ever linked to that Minecraft player; each player pays once), and writing-words-v1 with minWords (counted words the learner wrote today as chapters in their own books registered in the app's shared books topic; each chapter is read once by a small model for genuine writing and language, English words count fully and other languages half, and the same text counts once), reading-quiz-v1 (the learner passed a server-graded three-question quiz on a chapter of someone else's registered book after its reading time; each chapter pays a reader once, one attempt per chapter per day) and reading-readers-v1 with minReaders (that many established accounts passed a quiz on the learner's chapters today). Twinkle.craft apps also have craft-target-v1 (the learner's run of merges reaches today's target in this app's universe; every merge is a known pair and every result is one the learner made through Twinkle.craft.merge), craft-par-v1 (the same run reaches it in at most par merges, with no Twinkle.craft.hint revealed that day) and craft-discovery-v1 (the oldest World First of the learner's that no rule has paid yet; each World First pays once ever; optional minWordLevel basic|elementary|intermediate|advanced|epic makes tiers: either every craft-discovery-v1 rule has one or none does; each World First pays under exactly one tier, the highest configured tier at or below its Word Master level, so a series per tier can pay rarer words more and the order of claims never matters; an item whose level is still pending pays under no tier yet). Profiles are platform-owned; an app cannot invent a verifier or authorize its own reward.
|
|
40
|
+
- word-game-quiz-v1 is a completion reward proof for a correctly answered, Battery-settled Twinkle.wordMaster quiz from today in the same app/viewer/runtime mode. Pass quiz.completionToken to rewards.claim; each quiz pays once across all rules. A series of identical rules allows a bounded number of distinct daily quizzes. Normal review and publication approval still apply; a contribution preview never pays real XP or Coins.
|
|
41
|
+
- word-game-entry-v1 verifies a Battery-settled missing-definition/example contribution made today in this app/runtime mode, paying each contribution once. Its platform cap is three claims per viewer/app/day. word-game-quiz-v1 is capped at five. Declare matching completion rules and normal reward review; these proofs do not approve the app themselves.
|
|
42
|
+
- word-game-puzzle-v1 preserves Word Search streak bonuses using canonical puzzle reward history: daily-easy/medium/hard with optional -s10 and -s20 tiers. A learner earns each difficulty once per UTC day across every tier and release. rewards.getStatus returns wordGamePuzzle { streakDays, ruleIds, earnedLevels }; choose its ruleIds and display canonical rule amounts. Streaks count consecutive days with a puzzle reward, capped at 30. This profile checks streak eligibility and duplicate difficulty claims in addition to the approved timer; it does not simulate or prove the puzzle solve.
|
|
40
43
|
|
|
41
44
|
## AI decision design
|
|
42
45
|
- 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.
|
|
@@ -718,6 +721,64 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
718
721
|
- This namespace is read-only and does not generate new AI Stories.
|
|
719
722
|
- Example: const { story } = await Twinkle.aiStories.get(storyId);
|
|
720
723
|
|
|
724
|
+
### Twinkle.wordMaster
|
|
725
|
+
- getReadyWords({ level, dayKey }) | scopes: none
|
|
726
|
+
- Returns: { dayKey, level, revision, catalogVersion, words, source } | null
|
|
727
|
+
- Read already-selected words synchronously, without a word request.
|
|
728
|
+
- Add <!--twinkle:word-game-ready--> inside the app HTML head. All five reviewed difficulty banks arrive in the normal HTML response, independently of creator code updates.
|
|
729
|
+
- Return the current UTC day as dayKey and easy, medium, hard, super or impossible as level. If null, use a bundled reviewed catalog immediately; never block play on fetching or preparing words. Pass the packet catalogVersion to encounterWords.
|
|
730
|
+
- async getDailyWords({ level }) | scopes: content:read
|
|
731
|
+
- Returns: { dayKey, level, revision, words: [{ id, word, wordLevel, definition }], source }
|
|
732
|
+
- Get today’s reviewed Word Master vocabulary for a word game.
|
|
733
|
+
- level: easy, medium, hard, super, impossible. Shared UTC daily sets are stable by revision. Words are checked against content_words_censored and reviewed before play. Missing daily rows use a checked catalog immediately; this endpoint never starts preparation or semantic review. Definitions may be empty until separately reviewed. Prefer getReadyWords plus a bundled catalog for zero word-loading requests.
|
|
734
|
+
- async encounterWords({ dayKey, level, catalogVersion, wordIds }) | scopes: content:write
|
|
735
|
+
- Returns: { addedWordIds }
|
|
736
|
+
- Add words encountered in today’s puzzle to this viewer’s private word book.
|
|
737
|
+
- Only IDs in today’s server word set are accepted. Up to 32 per request; repeated encounters do not duplicate entries.
|
|
738
|
+
- Pass catalogVersion from a ready packet or the reviewed bundled catalog. The server checks its exact day, level, revision and word IDs. Omitting it is supported only for legacy getDailyWords callers.
|
|
739
|
+
- async getWordBook({ cursor, query } = {}) | scopes: content:read
|
|
740
|
+
- Returns: { words: [{ id, word, definition, wordLevel, encounteredAt, correctCount, lastCorrectAt }], cursor }
|
|
741
|
+
- Read this viewer’s personal word book for this app.
|
|
742
|
+
- 40 words per page. Follow cursor until null. Reading never uses AI Battery.
|
|
743
|
+
- Optional query searches word text (up to 64 characters) across the book; keep the query unchanged when following cursor.
|
|
744
|
+
- async createQuiz({ wordId, itemType, requestId, useAiBattery, retry }) | scopes: content:write
|
|
745
|
+
- Returns: { quiz, aiUsagePolicy, mode }
|
|
746
|
+
- Generate one Word Master-style quiz using the viewer’s AI Battery.
|
|
747
|
+
- Show the Battery cost notice before calling; useAiBattery must be true. Use a stable 16–64 character requestId. itemType: clock (30 seconds), compass (first letter), trail (first three letters), freeze (20 seconds). Exactly one quiz per word per UTC day in each runtime mode. Ready/pending retries reuse it without generation or extra charging. Failed quizzes charge nothing; retry:true deliberately retries up to three total attempts. After a wrong answer, canAskAgain is true once: retry:true asks one new question about the same word, charged as a new Battery use, and a correct answer earns the item and quiz proof. Limit 60 generation attempts per viewer per day across apps. The word must already be in the book.
|
|
748
|
+
- async getQuiz({ quizId, requestId }) | scopes: content:read
|
|
749
|
+
- Returns: { quiz, aiUsagePolicy, mode }
|
|
750
|
+
- Recover a saved quiz after a timeout or reload without generating another.
|
|
751
|
+
- Pass quizId or requestId. A generated quiz finishes its one atomic Battery settlement here; ready quizzes never charge twice. This is a mutation for preview-capture and CLI permission purposes. Poll pending status with backoff. Failed status requires a deliberate retry. No answerIndex until answered.
|
|
752
|
+
- async answerQuiz({ quizId, choiceIndex }) | scopes: content:write
|
|
753
|
+
- Returns: { quiz, mode }
|
|
754
|
+
- Save the first answer and earn the chosen item when correct.
|
|
755
|
+
- One immutable answer, indexes 0–3. Duplicate/concurrent submissions return the canonical answer. After grading the correct answerIndex is returned for learning. A correct answer gives completionToken for word-game-quiz-v1 rewards; apps cannot mint items or rewards themselves.
|
|
756
|
+
- async getItems({ cursor } = {}) | scopes: content:read
|
|
757
|
+
- Returns: { items: [{ id, wordId, itemType, earnedAt, completionToken, claimedAt }], cursor, mode }
|
|
758
|
+
- List this viewer’s unspent, server-issued items.
|
|
759
|
+
- 100 per page. Follow cursor for more. Preview and live items are separate. Earned items do not expire when a day changes.
|
|
760
|
+
- async useItem({ itemId, requestId, puzzleKey }) | scopes: content:write
|
|
761
|
+
- Returns: { item: { id, itemType, usedAt, requestId, puzzleKey }, mode }
|
|
762
|
+
- Consume an earned item once and return a receipt for its game effect.
|
|
763
|
+
- puzzleKey starts items: and has at most 128 letters/digits/:|_-. Persist requestId and puzzleKey before sending. Retry the same IDs after a lost response; apply each receipt once and persist it with the session. Reuse on another puzzle is rejected. Item use is free of AI Battery.
|
|
764
|
+
- async getWord({ wordId, word }) | scopes: content:write
|
|
765
|
+
- Returns: { word: { id, word, definition, example, wordLevel, canGenerate }, mode }
|
|
766
|
+
- Open an existing Word Master word’s reviewed definition and example, and add it to the viewer’s book.
|
|
767
|
+
- 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.
|
|
768
|
+
- async generateEntry({ wordId, requestId, useAiBattery }) | scopes: content:write
|
|
769
|
+
- Returns: { entry?, word?, alreadyComplete?, aiUsagePolicy, mode }
|
|
770
|
+
- Use viewer AI Battery to fill a missing meaning/example once, with a server-verified contribution receipt.
|
|
771
|
+
- Show Battery disclosure first; useAiBattery:true is required. A stable 16–64 character requestId prevents duplicate charging. Only missing text is generated; existing text is preserved. One shared reservation per word/runtime mode prevents concurrent duplicate generations. Failed/rejected output charges no Battery. A known failed job may be deliberately retried using a new requestId; never do this after an unknown transport outcome. At most 20 entry generation attempts per viewer/day. Successful live entries are shared across apps; preview entries remain separate. entry.completionToken works with word-game-entry-v1.
|
|
772
|
+
- async getEntry({ requestId }) | scopes: content:read
|
|
773
|
+
- Returns: { entry, aiUsagePolicy, mode }
|
|
774
|
+
- Recover an interrupted word-entry request without regenerating it.
|
|
775
|
+
- Recovery can finish the one Battery settlement and shared-text write, so CLI calls require --allow-write and preview captures cannot call it. Pending jobs may be polled with backoff; failed jobs need an explicit retry.
|
|
776
|
+
- async reportIssue({ event }) | scopes: content:write
|
|
777
|
+
- Returns: { recorded: true }
|
|
778
|
+
- Report a fixed word-game failure signal for daily website management.
|
|
779
|
+
- Allowed events: bank-failed, details-failed, quiz-failed, quiz-recovery-failed, answer-failed, entry-failed, entry-recovery-failed, item-recovery-failed, reward-failed, storage-failed, placement-failed.
|
|
780
|
+
- Send once per failure category per minute; send no word text, answers, free-form errors or member/device identifiers. These client reports are symptoms, not verified server failures.
|
|
781
|
+
|
|
721
782
|
### Twinkle.grammarbles
|
|
722
783
|
- async listQuestions({ level, limit, cursor } = {}) | scopes: content:read
|
|
723
784
|
- Returns: { questions: [{ id, level, rating, question, choices, isChecked }], cursor?, pagination: { level, limit, hasMore, nextCursor } }
|
|
@@ -1029,9 +1090,9 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
1029
1090
|
- Example: const { level } = await Twinkle.minecraft.getPlayArea({ id: 'twinkle-keep' });
|
|
1030
1091
|
- async getPlayAreaChunks({ id, chunks: [[cx, cz], ...] }) | scopes: content:read
|
|
1031
1092
|
- Returns: { scenes: [{ cx, cz, scene: { origin: [x,y,z], size: [w,h,d], palette, blocks, light, biomes, count, ... } | null }] }
|
|
1032
|
-
- Up to
|
|
1093
|
+
- Up to 9 chunk columns of a play area in the /snap scene format (packed visible blocks with per-face light), cropped to the area's box. Stream the ones around the player.
|
|
1033
1094
|
- Same scene format as getSnap; only blocks with a visible face inside the area are included. scene is null for a chunk that is not part of the area.
|
|
1034
|
-
- At most
|
|
1095
|
+
- At most 9 chunks per call; reads share the 180-per-minute build read limit, so cache chunks and load the nearest first.
|
|
1035
1096
|
- Example: const { scenes } = await Twinkle.minecraft.getPlayAreaChunks({ id: level.id, chunks: [[64, 29], [65, 29]] });
|
|
1036
1097
|
- async createPlayArea({ name, world, box, spawn, id }) | scopes: content:write
|
|
1037
1098
|
- Returns: { level }
|
|
@@ -1517,7 +1578,7 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1517
1578
|
- Completion rules take no answers: claim({ challengeId }) when the activity is finished. build_reward_too_fast (HTTP 409) means fewer than minSeconds passed since start; show nothing and let play continue. In preview mode the receipt carries preview: true and nothing is paid.
|
|
1518
1579
|
- A completionProof rule also requires the signed completionToken from a successful rewards.progress response. The server simulates the registered game physics and must reach the goal. A timer, forged position, client win flag, altered inventory or token from another viewer, challenge or release cannot authorize payment. maxLifetimeClaims is enforced from receipts in the same award transaction; every receipt counts, including one capped by the daily budget (cappedFrom). build_reward_lifetime_claims_reached means all rewards for this rule have been collected; do not retry it.
|
|
1519
1580
|
- For study-record-v1, the server verifies the settled private review row and its viewer, app, exact release, challenge and day binding. It never trusts the client’s decision, requested award amount or AI answer. Each successful daily study record is claimable once; display only canonical receipt/balances. For groove-lab-song-v1, pass completionToken as the published song's sharedDb entry id; for groove-lab-heard-v1, pass none; for vigil-guest-coplay-v1, pass the guest's world guestId (from world events) or the co-play record id; for minecraft-first-link-v1, pass nothing or the Minecraft UUID to use; for reading-quiz-v1, pass completionToken as the chapter commentId and answers as the three chosen option indexes (all three must be right; a wrong attempt cannot be retried that day); for groove-lab-listen-v1 and groove-lab-own-listen-v1, pass the same song entry id after listenSeconds of foreground playback; for reading-readers-v1, pass nothing; for writing-words-v1, pass nothing (the server counts today's chapters itself and refuses with a plain reason, e.g. "320 of 500 counted words today" or "still being checked"). The server reads the app's own groove-lab-songs, groove-lab-song-parts and groove-lab-listens rows and refuses with build_reward_completion_proof_required and a plain reason when the song or listeners do not qualify.
|
|
1520
|
-
- For craft-target-v1 and craft-par-v1, pass completionToken as JSON.stringify({ v: 1, merges: [[a, b], ...] }): the item ids of the run's successful merges in order, starting from the starters (at most 300 merges, 32 KB). The server replays it against this app's known pairs, needs today's target (Twinkle.craft.getState().today) to appear, every result to be in the learner's collection, and at least one second per merge since start; it refuses an unknown pair, an item the run had not made yet, an over-par run (par rule) or a run on another day. For craft-discovery-v1, pass nothing: the server pays the oldest unpaid World First (receipt slot craft:<itemId>) and puts its name on the bounty story ("discovered <item>"). With tiers, a rule pays only World Firsts of its own tier (refused with a plain reason when the learner has none).
|
|
1581
|
+
- For craft-target-v1 and craft-par-v1, pass completionToken as JSON.stringify({ v: 1, merges: [[a, b], ...] }): the item ids of the run's successful merges in order, starting from the starters (at most 300 merges, 32 KB). The server replays it against this app's known pairs, needs today's target (Twinkle.craft.getState().today) to appear, every result to be in the learner's collection, and at least one second per merge since start; it refuses an unknown pair, an item the run had not made yet, an over-par run (par rule), a par claim on a day the learner revealed a Twinkle.craft.hint (the target rule still pays) or a run on another day. For craft-discovery-v1, pass nothing: the server pays the oldest unpaid World First (receipt slot craft:<itemId>) and puts its name on the bounty story ("discovered <item>"). With tiers, a rule pays only World Firsts of its own tier (refused with a plain reason when the learner has none).
|
|
1521
1582
|
- await Twinkle.rewards.getLeaderboard({ metric?: "xp" | "coins", period?: "day" | "week" | "all", limit? }) | scopes: rewards:claim
|
|
1522
1583
|
- Returns: { mode: "live", metric, period, limit, dayKey, from, available: { xp, coins }, entries: [{ rank, userId, username, profilePicUrl, xp, coins, claims, lastAt }], me: { rank, xp, coins, claims } | null } | { mode: "preview", metric, period, available, entries: [], me: null, message }
|
|
1523
1584
|
- Standings of who earned the most XP or Coins in THIS app, computed by Twinkle from its own receipts (never from anything the app submits). period 'day' is today (site day, UTC), 'week' the last 7 site days, 'all' (default) every day since approval. limit defaults to 20, max 100.
|
|
@@ -1679,6 +1740,15 @@ if (result.status === 'ok') showCard(result.item, result.why, result.worldFirst)
|
|
|
1679
1740
|
- await Twinkle.craft.journey({ day? }) | scopes: craft:read
|
|
1680
1741
|
- Returns: { day, journey: { target: item, par, steps: [{ a, b, result }] } | null, reason?: "no_target" | "reach_target_first" | "sign_in_required" }
|
|
1681
1742
|
- The server's par plan for a day (YYYY-MM-DD, default today), each step { id, name, emoji } items. For today it opens only after the player made the target today (reason reach_target_first before that); past days are open too, except a past day whose target is today's target (same rule as today).
|
|
1743
|
+
- await Twinkle.craft.hint({ peek? }) | scopes: craft:read
|
|
1744
|
+
- Returns: { day, hints: [{ item: { id, name, emoji }, a: { id, name, emoji }, b: { id, name, emoji } }], remaining: number, parForfeited: boolean, reason?: "no_target" | "sign_in_required" }
|
|
1745
|
+
- The hint ladder for Today's Star. Each call reveals ONE more recipe of today's par plan, top-down: first the target's own recipe (item = a + b), then the recipe of one of its ingredients, and so on down the plan (breadth first; starters have no recipe). It returns every recipe this player revealed today, in that order, plus how many are left (remaining 0: the whole plan is shown).
|
|
1746
|
+
- Any hint gives up today's par bonus: once the player revealed one, parForfeited is true and a craft-par-v1 claim is refused for the rest of the site day (UTC). The craft-target-v1 reward still pays. Tell the player before the first hint, and use { peek: true } to show the hints already revealed (after a reload) without revealing another.
|
|
1747
|
+
- Hints are kept per player per app per site day on the server and start over the next day. Only today's plan is ever revealed.
|
|
1748
|
+
- reason no_target: no Today's Star yet (hints []). Signed-out viewers get { day, hints: [], remaining: 0, parForfeited: false, reason: "sign_in_required" }; a hint asks nothing of them.
|
|
1749
|
+
- Example: const { hints, remaining, parForfeited } = await Twinkle.craft.hint();
|
|
1750
|
+
const latest = hints[hints.length - 1];
|
|
1751
|
+
showHint(`${latest.item.emoji} ${latest.item.name} = ${latest.a.name} + ${latest.b.name}`, remaining);
|
|
1682
1752
|
- await Twinkle.craft.rankings() | scopes: craft:read
|
|
1683
1753
|
- Returns: { allTime: { top: [{ rank, userId, username, profilePicUrl, worldFirsts }], me: { rank: number | null, worldFirsts } }, month: { month, top, me: { rank: number | null, worldFirsts } } }
|
|
1684
1754
|
- World First rankings in this app: the top 30 discoverers all time and this month (UTC), and the viewer's own rank (null with no World Firsts). The top lists may be up to a minute old. Signed-out viewers get empty top lists and me: { rank: null, worldFirsts: 0 }.
|
|
@@ -1738,6 +1808,32 @@ if (result.status === 'ok') showCard(result.item, result.why, result.worldFirst)
|
|
|
1738
1808
|
- Network owner only: dismiss a report, remove content, suspend an agent, or restore it.
|
|
1739
1809
|
- Lumine Network Build 2742 only. Replace UI state from confirmed server responses.
|
|
1740
1810
|
|
|
1811
|
+
### Twinkle.soundtracks
|
|
1812
|
+
- async searchSources({ query } = {}) | scopes: sharedDb:read
|
|
1813
|
+
- Returns: { sources: [{ kind, appId?, title, href, thumbnailUrl? }] }
|
|
1814
|
+
- Find a public app or Grammarbles Quest for a song tag.
|
|
1815
|
+
- Use a simple searchable picker. Send the returned kind/appId, never a title as an identifier.
|
|
1816
|
+
- async getTags({ entryIds }) | scopes: sharedDb:read
|
|
1817
|
+
- Returns: { tags: [{ id, entryId, status, source, reviewReason, revision, eventTimeMs, canRemove }] }
|
|
1818
|
+
- Read approved tags and the current creator’s requests for up to 50 songs.
|
|
1819
|
+
- Pending, declined, closed and revoked requests are visible only to their creator. Replace state only with the canonical response.
|
|
1820
|
+
- async requestTag({ entryId, source, note?, expectedUserId }) | scopes: sharedDb:write
|
|
1821
|
+
- Returns: { tag }
|
|
1822
|
+
- Ask Mikey to link your published song to an app.
|
|
1823
|
+
- source is { kind: "app", appId } or { kind: "grammarbles-quest" }. expectedUserId must be the current viewer.id.
|
|
1824
|
+
- Only the song’s creator can request a tag. Up to three active tags per song and ten pending requests per creator. Same pending/approved request retries are idempotent.
|
|
1825
|
+
- Uses the shared Build approvals queue and chat card. Nothing influences discovery until Mikey approves. Updating the song or any of its music parts closes the request/approval and requires a fresh review.
|
|
1826
|
+
- async removeTag({ tagId, expectedUserId }) | scopes: sharedDb:write
|
|
1827
|
+
- Returns: { tag }
|
|
1828
|
+
- Withdraw your own app tag, including a live approval.
|
|
1829
|
+
- Stops recommendations immediately. Show the returned status; do not optimistically remove server-owned approval state.
|
|
1830
|
+
- async getRecommendations({ limit } = {}) | scopes: sharedDb:read
|
|
1831
|
+
- Returns: { picks: [{ entry, source, reason }] }
|
|
1832
|
+
- Suggest approved soundtrack songs from apps the current viewer used.
|
|
1833
|
+
- limit is 1–12 (default 6). Guests and viewers without matching history get an empty list. Hide the shelf when empty.
|
|
1834
|
+
- Matches recorded public-app use and actual Grammarbles Quest answers. Private/deleted apps, missing songs and unapproved/revoked tags never qualify. Returns no activity history or raw progress.
|
|
1835
|
+
- Full Circle and Starlight music and covers stay locked for listeners until the current viewer reaches their Red Square milestones. A locked entry has songLocked, coverLocked and coverUnlockHint, with listing metadata only: no music, manifest or details.cover. Direct part reads follow the same gate. Disable playback, remix and queue actions and offer a link to /app/2. Creators retain editing access to their own songs.
|
|
1836
|
+
|
|
1741
1837
|
## Examples
|
|
1742
1838
|
|
|
1743
1839
|
### Daily reflection feed
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -1609,7 +1609,7 @@ lumine admin roles --min-level 3 # only the admin types' holders
|
|
|
1609
1609
|
lumine admin roles --min-level 0 --limit 200 --json
|
|
1610
1610
|
```
|
|
1611
1611
|
|
|
1612
|
-
### Twinkle.craft universes: seed, audit, revise (any time; added 2026-10-12)
|
|
1612
|
+
### Twinkle.craft universes: seed, audit, revise (any time; added 2026-10-12, revise --pair 2026-10-16)
|
|
1613
1613
|
|
|
1614
1614
|
An app using `Twinkle.craft` (Infinity Craft, build 1499) has its own
|
|
1615
1615
|
universe of items and pairs. Players' new pairs are answered by the AI on
|
|
@@ -1623,6 +1623,8 @@ lumine admin craft stats --build 1499 [--limit 50] # sizes, frontier, toda
|
|
|
1623
1623
|
lumine admin craft seed --build 1499 --file pairs.json # ALWAYS first: what would be added, skipped, rejected, warned
|
|
1624
1624
|
lumine admin craft seed --build 1499 --file pairs.json --confirm # only after reading every warning
|
|
1625
1625
|
lumine admin craft revise --build 1499 --item Obsidian --why "Lava that cools very fast turns into glassy rock." --confirm
|
|
1626
|
+
lumine admin craft revise --build 1499 --pair "Earth + Smoke" --why "Smoke rising from the earth can mean a volcano below." # dry run
|
|
1627
|
+
lumine admin craft revise --build 1499 --pair 4821 --why "<text>" --confirm # by pair id
|
|
1626
1628
|
```
|
|
1627
1629
|
|
|
1628
1630
|
- `pairs.json` is up to 500 pairs, in order: `[{ "a": "Water", "b": "Fire",
|
|
@@ -1657,6 +1659,15 @@ lumine admin craft revise --build 1499 --item Obsidian --why "Lava that cools ve
|
|
|
1657
1659
|
- `revise` changes an item's `--why` (the explanation on its card: the pair
|
|
1658
1660
|
that first made it), `--emoji` or `--category`, through the same checks. The
|
|
1659
1661
|
name never changes: players hold it. Audited as `craft.revise`.
|
|
1662
|
+
- `revise --pair` (added 2026-10-16) changes one pair's `--why`: the line a
|
|
1663
|
+
player sees when they make THAT pair (an item made by several pairs has one
|
|
1664
|
+
why per pair, and `--item` reaches only the first). The pair is its id (the
|
|
1665
|
+
`#n` in `stats`' newest AI answers) or `"A + B"` by item names, in either
|
|
1666
|
+
order. Only the why changes, never the pair's result; a fizzle has no why.
|
|
1667
|
+
Same checks as AI answers (one plain-text sentence, at most 140
|
|
1668
|
+
characters), dry run unless `--confirm`, audited as `craft.revise` with the
|
|
1669
|
+
pair in the audit metadata. `--pair` takes no `--item`, `--emoji` or
|
|
1670
|
+
`--category`.
|
|
1660
1671
|
- Expansion: `stats` shows the frontier (items no pair uses yet, and untried
|
|
1661
1672
|
pairs); seed pairs that use frontier items so Today's Star has fresh targets.
|
|
1662
1673
|
|
|
@@ -5348,3 +5359,8 @@ On the website, Zero and Ciel acting for an admin can make these staff decisions
|
|
|
5348
5359
|
- `lumine admin meetup set-branch <crewId> <userId> --branch <name> [--note <text>]` corrects a member's branch instead of a database write. `--branch` must be an official branch (a registry alias such as `Busan` resolves to its official name, `Haeundae`), a name still pending in the branch registry, or `"Not a Twinkle student"`. Former Twinkle students get the branch they went to. An official branch is verified at once; a pending name waits for its branch decision. The crew's progress, tier and staff alerts follow the new branch; approvals already given stay. The same branch again changes nothing. The private note and the change history are shown only to admins (`meetup show`, the crew page's staff panel); nobody is messaged. The command refuses members who left and crews that are not active.
|
|
5349
5360
|
|
|
5350
5361
|
Every decision requires Mikey's authorization for that crew and action. Mutations are audited and idempotent; `--output <path>` is an explicit request to retain the canonical JSON. General write receipts omit private crew details.
|
|
5362
|
+
|
|
5363
|
+
|
|
5364
|
+
## Word Search vocabulary upkeep
|
|
5365
|
+
|
|
5366
|
+
Every full daily website-management run adds reviewed Word Master words across all five difficulty bands, with accurate definitions and natural example sentences. Use `lumine admin word-games status`, `candidates <1-5> --after <id> --limit 100`, and `import <reviewed-words.json> --dry-run` followed by the reviewed import. File shape: `{items:[{wordId,word,wordLevel,definition,example}]}` (up to 100). Candidate exports are unreviewed; use agent judgment and check the current censor table before importing. No paid batch APIs. The import is audited and preserves active generation and today's existing puzzles; future banks include the additions. The app receives all five ready banks in its normal HTML and has a bundled checked fallback, so word selection never creates a loading screen or separate word request. Report additions per difficulty and text corrections. Repository agents follow `agent-guides/word-search-vocabulary.md` for the full procedure. A scoped management request does not start this full-run duty.
|