@stage5/lumine 0.3.36 → 0.3.38

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.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 cardcraft.`,
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
  ) {
@@ -3509,7 +3513,7 @@ export function parseAdminOperation(options) {
3509
3513
  // long as two GPT-6.1 Sol calls, so it gets its own timeout. The daily
3510
3514
  // website management run calls it in a bounded loop (Mikey, 2026-09-28).
3511
3515
  const usage =
3512
- "Usage: lumine admin reward-bank coverage <buildId> [--max-step 3] | seed <buildId> [--max-step 3] [--identity zero|ciel|auto] | report <buildId> [--days 1].";
3516
+ "Usage: lumine admin reward-bank coverage <buildId> [--max-step 3] | seed <buildId> [--max-step 3] [--identity zero|ciel|auto] | report <buildId> [--days 1] | question <buildId> --question-id <id> | repair <buildId> --file <repair.json> [--dry-run].";
3513
3517
  const buildId = parseRequiredInteger(
3514
3518
  target || options.adminBuild,
3515
3519
  "Build ID",
@@ -3519,6 +3523,59 @@ export function parseAdminOperation(options) {
3519
3523
  ? parseRequiredInteger(options.adminMaxStep, "--max-step", 1)
3520
3524
  : 3;
3521
3525
  if (maxStep > 10) throw cliValidationError("--max-step must be at most 10.");
3526
+ if (action === "question") {
3527
+ const questionId = parseRequiredInteger(
3528
+ options.adminQuestionId,
3529
+ "--question-id",
3530
+ 1,
3531
+ );
3532
+ return readOperation(
3533
+ "reward-bank.question",
3534
+ withQuery("/cli/admin/reward-bank/question", {
3535
+ buildId: String(buildId),
3536
+ questionId: String(questionId),
3537
+ }),
3538
+ { requiresRun: false },
3539
+ );
3540
+ }
3541
+ if (action === "repair") {
3542
+ const repair = readAdminJsonFile(
3543
+ options.adminFile,
3544
+ "--file <repair.json>",
3545
+ { maxBytes: 16 * 1024 },
3546
+ );
3547
+ const fields = ["questionId", "expectedFingerprint", "answer", "reason"];
3548
+ if (
3549
+ !repair ||
3550
+ typeof repair !== "object" ||
3551
+ Array.isArray(repair) ||
3552
+ Object.keys(repair).some((key) => !fields.includes(key)) ||
3553
+ !Number.isSafeInteger(repair.questionId) ||
3554
+ repair.questionId <= 0 ||
3555
+ typeof repair.answer !== "number" ||
3556
+ !Number.isFinite(repair.answer) ||
3557
+ typeof repair.expectedFingerprint !== "string" ||
3558
+ !/^[a-f0-9]{64}$/.test(repair.expectedFingerprint) ||
3559
+ typeof repair.reason !== "string" ||
3560
+ !repair.reason.trim() ||
3561
+ repair.reason.length > 2000
3562
+ ) {
3563
+ throw cliValidationError(
3564
+ "Repair JSON needs questionId, the inspected expectedFingerprint, a finite numeric answer and a reason (1–2000 characters); no other fields.",
3565
+ );
3566
+ }
3567
+ return writeOperation(
3568
+ "reward-bank.repair",
3569
+ "POST",
3570
+ "/cli/admin/reward-bank/repair",
3571
+ {
3572
+ buildId,
3573
+ repair,
3574
+ dryRun: Boolean(options.adminDryRun),
3575
+ },
3576
+ { requiresRun: false, mutates: !options.adminDryRun },
3577
+ );
3578
+ }
3522
3579
  if (action === "report") {
3523
3580
  const days = options.adminDays
3524
3581
  ? parseRequiredInteger(options.adminDays, "--days", 1)
@@ -7385,6 +7442,26 @@ function printAdminResult({ operation, result }) {
7385
7442
  }
7386
7443
  }
7387
7444
  if (operation.name.startsWith("reward-bank.")) {
7445
+ if (
7446
+ ["reward-bank.question", "reward-bank.repair"].includes(operation.name)
7447
+ ) {
7448
+ const question = data.question;
7449
+ console.log(
7450
+ `Question #${question.id} · Build ${question.buildId} · ${question.mode} · ${question.status}`,
7451
+ );
7452
+ console.log(question.prompt);
7453
+ console.log(`Stored answer: ${question.answer}`);
7454
+ if (question.hint) console.log(`Hint: ${question.hint}`);
7455
+ if (question.guide) console.log(`Guide: ${question.guide}`);
7456
+ console.log(`Fingerprint: ${data.fingerprint}`);
7457
+ console.log(`History: ${JSON.stringify(data.impact)}`);
7458
+ if (Object.hasOwn(data, "proposedAnswer"))
7459
+ console.log(
7460
+ `Proposed answer: ${data.proposedAnswer} · would change: ${data.wouldChange}`,
7461
+ );
7462
+ console.log(data.historyPolicy);
7463
+ return;
7464
+ }
7388
7465
  if (operation.name === "reward-bank.seed") {
7389
7466
  console.log(
7390
7467
  data.seeded
package/lib/commands.js CHANGED
@@ -2803,6 +2803,7 @@ export function parseArgs(args) {
2803
2803
  adminEditionId: raw.editionId ? String(raw.editionId) : "",
2804
2804
  adminLeaseToken: raw.leaseToken ? String(raw.leaseToken) : "",
2805
2805
  adminFile: raw.file ? String(raw.file) : "",
2806
+ adminQuestionId: raw.questionId ? String(raw.questionId) : "",
2806
2807
  rewardPreflight: Boolean(raw.preflight),
2807
2808
  rewardApprovalFile: raw.approvalFile ? String(raw.approvalFile) : "",
2808
2809
  // admin craft revise --item <id|name> [--why] [--emoji] [--category]
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"] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.3.36",
3
+ "version": "0.3.38",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1738,6 +1738,32 @@ if (result.status === 'ok') showCard(result.item, result.why, result.worldFirst)
1738
1738
  - Network owner only: dismiss a report, remove content, suspend an agent, or restore it.
1739
1739
  - Lumine Network Build 2742 only. Replace UI state from confirmed server responses.
1740
1740
 
1741
+ ### Twinkle.soundtracks
1742
+ - async searchSources({ query } = {}) | scopes: sharedDb:read
1743
+ - Returns: { sources: [{ kind, appId?, title, href, thumbnailUrl? }] }
1744
+ - Find a public app or Grammarbles Quest for a song tag.
1745
+ - Use a simple searchable picker. Send the returned kind/appId, never a title as an identifier.
1746
+ - async getTags({ entryIds }) | scopes: sharedDb:read
1747
+ - Returns: { tags: [{ id, entryId, status, source, reviewReason, revision, eventTimeMs, canRemove }] }
1748
+ - Read approved tags and the current creator’s requests for up to 50 songs.
1749
+ - Pending, declined, closed and revoked requests are visible only to their creator. Replace state only with the canonical response.
1750
+ - async requestTag({ entryId, source, note?, expectedUserId }) | scopes: sharedDb:write
1751
+ - Returns: { tag }
1752
+ - Ask Mikey to link your published song to an app.
1753
+ - source is { kind: "app", appId } or { kind: "grammarbles-quest" }. expectedUserId must be the current viewer.id.
1754
+ - 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.
1755
+ - 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.
1756
+ - async removeTag({ tagId, expectedUserId }) | scopes: sharedDb:write
1757
+ - Returns: { tag }
1758
+ - Withdraw your own app tag, including a live approval.
1759
+ - Stops recommendations immediately. Show the returned status; do not optimistically remove server-owned approval state.
1760
+ - async getRecommendations({ limit } = {}) | scopes: sharedDb:read
1761
+ - Returns: { picks: [{ entry, source, reason }] }
1762
+ - Suggest approved soundtrack songs from apps the current viewer used.
1763
+ - limit is 1–12 (default 6). Guests and viewers without matching history get an empty list. Hide the shelf when empty.
1764
+ - 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.
1765
+ - 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.
1766
+
1741
1767
  ## Examples
1742
1768
 
1743
1769
  ### Daily reflection feed
@@ -1714,8 +1714,32 @@ them so the first learner on a unit never waits or pays.
1714
1714
  lumine admin reward-bank coverage <buildId> [--max-step 3] [--json] # read-only, no run
1715
1715
  lumine admin reward-bank seed <buildId> [--max-step 3] [--json] # delegated full run
1716
1716
  lumine admin reward-bank report <buildId> [--days 1] [--json] # read-only telemetry
1717
+ lumine admin reward-bank question <buildId> --question-id <id> [--json]
1718
+ lumine admin reward-bank repair <buildId> --file <repair.json> --dry-run [--json]
1719
+ lumine admin reward-bank repair <buildId> --file <repair.json> [--json]
1717
1720
  ```
1718
1721
 
1722
+ `question` returns the complete prompt, answer, hint and guide, question
1723
+ statistics, a canonical `fingerprint`, and aggregate challenge history. It is
1724
+ owner-only and does not need a daily run. A report excerpt is not an exact
1725
+ question inspection.
1726
+
1727
+ `repair` accepts exactly `{ "questionId": 41, "expectedFingerprint": "<hash
1728
+ from inspection>", "answer": 0.5, "reason": "Verified explanation and arithmetic" }`.
1729
+ Inspect and verify the mathematics before preparing a repair. `--dry-run`
1730
+ checks the same guard without writing. The real command locks and rechecks the
1731
+ question, changes only its numeric answer, returns canonical readback and
1732
+ records an idempotent transaction audit. A stale fingerprint is rejected;
1733
+ inspect again instead of guessing a new hash. Unknown plan fields are rejected.
1734
+ No model call or AI Energy charge is involved.
1735
+
1736
+ The repaired answer governs subsequent attempts, including already-issued
1737
+ unsolved challenges. Past attempts, grades, ladders, question statistics and
1738
+ rewards stay unchanged; the receipt states this explicitly. The history counts
1739
+ are observations, not a lock over every learner's activity. If the old answer
1740
+ affected a learner, inspect that history and prepare a separate, exact remedy;
1741
+ this command never guesses compensating rewards or rewrites past grades.
1742
+
1719
1743
  `report` covers the last `--days` (1-31): generation rounds by source
1720
1744
  (learner or seed) with outcomes (`accepted`, `off_brief`: the checker judged
1721
1745
  the question outside the brief's language, level or LaTeX rule;