@stage5/lumine 0.3.16 → 0.3.17

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.
@@ -4,7 +4,16 @@
4
4
  // no daily run needed. The server keeps 30 days.
5
5
 
6
6
  export const OWNER_TRACE_USAGE =
7
- "Usage: lumine admin owner-trace [--since 2h|30m|3d|<iso>|<unix>] [--type route,feed-open,scroll-anchor,content-scroll,error,...] [--path <substring>] [--limit 300] [--json].";
7
+ "Usage: lumine admin owner-trace [--since 2h|30m|3d|<iso>|<unix>] [--type route,feed-open,scroll-anchor,content-scroll,error,...] [--perf] [--path <substring>] [--limit 300] [--json].";
8
+
9
+ // --perf: only the performance events (tap-to-page timeline, slow API
10
+ // requests, main-thread stalls, page load).
11
+ export const OWNER_TRACE_PERF_TYPES = [
12
+ "nav-timing",
13
+ "slow-request",
14
+ "stall",
15
+ "page-load",
16
+ ];
8
17
 
9
18
  const MAX_LIMIT = 2000;
10
19
 
@@ -41,7 +50,16 @@ export function buildOwnerTraceOperation(options) {
41
50
  const query = new URLSearchParams();
42
51
  query.set("since", normalizeSince(options.adminSince));
43
52
  query.set("limit", String(limit));
44
- if (options.adminType) query.set("type", String(options.adminType));
53
+ const types = String(options.adminType || "")
54
+ .split(",")
55
+ .map((type) => type.trim())
56
+ .filter(Boolean);
57
+ if (options.adminPerf) {
58
+ for (const type of OWNER_TRACE_PERF_TYPES) {
59
+ if (!types.includes(type)) types.push(type);
60
+ }
61
+ }
62
+ if (types.length) query.set("type", types.join(","));
45
63
  if (options.adminPath) query.set("path", String(options.adminPath));
46
64
  return {
47
65
  name: "owner-trace.read",
@@ -107,6 +125,27 @@ export function describeOwnerTraceEvent(event) {
107
125
  return `${data.e || "?"} ${pairs(data, ["e"])}`.trim();
108
126
  case "content-scroll":
109
127
  return `${data.phase} ${data.ct}:${data.id} scrollTop=${data.st} height=${data.sh} viewport=${data.vh}`;
128
+ case "nav-timing":
129
+ return describeNavTiming(data);
130
+ case "slow-request": {
131
+ const extra = [
132
+ data.q ? `queued ${data.q}ms` : "",
133
+ data.tries ? `${data.tries} tries` : "",
134
+ ].filter(Boolean);
135
+ return `${data.m || "?"} ${data.api || "?"} → ${data.status ?? "?"} in ${data.ms}ms${extra.length ? ` (${extra.join(", ")})` : ""}`;
136
+ }
137
+ case "stall":
138
+ return `main thread blocked ${data.ms}ms${data.n > 1 ? ` (${data.n} stalls, ${data.total}ms blocked over ${data.span}ms)` : ""}`;
139
+ case "page-load": {
140
+ const parts = [
141
+ data.type || "",
142
+ data.ttfb !== undefined ? `ttfb ${data.ttfb}` : "",
143
+ data.dcl !== undefined ? `domcontentloaded ${data.dcl}` : "",
144
+ data.load !== undefined ? `load ${data.load}` : "",
145
+ data.route !== undefined ? `first route ${data.route}` : "",
146
+ ].filter(Boolean);
147
+ return `${parts.join(" · ")} ms${data.cached ? " (document from cache)" : ""}`;
148
+ }
110
149
  case "error":
111
150
  case "rejection":
112
151
  return `${data.msg || "(no message)"}${data.src ? ` (${data.src}:${data.line}:${data.col})` : ""}${data.status ? ` HTTP ${data.status}` : ""}`;
@@ -115,6 +154,29 @@ export function describeOwnerTraceEvent(event) {
115
154
  }
116
155
  }
117
156
 
157
+ // "tap→page 4210ms · PUSH via nav · url +90 · gate +4180 (probe timeout,
158
+ // waited 4100ms) · commit +4210 · ready +4600": offsets are ms from the tap.
159
+ function describeNavTiming(data) {
160
+ const end = data.ready ?? data.commit;
161
+ const head =
162
+ end !== undefined
163
+ ? `${data.via === "url" || data.via === "pop" ? "start" : "tap"}→${data.ready !== undefined ? "ready" : "page"} ${end}ms`
164
+ : `cut after ${data.total}ms`;
165
+ const steps = [
166
+ data.url !== undefined ? `url +${data.url}` : "",
167
+ data.gate !== undefined
168
+ ? `gate +${data.gate}${data.probe ? ` (probe ${data.probe}${data.probeMs ? `, waited ${data.probeMs}ms` : ""})` : ""}`
169
+ : "",
170
+ data.commit !== undefined ? `commit +${data.commit}` : "",
171
+ data.ready !== undefined ? `ready +${data.ready}` : "",
172
+ ].filter(Boolean);
173
+ const tail = [
174
+ data.result ? `gate ${data.result}` : "",
175
+ data.cut ? `cut by ${data.cut}` : "",
176
+ ].filter(Boolean);
177
+ return [head, `${data.nav || "?"} via ${data.via || "?"}`, ...steps, ...tail].join(" · ");
178
+ }
179
+
118
180
  export function formatOwnerTrace(data) {
119
181
  const events = Array.isArray(data?.events) ? data.events : [];
120
182
  const lines = [];
package/lib/commands.js CHANGED
@@ -2381,6 +2381,7 @@ export function parseArgs(args) {
2381
2381
  "all",
2382
2382
  "resume",
2383
2383
  "sinceRun",
2384
+ "perf",
2384
2385
  "includeLegacy",
2385
2386
  "includePrivateEvidence",
2386
2387
  "reviewed",
@@ -2497,6 +2498,7 @@ export function parseArgs(args) {
2497
2498
  adminSinceRun: Boolean(raw.sinceRun),
2498
2499
  adminSince: raw.since ? String(raw.since) : "",
2499
2500
  adminPath: raw.path ? String(raw.path) : "",
2501
+ adminPerf: Boolean(raw.perf),
2500
2502
  adminIncludeLegacy: Boolean(raw.includeLegacy),
2501
2503
  adminIncludePrivateEvidence: parseBoolean(
2502
2504
  raw.includePrivateEvidence,
@@ -3081,7 +3083,7 @@ export function printHelp() {
3081
3083
  lumine admin reward-bank coverage <buildId> [--max-step 3] [--json]
3082
3084
  lumine admin reward-bank seed <buildId> [--max-step 3] [--identity zero|ciel|auto] [--json] (writes ONE missing seed question, lowest step first, billed to Zero/Ciel's AI Energy; inside a daily run or on its own, owner only)
3083
3085
  lumine admin reward-bank report <buildId> [--days 1] [--json] (question-bank telemetry: generation outcomes, learners, payouts, ladder, questions to review)
3084
- lumine admin owner-trace [--since 2h|30m|3d|<iso>] [--type route,feed-open,scroll-anchor,content-scroll,error,rejection,session] [--path <substring>] [--limit 300] [--json] (what the website did on Mikey's own devices: routes with PUSH/POP/REPLACE, Home feed card opens, scroll-anchor restores, content scroll positions, window errors; kept 30 days)
3086
+ lumine admin owner-trace [--since 2h|30m|3d|<iso>] [--type route,feed-open,scroll-anchor,content-scroll,error,rejection,session,nav-timing,slow-request,stall,page-load] [--perf] [--path <substring>] [--limit 300] [--json] (what the website did on Mikey's own devices: routes with PUSH/POP/REPLACE, Home feed card opens, scroll-anchor restores, content scroll positions, window errors, and performance: tap-to-page timing, API requests over 1 s, main-thread stalls over 200 ms, page load; --perf shows only those; kept 30 days)
3085
3087
  lumine admin comment draft <target> [--type subject|comment|build|aiStory|dailyReflection] [--file <comment.md>] [--review-receipt <review.json>|--reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--identity zero|ciel|auto] [--json]
3086
3088
  lumine admin comment reply comment:<id> [--file <reply.md>] [--reviewed-version <id> --reviewed-via runtime|code] [--review-context <context.json>] [--identity zero|ciel|auto] [--json]
3087
3089
  lumine admin comment post --draft-id <id> [--json]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,7 +2,7 @@
2
2
 
3
3
  Version: 1.63.0
4
4
  Updated: 2026-09-28
5
- Generated: 2026-10-05T14:12:15.800Z
5
+ Generated: 2026-10-06T10:12:09.371Z
6
6
 
7
7
  ## Notes
8
8
  - This SDK is injected into Build iframes via the Build preview/runtime.
@@ -1527,19 +1527,19 @@ world.updatePresence({ x, y, z, facing });
1527
1527
  - Requires an open challenge of the learner's own (build_reward_challenge_not_open, HTTP 409 otherwise). Limit: 120 characters (build_reward_answer_text_invalid, 400), rate limited to a burst of 4 then one read every 5 seconds (build_reward_throttled, 429). build_reward_answer_reader_unavailable (503) means the reader is down: ask the learner to type just the number.
1528
1528
  - Drafts and owner previews return mode 'preview' and need no open challenge.
1529
1529
  - await Twinkle.rewards.getChallengeBoard() | scopes: rewards:claim
1530
- - Returns: { mode: "live", dayKey, puzzles: [{ ruleId, title, setKey, rating, solvers, failers, failures, daysStanding, status: "untouched" | "undefeated" | "defeated" }], topSolvers: [{ rank, userId, username, profilePicUrl, rating, solves, hardestSolve: { ruleId, setKey, rating } | null }], recentSolves: [{ id, userId, username, ruleId, setKey, tier: "solved" | "first-solve" | "hard-won" | "legendary", attempt, firstSolve, failersBefore, failuresBefore, daysStanding, puzzleRating, at }], me: { rating, solves, rank } | null } | { mode: "preview", puzzles: [], topSolvers: [], recentSolves: [], me: null, message }
1530
+ - Returns: { mode: "live", dayKey, puzzles: [{ ruleId, title, setKey, rating, solvers, failers, failures?, daysStanding, status: "untouched" | "undefeated" | "defeated" }], topSolvers: [{ rank, userId, username, profilePicUrl, rating, solves, hardestSolve: { ruleId, setKey, rating } | null }], recentSolves: [{ id, userId, username, ruleId, setKey, tier: "solved" | "first-solve" | "hard-won" | "legendary", firstTry, firstSolve, failersBefore, daysStanding, puzzleRating, at }], me: { rating, solves, rank } | null } | { mode: "preview", puzzles: [], topSolvers: [], recentSolves: [], me: null, message }
1531
1531
  - Front-page difficulty board for until-earned quiz bounties: what is being tried and failed, who is rated highest, and the latest defeats.
1532
1532
  - Computed by Twinkle from its own receipts and attempts, never from anything the app submits. Puzzles are Elo-rated against players: a player's first miss on a puzzle is a win for the puzzle, a solve is a win for the player, and repeat misses by one player never stack.
1533
- - puzzles lists today's set for each until-earned rule. status 'undefeated' means someone tried and nobody has solved it yet; show it as a standing challenge. failers is how many different players missed it; failures is total wrong answers.
1534
- - tier on a recent solve says how loudly to celebrate: 'legendary' (first solve of a long-undefeated puzzle), 'hard-won', 'first-solve', or plain 'solved'. It is fixed at solve time, so it never changes later.
1533
+ - puzzles lists today's set for each until-earned rule. status 'undefeated' means someone tried and nobody has solved it yet; show it as a standing challenge. failers is how many different players missed it; failures is total wrong answers, sent only while the puzzle is still unsolved (absent on defeated puzzles, so a solver's own misses cannot be worked out).
1534
+ - tier on a recent solve says how loudly to celebrate: 'legendary' (first solve of a long-undefeated puzzle), 'hard-won', 'first-solve', or plain 'solved'. It is fixed at solve time, so it never changes later. firstTry is true when the solver got it on the first try; which try it took and the solver's own wrong answers are never shown to other players, so never reconstruct them.
1535
1535
  - No questions, answers, guides or private work are ever returned. Names are the same public names the XP leaderboard shows; other players who missed a puzzle are counted, never named. Drafts and previews return mode 'preview' with empty lists.
1536
1536
  - await Twinkle.rewards.getTimeline({ ruleId?, cursor?, limit? } = {}) | scopes: rewards:claim
1537
- - Returns: { mode: "live", dayKey, entries: [{ receiptId, ruleId, ruleTitle, setKey, title, promptPreview, dayKey, closedAt, solvedAt, solver: { userId, username }, firstSolver, xp, coins, attempt, story: { tier, attempt, failersBefore, failuresBefore, daysStanding } | null, puzzle: { rating, solvers, failers, failures } | null, vault: { rating, solvers, failers, failures } | null, rematchOpen }], nextCursor } | { mode: "preview", entries: [], nextCursor: null, message }
1537
+ - Returns: { mode: "live", dayKey, entries: [{ receiptId, ruleId, ruleTitle, setKey, title, promptPreview, dayKey, closedAt, solvedAt, solver: { userId, username }, firstSolver, xp, coins, firstTry, story: { tier, firstTry, failersBefore, daysStanding } | null, puzzle: { rating, solvers, failers } | null, vault: { rating, solvers, failers, failures } | null, rematchOpen }], nextCursor } | { mode: "preview", entries: [], nextCursor: null, message }
1538
1538
  - Browse confirmed solves of retired until-earned quiz bounties, newest first.
1539
1539
  - A solve appears only after the next site reset (UTC midnight, 9 AM Korea). Today’s solves, unsolved sets, standing quizzes and dated quizzes are excluded. The server decides retirement; client dates cannot unlock content. Zero-reward correct solves are included.
1540
1540
  - One entry per solve receipt, with firstSolver identifying the first receipt for that rule and set across approved versions. limit defaults to 20, max 50; pass nextCursor unchanged for older solves and omit it when changing ruleId. Pages may have fewer entries when old question sheets cannot be recovered; continue while nextCursor is present.
1541
1541
  - No daily-claim limit applies to reading. Requires the current approved published runtime grant; previews return an empty timeline. Never awards or changes balances. Open a receipt with getArchivedProblem to load the original question and guide.
1542
- - Museum fields: story is how THAT solve went, fixed at solve time (tier 'solved' | 'first-solve' | 'hard-won' | 'legendary', the solver's attempt, how many other players had missed it and how often, days it stood); null for receipts older than solve logging. puzzle is the official challenge rating and counts now; vault is the puzzle's living practice stats (its own rating, practice solvers, players whose first practice answer missed, all wrong practice answers), or null before anyone practised it. Counts only: players who missed are never named.
1542
+ - Museum fields: story is how THAT solve went, fixed at solve time (tier 'solved' | 'first-solve' | 'hard-won' | 'legendary', firstTry: whether the solver got it on the first try, never which try, how many other players had missed it, days it stood); null for receipts older than solve logging. puzzle is the official challenge rating and counts now (no total wrong answers: it is solved, and that total would give away the solver's own misses); vault is the puzzle's living practice stats (its own rating, practice solvers, players whose first practice answer missed, all wrong practice answers), or null before anyone practised it. Counts only: players who missed are never named.
1543
1543
  - rematchOpen is true on the entry that is today's daily rematch while the viewer can still answer it for pay: label it and open it as the rematch, not as a lesson. Daily-rematch receipts are never timeline entries.
1544
1544
  - await Twinkle.rewards.getArchivedProblem({ receiptId, withGuide?: true }) | scopes: rewards:claim
1545
1545
  - Returns: { mode: "live", entry: <same solve entry as getTimeline, with rematchOpen>, questions: [{ prompt, hint?, guide? }], rematchOpen } | { mode: "preview", entry: null, questions: [], message }
@@ -1378,6 +1378,7 @@ else's is a no-op and the upload route refuses them); the server keeps 30 days.
1378
1378
  lumine admin owner-trace --since 30m # readable timeline, oldest first
1379
1379
  lumine admin owner-trace --since 2026-10-06T09:00 --type route,feed-open
1380
1380
  lumine admin owner-trace --path /comments/123 --json # raw rows
1381
+ lumine admin owner-trace --since 1h --perf # performance events only
1381
1382
  ```
1382
1383
 
1383
1384
  Event types: `session` (version, viewport, platform, PWA or tab), `route`
@@ -1390,6 +1391,17 @@ later), `error`/`rejection` (short message and source file), `visibility`,
1390
1391
  `pagehide`, `trace-dropped`. Times are the phone's clock in this machine's
1391
1392
  time zone. Ids and paths only, never message text or other members' content.
1392
1393
 
1394
+ Performance events (`--perf` shows only these): `nav-timing` (one per
1395
+ navigation: ms from the tap to the URL change, the deploy-probe gate release
1396
+ with the probe's outcome current/armed/timeout/failed/throttled/backoff/pending
1397
+ and how long the tap waited for it, the route commit and, on content pages,
1398
+ content-ready; `cut by next-nav` means another navigation started before this
1399
+ one committed), `slow-request` (API requests over 1 s: method, path pattern
1400
+ with query keys only, status, ms, scheduler queue time), `stall` (main-thread
1401
+ gaps over 200 ms while the page is visible, bursts within 1 s coalesced) and
1402
+ `page-load` (once per load: TTFB, DOMContentLoaded, load, first route ready,
1403
+ ms since navigation start).
1404
+
1393
1405
  ### Reward question-bank seeding (every full daily review; added 2026-09-28)
1394
1406
 
1395
1407
  `generated-quiz` rules fill a question bank on the server: a question is