@stage5/lumine 0.3.45 → 0.3.46
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-sliding-block.js +89 -0
- package/lib/admin.js +19 -0
- package/lib/commands.js +1 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +8 -7
- package/sdk/LUMINE_ADMIN.md +11 -3
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readAdminJsonFile } from "./admin-news.js";
|
|
2
|
+
|
|
3
|
+
// `lumine admin sliding-block status|add`: the sliding-block-v1 campaign
|
|
4
|
+
// (RushHour, build 2799), refilled Math Lab style by the daily website run.
|
|
5
|
+
// status shows how many levels each difficulty has; add appends boards made
|
|
6
|
+
// by twinkle-api/scripts/generate-sliding-block-boards.mjs after the
|
|
7
|
+
// campaign. The server solves every board again and skips duplicates; levels
|
|
8
|
+
// are never renumbered. add is a dry run unless --confirm is given.
|
|
9
|
+
|
|
10
|
+
export const SLIDING_BLOCK_USAGE =
|
|
11
|
+
"Usage: lumine admin sliding-block status | add --file <levels.json> [--confirm] [--json] (levels.json: [[minMoves, board], ...])";
|
|
12
|
+
const ADD_MAX = 100;
|
|
13
|
+
|
|
14
|
+
function validationError(message) {
|
|
15
|
+
const error = new Error(message);
|
|
16
|
+
error.code = "CLI_ADMIN_CLI_VALIDATION";
|
|
17
|
+
return error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function confirmed(options) {
|
|
21
|
+
const confirm = options.adminConfirm;
|
|
22
|
+
// absent is "", a bare --confirm is "true"
|
|
23
|
+
if (confirm !== "" && confirm !== "true") {
|
|
24
|
+
throw validationError("Pass --confirm on its own, without a value.");
|
|
25
|
+
}
|
|
26
|
+
return confirm === "true";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildSlidingBlockOperation({ action, options }) {
|
|
30
|
+
if (!action || action === "status") {
|
|
31
|
+
return {
|
|
32
|
+
name: "sliding-block.status",
|
|
33
|
+
method: "GET",
|
|
34
|
+
path: "/cli/admin/sliding-block/status",
|
|
35
|
+
body: undefined,
|
|
36
|
+
mutates: false,
|
|
37
|
+
requiresRun: false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (action === "add") {
|
|
41
|
+
const levels = readAdminJsonFile(options.adminFile, "--file <levels.json>", {
|
|
42
|
+
maxBytes: 1024 * 1024,
|
|
43
|
+
});
|
|
44
|
+
if (!Array.isArray(levels) || !levels.length || levels.length > ADD_MAX) {
|
|
45
|
+
throw validationError(
|
|
46
|
+
`The file must hold 1 to ${ADD_MAX} levels as [[minMoves, board], ...]; split a bigger refill.`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const confirm = confirmed(options);
|
|
50
|
+
return {
|
|
51
|
+
name: confirm ? "sliding-block.add" : "sliding-block.add.preview",
|
|
52
|
+
method: "POST",
|
|
53
|
+
path: "/cli/admin/sliding-block/add",
|
|
54
|
+
body: { levels, dryRun: !confirm },
|
|
55
|
+
mutates: confirm,
|
|
56
|
+
requiresRun: false,
|
|
57
|
+
timeoutMs: 120000,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
throw validationError(SLIDING_BLOCK_USAGE);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function formatSlidingBlockResult({ operation, data }) {
|
|
64
|
+
if (operation.name === "sliding-block.status") {
|
|
65
|
+
const moves = Object.entries(data?.byMoves || {})
|
|
66
|
+
.map(([m, n]) => `${m}:${n}`)
|
|
67
|
+
.join(" ");
|
|
68
|
+
return [
|
|
69
|
+
`Campaign: ${data?.levels} levels (${data?.baseLevels} base + ${data?.addedLevels} added)`,
|
|
70
|
+
`Levels by minimum moves: ${moves}`,
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
const added = data?.added || [];
|
|
74
|
+
const skipped = data?.skipped || [];
|
|
75
|
+
const lines = [
|
|
76
|
+
`${data?.dryRun ? "Dry run: would add" : "Added"} ${added.length} level(s)${
|
|
77
|
+
added.length ? ` (${added[0].index + 1}-${added[added.length - 1].index + 1})` : ""
|
|
78
|
+
}; campaign ${data?.dryRun ? "would have" : "now has"} ${data?.levels} levels.`,
|
|
79
|
+
];
|
|
80
|
+
if (skipped.length)
|
|
81
|
+
lines.push(
|
|
82
|
+
`Skipped ${skipped.length}: ${skipped
|
|
83
|
+
.slice(0, 10)
|
|
84
|
+
.map((s) => `#${s.position} ${s.reason}`)
|
|
85
|
+
.join("; ")}${skipped.length > 10 ? "; …" : ""}`,
|
|
86
|
+
);
|
|
87
|
+
if (data?.dryRun) lines.push("Nothing was written. Add --confirm to append them.");
|
|
88
|
+
return lines;
|
|
89
|
+
}
|
package/lib/admin.js
CHANGED
|
@@ -45,6 +45,10 @@ import {
|
|
|
45
45
|
} from "./admin-owner-trace.js";
|
|
46
46
|
import { buildRolesOperation, printRoles } from "./admin-roles.js";
|
|
47
47
|
import { buildCraftOperation, formatCraftResult } from "./admin-craft.js";
|
|
48
|
+
import {
|
|
49
|
+
buildSlidingBlockOperation,
|
|
50
|
+
formatSlidingBlockResult,
|
|
51
|
+
} from "./admin-sliding-block.js";
|
|
48
52
|
import { formatChatReportResult } from "./admin-chat-reports.js";
|
|
49
53
|
import { formatIdentityDeviceUsage } from "./admin-identity-inspection.js";
|
|
50
54
|
import {
|
|
@@ -2638,6 +2642,11 @@ export function parseAdminOperation(options) {
|
|
|
2638
2642
|
return buildCraftOperation({ action, options });
|
|
2639
2643
|
}
|
|
2640
2644
|
|
|
2645
|
+
// The sliding-block-v1 campaign refills (admin-sliding-block.js); no run.
|
|
2646
|
+
if (namespace === "sliding-block") {
|
|
2647
|
+
return buildSlidingBlockOperation({ action, options });
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2641
2650
|
if (namespace === "correction" || namespace === "corrections") {
|
|
2642
2651
|
if (!action || action === "status") {
|
|
2643
2652
|
return readOperation(
|
|
@@ -6173,6 +6182,11 @@ function printRewardReviewResult({ operation, data }) {
|
|
|
6173
6182
|
`Rule IDs found in source (heuristic): ${review.detectedRuleIds.join(", ") || "none"}`,
|
|
6174
6183
|
);
|
|
6175
6184
|
}
|
|
6185
|
+
// app-check-v1: the server's probe of the frozen /rewards.verify.js
|
|
6186
|
+
if (Array.isArray(review.appCheckFindings) && review.appCheckFindings.length) {
|
|
6187
|
+
console.log("WARNING: the app's own reward check (rewards.verify.js) failed its self-test:");
|
|
6188
|
+
for (const finding of review.appCheckFindings) console.log(` - ${finding}`);
|
|
6189
|
+
}
|
|
6176
6190
|
if (review.isLatest === false)
|
|
6177
6191
|
console.log(
|
|
6178
6192
|
"WARNING: a newer request exists for this app; decide on the latest one.",
|
|
@@ -7370,6 +7384,11 @@ function printAdminResult({ operation, result }) {
|
|
|
7370
7384
|
printRoles(data);
|
|
7371
7385
|
return;
|
|
7372
7386
|
}
|
|
7387
|
+
if (operation.name.startsWith("sliding-block.")) {
|
|
7388
|
+
for (const line of formatSlidingBlockResult({ operation, data }))
|
|
7389
|
+
console.log(line);
|
|
7390
|
+
return;
|
|
7391
|
+
}
|
|
7373
7392
|
if (operation.name.startsWith("craft.")) {
|
|
7374
7393
|
const lines = formatCraftResult({ operation, data });
|
|
7375
7394
|
if (lines) {
|
package/lib/commands.js
CHANGED
|
@@ -3260,6 +3260,7 @@ export function printHelp() {
|
|
|
3260
3260
|
lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] [--json]
|
|
3261
3261
|
lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
|
|
3262
3262
|
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)
|
|
3263
|
+
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)
|
|
3263
3264
|
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>]
|
|
3264
3265
|
lumine admin meetup list [--status review|active|completed|all] | show <crewId> | info <crewId> <userId> --decision ask|accept|ask-again|withdraw [--note <text>] | set-branch <crewId> <userId> --branch "<name>" [--note <text>] | branches [list [--status pending|official|rejected|all] | show <id> | approve <id> | reject <id> [--note <text>] | merge <id> --into <officialId>] | emails [list|send <id>|discard <id>|review on|off] | friends [status|on|off] | funnel [--days <1-90>] | announce-checkins [--send] [--as zero|ciel] (Bridge Builder crews and check-ins)
|
|
3265
3266
|
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/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Version: 1.68.0
|
|
4
4
|
Updated: 2026-10-16
|
|
5
|
-
Generated: 2026-10-
|
|
5
|
+
Generated: 2026-10-18T22:57:11.632Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -35,9 +35,9 @@ Generated: 2026-10-18T01:06:28.603Z
|
|
|
35
35
|
- Media Energy is separate from AI Energy. Replace Media Energy UI only from canonical mediaEnergy/getUsage responses; never decrement, reserve, or synthesize it in app code.
|
|
36
36
|
- Twinkle.rewards awards real XP and Coins only in the current approved published release. Drafts, local previews, private apps and superseded releases cannot earn. The server supplies a published-runtime grant; app code cannot choose a recipient or award amount.
|
|
37
37
|
- 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.
|
|
38
|
-
- 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
|
+
- 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. So a plain timer is only for activity nothing can check (time spent reading, a visit). When the reward is for a result the app could check (a solved puzzle, a cleared level, a won game, a right answer), add a completionProof: a registered profile below when one fits, otherwise app-check-v1, the app's own server-side check. '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.
|
|
39
39
|
- 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.
|
|
40
|
-
- 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).
|
|
40
|
+
- 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). sliding-block-v1 with minMoves (2 to 40) verifies a Rush Hour style 6x6 sliding-block puzzle the server deals: start returns completion { board: [[row, col, length, horizontal 0|1], ...] (car 0 is the red car on row 2), positions, minMoves, token, maxFrames }; show exactly that board, send each move as a frame [msSincePreviousMove, carIndex, newPosition] through progress (only real slides: a tap that moves nothing is refused), and claim once the red car has left on the right (position 4). Moves faster than 250 ms, impossible slides, or more time than has passed are refused; each challenge has one fixed board once started. Pass levelIndex to start to play a numbered campaign level instead of a random board: minMoves is then a floor, so an easy level never pays a harder rule. The campaign only grows and is never renumbered: the 857-level base (RushHour, build 2799, ships it as levels.js) plus levels Twinkle adds over time, and getStatus returns slidingBlock { levels, baseLevels, added: [[minMoves, board], ...] } for an app with such a rule so it can append the new ones after its base. Use it for any sliding-block, parking or 'unblock me' puzzle instead of a plain completion timer. app-check-v1 is the app's own server-side check, for any game or activity whose result can be checked that no registered profile covers (a nonogram, a maze, a word or number puzzle, a quiz with a hidden answer, a game whose moves can be replayed). Add /rewards.verify.js at the project root: plain JavaScript defining function deal(input) and function check(input), with no imports, no network and nothing but its input. deal({ ruleId, params }) returns { puzzle, secret }: the challenge's puzzle (sent to the app) and anything only the server should know (an answer, a hidden layout). Its Math.random is a server stream unique to the challenge, so the same challenge always deals the same puzzle and nobody can predict it from the public file; Date is frozen at 1970. check({ ruleId, params, puzzle, secret, record, elapsedSeconds }) returns { ok: true } or { ok: false, reason } (the reason is shown to the player). It must check the player's own record (the filled grid, the moves, the answer) against the dealt puzzle, never a client 'won' flag, and may require a human-paced elapsedSeconds. A refused record uses one of the challenge's tries like a wrong quiz answer (maxAttempts, default 3; retry shares apply), and any claim within two seconds after a refusal, right or wrong, gets build_reward_throttled (429) and should be sent again after the wait, and claim then returns { awarded: false, reason, attemptsRemaining }. The reason is shown to the player, so it must never reveal the answer or any part of the secret (say 'Not this time', not which cell is wrong). For a hidden-answer quiz, set maxAttempts (often 1) so guessing doesn't pay. Each call gets 250 ms and 16 MB; a record is at most 64 KB. The rule's checkParams (JSON, up to 2,000 characters, e.g. { "size": 10 }) reach both as params. `lumine rewards check` deals twice and offers check records that prove nothing (none, true, {}, "solved", the puzzle itself) and reports a check that accepts any; previews deal and check from the draft's file, so test there before sending for review. The other profiles are platform-owned. An app-check-v1 rule pays only after the reviewer approves that exact file with the rest of the code.
|
|
41
41
|
- 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.
|
|
42
42
|
- 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.
|
|
43
43
|
- 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.
|
|
@@ -1579,7 +1579,7 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1579
1579
|
- Errors: build_reward_not_scheduled when the rule has no questions for today; build_reward_daily_claims_reached when the viewer already earned today’s cap. attemptsRemaining is null for unlimited rules.
|
|
1580
1580
|
- For a completion rule call start when the activity begins (the moment the stage starts); the challenge's age is what the claim is measured against. In preview mode start also works for the owner (a stateless simulation).
|
|
1581
1581
|
- For completionProof: classic-tower-v1, start also returns completion { profile, token, maxFrames, completed, failed }. A new start resets only the simulated climb to its canonical spawn; it cannot reset daily or lifetime rewards. Record inputs from the first physics frame. The completion token is bound to the viewer, challenge, rule and published release.
|
|
1582
|
-
- For breadface-v1, breadface-v2 and breadface-v3, pass the zero-based canonical levelIndex. Record [dt, inputBits] from the first physics frame; start returns its server token and maxFrames. For study-record-v1, start returns completion { profile, usesAiEnergy: true }; there is no client-authored proof token. For groove-lab-song-v1, groove-lab-heard-v1, vigil-guest-coplay-v1, minecraft-first-link-v1 and writing-words-v1, start returns completion { profile }; there is no progress step. For groove-lab-listen-v1 and groove-lab-own-listen-v1, call start({ ruleId, completionToken: <published song entry id> }) when that song starts playing; it returns completion { profile, songEntryId, durationSeconds, listenSeconds } and refuses with a plain reason (not published, whose song, too short, already earned for this song today). Starting a different song re-binds the rule's unpaid challenge and restarts its clock. For reading-quiz-v1, call progress({ challengeId, completionToken: <chapter commentId>, record: 'open' }) when the reader opens the chapter (returns readyInSeconds, readingSeconds, alreadyEarned, attemptedToday), then progress({ ..., record: 'quiz' }) once readyInSeconds has passed to get questions [{ question, options[4] }] (never answers). getStatus returns reading { chaptersEarnedToday, readersToday } when the app uses a reading proof. For writing-words-v1, getStatus also returns writing { countedWords, checkingChapters, chapters: [{ commentId, words, countedWords, status: counted|not-counted|checking|duplicate|too-short, english, reason }] } for today.
|
|
1582
|
+
- For breadface-v1, breadface-v2 and breadface-v3, pass the zero-based canonical levelIndex. Record [dt, inputBits] from the first physics frame; start returns its server token and maxFrames. For study-record-v1, start returns completion { profile, usesAiEnergy: true }; there is no client-authored proof token. For groove-lab-song-v1, groove-lab-heard-v1, vigil-guest-coplay-v1, minecraft-first-link-v1 and writing-words-v1, start returns completion { profile }; there is no progress step. For groove-lab-listen-v1 and groove-lab-own-listen-v1, call start({ ruleId, completionToken: <published song entry id> }) when that song starts playing; it returns completion { profile, songEntryId, durationSeconds, listenSeconds } and refuses with a plain reason (not published, whose song, too short, already earned for this song today). Starting a different song re-binds the rule's unpaid challenge and restarts its clock. For reading-quiz-v1, call progress({ challengeId, completionToken: <chapter commentId>, record: 'open' }) when the reader opens the chapter (returns readyInSeconds, readingSeconds, alreadyEarned, attemptedToday), then progress({ ..., record: 'quiz' }) once readyInSeconds has passed to get questions [{ question, options[4] }] (never answers). getStatus returns reading { chaptersEarnedToday, readersToday } when the app uses a reading proof. For writing-words-v1, getStatus also returns writing { countedWords, checkingChapters, chapters: [{ commentId, words, countedWords, status: counted|not-counted|checking|duplicate|too-short, english, reason }] } for today. For sliding-block-v1, start deals the board (completion.board, positions, minMoves); render it, record each real slide as [ms, carIndex, newPosition], send moves with progress (frames up to maxFrames per call), then claim with the latest token. For app-check-v1, start returns completion { profile, puzzle }: show that puzzle (a repeat start of the same challenge returns the same one); there is no progress step.
|
|
1583
1583
|
- await Twinkle.rewards.progress({ challengeId, completionToken?, frames?, record?, requestId? }) | scopes: rewards:claim
|
|
1584
1584
|
- Returns: { mode: "live" | "preview", completion: { profile, token?, completed, failed?, decision?, message?, maxFrames? }, aiUsagePolicy? }
|
|
1585
1585
|
- Verify a bounded batch of inputs for an approved server-simulated climb.
|
|
@@ -1592,11 +1592,12 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1592
1592
|
- Returns: { awarded: false, attempts, attemptsRemaining, questions: [{ prompt, hint?, guide? }] } | { awarded: true, duplicate, receipt: { ruleId, xp, coins, attempt, firstTry, cappedFrom?: { xp, coins } }, questions: [{ prompt, hint?, guide? }], balances: { xp, coins } }
|
|
1593
1593
|
- Twinkle verifies every answer, approval, current published artifact and budget before atomically recording XP and Coins. The receipt’s xp/coins are what was actually paid: the full amounts on a first try, the retry share after a wrong answer (attempt > 1), clamped to the learner's remaining userDailyXP/userDailyCoins room today (each currency on its own). A clamped receipt carries cappedFrom: { xp, coins }, the amounts the rule would have paid; the rule still counts as earned today and the rest cannot be claimed later. build_reward_budget_reached (HTTP 409) means nothing would be paid: both rooms are used up, or the rule pays only a currency whose room is used up. This includes one-time rules: a capped award on a maxLifetimeClaims rule also pays only what fits and uses up that lifetime claim (e.g. a one-time 5,000 XP reward claimed at 9,999/10,000 XP pays 1 XP and is done). Use cappedFrom to explain it, e.g. "+1 XP (today's limit; this reward was 5,000 XP)". Always show receipt.xp/receipt.coins, never the rule's amounts, as what the learner got, and hide a zero part (a receipt can pay 0 XP and some Coins, or the reverse): show "+50 Coins", not "+0 XP · +50 Coins". Retry the same challengeId after a lost response; a confirmed claim returns its original receipt without another award. Never update balance UI optimistically. Under retry.paidAttempts a correct answer past that attempt returns awarded: true with a zero receipt: solved, not paid.
|
|
1594
1594
|
- Every claim response, wrong or right, returns the questions with their guides unlocked: show the teaching content right after the first answer. Answer keys are never returned.
|
|
1595
|
-
-
|
|
1595
|
+
- After a wrong answer, the next answer within two seconds, right or wrong, is refused with build_reward_throttled (HTTP 429): it does not count and says nothing about whether it was right. Keep the question open and let the person send it again after the wait; never treat a 429 as a wrong answer or retry-loop it.
|
|
1596
1596
|
- Optional subject says what the reward was for; it is shown on the public bounty story on Home while the app is public (e.g. "listened to Night Drive"). title is plain text up to 80 characters, path is this app's share-link segment (the one you pass to Twinkle.app.getShareUrl, letters, digits and dashes), imageUrl a Twinkle-hosted image. A malformed subject is refused with build_reward_subject_invalid (HTTP 400); an older server without subjects ignores it. Proof-backed rules that read the item on the server (Groove Lab songs) replace it with the stored title, link, cover and creator.
|
|
1597
1597
|
- 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.
|
|
1598
1598
|
- 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.
|
|
1599
1599
|
- 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.
|
|
1600
|
+
- For app-check-v1, pass completionToken as JSON.stringify(record), the record your check reads (at most 64 KB); the server deals the challenge again and runs the approved file's check before paying. A refused record returns { awarded: false, reason, attemptsRemaining } and uses a try; claim only when the app itself sees a finished solve.
|
|
1600
1601
|
- 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).
|
|
1601
1602
|
- await Twinkle.rewards.getLeaderboard({ metric?: "xp" | "coins", period?: "day" | "week" | "all", limit? }) | scopes: rewards:claim
|
|
1602
1603
|
- 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 }
|
|
@@ -1678,7 +1679,7 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1678
1679
|
- Returns: { mode: "live", awarded: false, correct: false, counted: true, challengeId, attempts, attemptsRemaining, nextReward: { xp, coins }, read?, practice, vault } | { mode: "live", awarded: true, duplicate: false, correct: true, counted: true, read?, receipt: { id, challengeId, ruleId, xp, coins, attempt, firstTry, createdAt, cappedFrom?: { xp, coins } }, balances: { xp, coins }, practice, vault } | { mode: "live", awarded: true, duplicate: true, receipt, balances } | { mode: "live", awarded: false, correct: false, counted: false, read: { understood: false, value: null } } | { mode: "preview", awarded: false, counted: false, message }
|
|
1679
1680
|
- Answer today's daily rematch. A correct answer writes a real receipt (ledger, budgets, Home feed story, leaderboard).
|
|
1680
1681
|
- Same answer check and tolerance as claim; text is read by the same answer reader as interpretAnswer (unreadable text uses no try). First try pays the rule's full amounts, paid retries the retry share, later correct answers pay nothing. One rematch receipt per player per site day; a repeat call after it returns the same receipt (duplicate: true).
|
|
1681
|
-
- Does not use the daily bounty count (userDailyClaims) but counts toward userDailyXP/userDailyCoins (clamped to the room left, with cappedFrom and build_reward_budget_reached as for claim; show receipt.xp/coins). Each answer also counts as the player's Vault answer for that puzzle (practice rating, Vault stats). Errors: build_reward_rematch_ineligible (409), build_reward_rematch_unavailable (404/409), build_reward_rematch_day_changed (409, reload getRematch), build_reward_throttled (429
|
|
1682
|
+
- Does not use the daily bounty count (userDailyClaims) but counts toward userDailyXP/userDailyCoins (clamped to the room left, with cappedFrom and build_reward_budget_reached as for claim; show receipt.xp/coins). Each answer also counts as the player's Vault answer for that puzzle (practice rating, Vault stats). Errors: build_reward_rematch_ineligible (409), build_reward_rematch_unavailable (404/409), build_reward_rematch_day_changed (409, reload getRematch), build_reward_throttled (429: any answer within two seconds of a wrong one; send it again after the wait).
|
|
1682
1683
|
- await Twinkle.rewards.getBankStatus({ ruleId }) | scopes: rewards:claim
|
|
1683
1684
|
- Returns: { mode, ruleId, dayKey, startStep, steps: [{ step, reward: { xp, coins } }], topics: [{ key, title, step, questionsAtStep }], today: { xp, coins, claims }, budgets: { userDailyXP, userDailyCoins } }
|
|
1684
1685
|
- Read a generated-quiz rule's ladder: what each step pays, the learner's step on every topic, and today's earnings.
|
|
@@ -1699,7 +1700,7 @@ world.updatePresence({ x, y, z, facing });
|
|
|
1699
1700
|
- await Twinkle.rewards.claimBankQuestion({ challengeId, answer: number }) | scopes: rewards:claim
|
|
1700
1701
|
- Returns: { correct, awarded, duplicate?, budgetReached?, receipt: { id, questionId, step, xp, coins, attempt, firstTry, cappedFrom?: { xp, coins } } | null, balances?, ladder?: { step, previousStep }, question: { ..., guide?, answer? }, attempts, attemptsRemaining, stats }
|
|
1701
1702
|
- Answer a bank question. The first answer moves the ladder; a correct answer pays the question's step share.
|
|
1702
|
-
- Send one number (convert a typed fraction yourself).
|
|
1703
|
+
- Send one number (convert a typed fraction yourself). After a wrong answer, any answer within two seconds, right or wrong, is refused with build_reward_throttled (429) and does not count; send it again after the wait.
|
|
1703
1704
|
- ladder appears on the learner's first answer to a question and gives their new step on that topic. Retrying a solved challengeId returns duplicate: true with the original receipt.
|
|
1704
1705
|
- A payment larger than the learner's remaining userDailyXP/userDailyCoins room today pays what fits (receipt.xp/coins, with cappedFrom: { xp, coins } the step share it would have paid); show the receipt amounts. budgetReached: true means the solve counted but nothing fit: today's room is used up. In preview mode nothing is paid and previewReward shows what the live app would pay.
|
|
1705
1706
|
- await Twinkle.rewards.reportBankQuestion({ questionId, reason }) | scopes: rewards:claim
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -642,8 +642,8 @@ final reporting. Every full daily run reports each grade's current question,
|
|
|
642
642
|
whether it was cleared today, uncleared published questions remaining, and
|
|
643
643
|
refill status. Count distinct cleared keys across all users and the app's full
|
|
644
644
|
history against the live approved sheet; the recent usage window and draft
|
|
645
|
-
additions are not the live inventory. At
|
|
646
|
-
refill to at least
|
|
645
|
+
additions are not the live inventory. At four or fewer remaining, prepare a
|
|
646
|
+
refill to at least twenty, with complete interactive guides, and track it until
|
|
647
647
|
approved publication. Report one or zero remaining prominently. Details are
|
|
648
648
|
in the linked guide's **Daily queue monitoring and refill** section.
|
|
649
649
|
Read this inventory with `lumine admin math-lab queue --json`. Its receipt
|
|
@@ -1409,7 +1409,15 @@ and answer keys from a private question sheet the creator's Lumine uploads with
|
|
|
1409
1409
|
readable by every player). **Send for review** (website, or `lumine rewards review` from the workspace) freezes the code and proposes
|
|
1410
1410
|
`rewards.json` merged with the sheet. Approval is Mikey's decision: read the
|
|
1411
1411
|
frozen code, check that the amounts are right and that the app cannot be
|
|
1412
|
-
farmed, change anything that is wrong, approve.
|
|
1412
|
+
farmed, change anything that is wrong, approve. A plain `completion` rule
|
|
1413
|
+
checks only time: when it pays for something the app could check (a puzzle,
|
|
1414
|
+
a level, a win), propose a completionProof instead of approving it. For an
|
|
1415
|
+
app's own check (`app-check-v1`), read `/rewards.verify.js`: `check` must test
|
|
1416
|
+
the player's record against the dealt puzzle, never a client flag, and its
|
|
1417
|
+
refusal `reason` (shown to players) must never reveal the answer; a hidden-answer
|
|
1418
|
+
quiz needs a small maxAttempts. `show`
|
|
1419
|
+
prints a WARNING when the server's self-test caught it paying a record that
|
|
1420
|
+
proves nothing. **Approval publishes** (since
|
|
1413
1421
|
2026-09-15): the exact frozen snapshot goes live in the same transaction, with
|
|
1414
1422
|
no Publish click by the creator; the app's previous release stays up until
|
|
1415
1423
|
that commit lands. Instead of approving, the reviewer may **propose changes**:
|