@stage5/lumine 0.3.49 → 0.3.51

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,7 @@
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,...] [--perf] [--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] [--wake] [--path <substring>] [--limit 300] [--json].";
8
8
 
9
9
  // --perf: only the performance events (tap-to-page timeline, slow API
10
10
  // requests, main-thread stalls, page load).
@@ -15,8 +15,14 @@ export const OWNER_TRACE_PERF_TYPES = [
15
15
  "page-load",
16
16
  "scroll-perf",
17
17
  "scroll-frame",
18
+ "wake",
18
19
  ];
19
20
 
21
+ // --wake: the session headers and the wake profiles (what the site did in the
22
+ // 20 s after each load or return to the tab), to compare Wi-Fi and cellular
23
+ // wakes (the server tags both with the network).
24
+ export const OWNER_TRACE_WAKE_TYPES = ["session", "wake"];
25
+
20
26
  const MAX_LIMIT = 2000;
21
27
 
22
28
  function validationError(message) {
@@ -61,6 +67,11 @@ export function buildOwnerTraceOperation(options) {
61
67
  if (!types.includes(type)) types.push(type);
62
68
  }
63
69
  }
70
+ if (options.adminWake) {
71
+ for (const type of OWNER_TRACE_WAKE_TYPES) {
72
+ if (!types.includes(type)) types.push(type);
73
+ }
74
+ }
64
75
  if (types.length) query.set("type", types.join(","));
65
76
  if (options.adminPath) query.set("path", String(options.adminPath));
66
77
  return {
@@ -103,6 +114,12 @@ function describeSession(data = {}, path = "") {
103
114
  data.mobile ? "mobile" : "",
104
115
  data.standalone ? "standalone (PWA)" : "browser tab",
105
116
  data.touch ? `touch ${data.touch}` : "",
117
+ data.navType === "back_forward"
118
+ ? "restored (iOS reloaded the discarded tab)"
119
+ : data.navType
120
+ ? `nav ${data.navType}`
121
+ : "",
122
+ describeNet(data.net),
106
123
  ].filter(Boolean);
107
124
  if (path) parts.push(`at ${path}`);
108
125
  return `${parts.join(" · ")}${data.ua ? `\n ua: ${data.ua}` : ""}`;
@@ -140,6 +157,8 @@ export function describeOwnerTraceEvent(event) {
140
157
  return describeScrollPerf(data);
141
158
  case "scroll-frame":
142
159
  return describeScrollFrame(data);
160
+ case "wake":
161
+ return describeWake(data);
143
162
  case "stall":
144
163
  return `main thread blocked ${data.ms}ms${data.n > 1 ? ` (${data.n} stalls, ${data.total}ms blocked over ${data.span}ms)` : ""}`;
145
164
  case "page-load": {
@@ -160,6 +179,60 @@ export function describeOwnerTraceEvent(event) {
160
179
  }
161
180
  }
162
181
 
182
+ // The network an upload came from: v4/v6 plus an opaque prefix hash. The same
183
+ // tag = the same network (home Wi-Fi vs the carrier).
184
+ function describeNet(net) {
185
+ if (!net?.id) return "";
186
+ return `net v${net.v}:${net.id}`;
187
+ }
188
+
189
+ // What the site did in the window after a load or a return to the tab.
190
+ // A field the server cut to "[cut N chars]" arrives as a string: every list or
191
+ // object is checked before use, so one trimmed row never breaks the output.
192
+ function describeWake(data) {
193
+ const head =
194
+ data.kind === "load"
195
+ ? `LOAD${data.navType === "back_forward" ? " (iOS restore)" : data.navType ? ` (${data.navType})` : ""}`
196
+ : data.kind === "resume"
197
+ ? `RESUME after ${formatDuration(data.awayMs)}`
198
+ : "WAKE (kind unknown)";
199
+ const socket = isPlainObject(data.socket) ? data.socket : {};
200
+ const reasons = Array.isArray(socket.reasons) ? socket.reasons : [];
201
+ const top = Array.isArray(data.top) ? data.top : [];
202
+ const work = Object.entries(isPlainObject(data.work) ? data.work : {})
203
+ .map(([name, n]) => `${name}${n > 1 ? ` x${n}` : ""}`)
204
+ .join(", ");
205
+ const parts = [
206
+ head,
207
+ describeNet(data.net),
208
+ `${formatDuration(data.windowMs)}${data.end && data.end !== "window" ? ` (ended: ${data.end})` : ""}`,
209
+ `frames ${data.frames ?? "?"}, ${data.over50 ?? 0} >50ms, ${data.over100 ?? 0} >100ms, max ${data.maxFrameMs ?? "?"}ms, jank ${data.jankMs ?? 0}ms`,
210
+ data.stalls ? `${data.stalls} stall(s) ${data.stallMs}ms` : "",
211
+ `${data.requests ?? 0} requests${data.repeats ? ` (${data.repeats} repeats)` : ""}`,
212
+ data.images ? `${data.images} images` : "",
213
+ `socket ${socket.atStart === true ? "up" : socket.atStart === false ? "down" : "?"} at start, ${socket.connects ?? 0} connect(s), ${socket.disconnects ?? 0} disconnect(s)${socket.attempts ? `, ${socket.attempts} attempt(s)` : ""}${reasons.length ? ` [${reasons.join(", ")}]` : ""}`,
214
+ work ? `work ${work}` : "",
215
+ top.length
216
+ ? `top ${top.join(", ")}${data.topCut ? ` +${data.topCut} more` : ""}`
217
+ : "",
218
+ data._trimmed ? `trimmed ${JSON.stringify(data._trimmed)}` : "",
219
+ ].filter(Boolean);
220
+ return parts.join(" · ");
221
+ }
222
+
223
+ function isPlainObject(value) {
224
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
225
+ }
226
+
227
+ function formatDuration(ms) {
228
+ const n = Number(ms);
229
+ if (!Number.isFinite(n)) return "?";
230
+ if (n < 1000) return `${Math.round(n)}ms`;
231
+ if (n < 120_000) return `${Math.round(n / 100) / 10}s`;
232
+ if (n < 7_200_000) return `${Math.round(n / 60_000)}m`;
233
+ return `${Math.round(n / 360_000) / 10}h`;
234
+ }
235
+
163
236
  // One Home/page scroll burst: frame pacing plus what was on screen.
164
237
  function describeScrollPerf(data) {
165
238
  const cards = (data.context?.cards || [])
package/lib/admin.js CHANGED
@@ -6135,7 +6135,7 @@ export function formatRewardRuleLine(rule) {
6135
6135
  return parts.join(" · ");
6136
6136
  }
6137
6137
 
6138
- function printRewardReviewResult({ operation, data }) {
6138
+ export function printRewardReviewResult({ operation, data }) {
6139
6139
  if (operation.name === "reward-review.list") {
6140
6140
  const reviews = Array.isArray(data.reviews) ? data.reviews : [];
6141
6141
  console.log(
@@ -6195,6 +6195,9 @@ function printRewardReviewResult({ operation, data }) {
6195
6195
  console.log("WARNING: the app's own reward check (rewards.verify.js) failed its self-test:");
6196
6196
  for (const finding of review.appCheckFindings) console.log(` - ${finding}`);
6197
6197
  }
6198
+ // rules only a timer checks: the same warning the creator saw before sending
6199
+ for (const warning of Array.isArray(review.timerOnlyWarnings) ? review.timerOnlyWarnings : [])
6200
+ console.log(`WARNING: ${warning}`);
6198
6201
  if (review.isLatest === false)
6199
6202
  console.log(
6200
6203
  "WARNING: a newer request exists for this app; decide on the latest one.",
package/lib/commands.js CHANGED
@@ -2469,6 +2469,7 @@ export function parseArgs(args) {
2469
2469
  "resume",
2470
2470
  "sinceRun",
2471
2471
  "perf",
2472
+ "wake",
2472
2473
  "includeLegacy",
2473
2474
  "includePrivateEvidence",
2474
2475
  "allFlagged",
@@ -2639,6 +2640,7 @@ export function parseArgs(args) {
2639
2640
  adminSince: raw.since ? String(raw.since) : "",
2640
2641
  adminPath: raw.path ? String(raw.path) : "",
2641
2642
  adminPerf: Boolean(raw.perf),
2643
+ adminWake: Boolean(raw.wake),
2642
2644
  adminMinLevel: raw.minLevel === undefined ? null : raw.minLevel,
2643
2645
  adminIncludeLegacy: Boolean(raw.includeLegacy),
2644
2646
  adminIncludePrivateEvidence: parseBoolean(
@@ -3312,7 +3314,7 @@ export function printHelp() {
3312
3314
  lumine admin reward-bank coverage <buildId> [--max-step 3] [--json]
3313
3315
  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)
3314
3316
  lumine admin reward-bank report <buildId> [--days 1] [--json] (question-bank telemetry: generation outcomes, learners, payouts, ladder, questions to review)
3315
- 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,scroll-perf,scroll-frame] [--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, scroll frame pacing and slow-frame attribution (Long Animation Frame scripts on Chrome, feed card renders on Safari); --perf shows only those; kept 30 days)
3317
+ 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,scroll-perf,scroll-frame] [--perf] [--wake] [--path <substring>] [--limit 300] [--json] (--wake: only sessions and the 20 s wake profiles after each load or return, with the network tag that separates Wi-Fi from cellular; 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, scroll frame pacing and slow-frame attribution (Long Animation Frame scripts on Chrome, feed card renders on Safari); --perf shows only those; kept 30 days)
3316
3318
  lumine admin roles [--min-level 1] [--limit 50] [--json] (every user type with its management level and who holds it; admin pages need level 3, achievement points never pass level 2)
3317
3319
  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]
3318
3320
  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]
package/lib/rewards.js CHANGED
@@ -68,7 +68,7 @@ async function checkDeclaration({
68
68
  });
69
69
  }
70
70
 
71
- function printDeclaration(result, { prefix = "" } = {}) {
71
+ export function printDeclaration(result, { prefix = "" } = {}) {
72
72
  const rules = Array.isArray(result?.rules) ? result.rules : [];
73
73
  if (result?.ok) {
74
74
  console.log(
@@ -87,6 +87,9 @@ function printDeclaration(result, { prefix = "" } = {}) {
87
87
  `${prefix} ${rule.id}: ${rule.title} · ${rule.xp} XP + ${rule.coins} Coins · ${what}`,
88
88
  );
89
89
  }
90
+ // rules only a timer checks (the server's warning; never blocks)
91
+ for (const warning of result.warnings || [])
92
+ console.log(`${prefix}WARNING: ${warning}`);
90
93
  if (result.nextStep) console.log(`${prefix}${result.nextStep}`);
91
94
  return;
92
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.3.49",
3
+ "version": "0.3.51",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {