@stage5/lumine 0.3.68 → 0.3.70
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-arena.js +33 -4
- package/lib/admin-build-plans.js +8 -2
- package/lib/admin-daily-collection.js +88 -0
- package/lib/admin-daily.js +152 -25
- package/lib/admin-diagnostic-collection.js +106 -0
- package/lib/admin-duties.js +14 -1
- package/lib/admin-jev-review.js +76 -0
- package/lib/admin-owner-trace.js +11 -2
- package/lib/admin.js +65 -4
- package/lib/commands.js +14 -2
- package/lib/game-diagnostic-summary.js +204 -0
- package/package.json +1 -1
- package/sdk/LUMINE_ADMIN.md +49 -0
package/lib/admin-duties.js
CHANGED
|
@@ -28,6 +28,8 @@ export function createDailyReviewState(dir, gather, previous = {}) {
|
|
|
28
28
|
.map((step) => ({
|
|
29
29
|
...(step.ok ? fingerprint(dir, step.file) : { file: step.file }),
|
|
30
30
|
fetched: step.ok,
|
|
31
|
+
...(step.deferred ? { deferred: true, reason: step.reason } : {}),
|
|
32
|
+
...(step.capturedAt ? { capturedAt: step.capturedAt, reused: Boolean(step.reused) } : {}),
|
|
31
33
|
read: false,
|
|
32
34
|
}));
|
|
33
35
|
if (gather.botOutput.complete)
|
|
@@ -42,6 +44,14 @@ export function createDailyReviewState(dir, gather, previous = {}) {
|
|
|
42
44
|
if (gather.buildReviews && (
|
|
43
45
|
source.file === "rr-pending.json" || /^build-reviews-\d+\.json$/.test(source.file)
|
|
44
46
|
)) continue;
|
|
47
|
+
if (gather.paperQueue && /^arena-paper-queue-\d+\.json$/.test(source.file)) continue;
|
|
48
|
+
if (gather.personalQueue && /^arena-today-queue-\d+\.json$/.test(source.file)) continue;
|
|
49
|
+
if (gather.rewardDiagnostics && /^reward-diagnostics(?:-\d+|-summary)?\.json$/.test(source.file)) continue;
|
|
50
|
+
if (gather.jevReview && /^jev-review(?:-\d+|-summary)?\.json$/.test(source.file)) continue;
|
|
51
|
+
if (gather.ownerWake?.collection && /^owner-wake(?:-sessions)?(?:-\d+)?\.json$/.test(source.file)) continue;
|
|
52
|
+
if (gather.botOutput && (
|
|
53
|
+
source.file === "bot-chats.txt" || /^bot-output-\d+\.json$/.test(source.file)
|
|
54
|
+
)) continue;
|
|
45
55
|
if (!sources.some((current) => current.file === source.file)) sources.push({ ...source, retained: true, read: false });
|
|
46
56
|
}
|
|
47
57
|
return {
|
|
@@ -49,6 +59,7 @@ export function createDailyReviewState(dir, gather, previous = {}) {
|
|
|
49
59
|
scope: previous.scope || "collection",
|
|
50
60
|
sources,
|
|
51
61
|
collectionFailures: gather.failed || [],
|
|
62
|
+
collectionDeferred: gather.deferred || [],
|
|
52
63
|
requiredDuties: [...new Set(["effort-levels", "report-browser", ...(previous.requiredDuties || [])])],
|
|
53
64
|
note: "Collection is not a full review. Add every duty required by the run scope and its carryovers. Reading and browser checks require explicit evidence acknowledgments.",
|
|
54
65
|
};
|
|
@@ -157,8 +168,9 @@ export function summarizeDailyProgress(dir, state, evidence = {}) {
|
|
|
157
168
|
.filter((item) => item.status !== "completed")
|
|
158
169
|
.map((item) => item.id);
|
|
159
170
|
const collectionFailures = state.collectionFailures || [];
|
|
171
|
+
const collectionDeferred = state.collectionDeferred || [];
|
|
160
172
|
const complete =
|
|
161
|
-
collectionFailures.length === 0 && sources.every((item) => item.read) && incomplete.length === 0;
|
|
173
|
+
collectionFailures.length === 0 && collectionDeferred.length === 0 && sources.every((item) => item.read) && incomplete.length === 0;
|
|
162
174
|
return {
|
|
163
175
|
scope: state.scope || "declared-duties",
|
|
164
176
|
coverageBasis: "declared sources and duties only",
|
|
@@ -166,6 +178,7 @@ export function summarizeDailyProgress(dir, state, evidence = {}) {
|
|
|
166
178
|
effortAssignments,
|
|
167
179
|
sources,
|
|
168
180
|
collectionFailures,
|
|
181
|
+
collectionDeferred,
|
|
169
182
|
duties,
|
|
170
183
|
complete,
|
|
171
184
|
reading: {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Bounded owner-only frozen evidence. Collection never acknowledges reading
|
|
2
|
+
// and never reconstructs missing input from the member's edited chat history.
|
|
3
|
+
export const JEV_REVIEW_USAGE =
|
|
4
|
+
"Usage: lumine admin jev-routing review --since <unix|ISO> --until <unix|ISO> [--limit 1..20] [--cursor <cursor>] [--include-synthetic] [--all] [--json].";
|
|
5
|
+
|
|
6
|
+
function invalid(message) {
|
|
7
|
+
const error = new Error(message);
|
|
8
|
+
error.code = "CLI_ADMIN_CLI_VALIDATION";
|
|
9
|
+
throw error;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function explicitTimestamp(value, name) {
|
|
13
|
+
if (typeof value !== "string" && typeof value !== "number") invalid(JEV_REVIEW_USAGE);
|
|
14
|
+
const text = String(value).trim();
|
|
15
|
+
if (!/^\d+$/.test(text) && !/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2}))?$/.test(text)) {
|
|
16
|
+
invalid(`--${name} requires an explicit Unix timestamp or ISO date, not a relative window.`);
|
|
17
|
+
}
|
|
18
|
+
const seconds = /^\d+$/.test(text) ? Number(text) : Math.floor(Date.parse(text) / 1000);
|
|
19
|
+
if (!Number.isSafeInteger(seconds) || seconds <= 0) invalid(`Invalid --${name} timestamp.`);
|
|
20
|
+
// Keep ISO input intact for the API's strict calendar/date-zone validation.
|
|
21
|
+
return { text, seconds };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildJevRoutingReviewOperation({ action, target, extra, options = {} }) {
|
|
25
|
+
if (action !== "review" || target || extra) invalid(JEV_REVIEW_USAGE);
|
|
26
|
+
if (options.adminAll && options.adminCursor) {
|
|
27
|
+
invalid("Use --all from the beginning of the window, or --resume with its saved checkpoint; use --cursor only for an individual page.");
|
|
28
|
+
}
|
|
29
|
+
const since = explicitTimestamp(options.adminSince, "since");
|
|
30
|
+
const until = explicitTimestamp(options.adminUntil, "until");
|
|
31
|
+
if (until.seconds <= since.seconds || until.seconds - since.seconds > 8 * 86400) {
|
|
32
|
+
invalid("Use a fixed window with since < until, at most eight days.");
|
|
33
|
+
}
|
|
34
|
+
// The global parser's `limit` defaults to 50 for unrelated commands.
|
|
35
|
+
// `adminLimit` records only an explicitly supplied --limit.
|
|
36
|
+
const rawLimit = options.adminLimit;
|
|
37
|
+
const limit = rawLimit == null || rawLimit === "" ? 20 : Number(rawLimit);
|
|
38
|
+
if ((rawLimit != null && !["string", "number"].includes(typeof rawLimit)) ||
|
|
39
|
+
!Number.isSafeInteger(limit) || limit < 1 || limit > 20) invalid("--limit must be 1–20.");
|
|
40
|
+
if (options.adminIncludeSynthetic !== undefined && typeof options.adminIncludeSynthetic !== "boolean") {
|
|
41
|
+
invalid("--include-synthetic is a flag without a value.");
|
|
42
|
+
}
|
|
43
|
+
const includeSynthetic = options.adminIncludeSynthetic === true;
|
|
44
|
+
const query = new URLSearchParams({ since: since.text, until: until.text, limit: String(limit) });
|
|
45
|
+
if (includeSynthetic) query.set("includeSynthetic", "true");
|
|
46
|
+
if (options.adminCursor) query.set("cursor", String(options.adminCursor));
|
|
47
|
+
return {
|
|
48
|
+
name: "jevRouting.review",
|
|
49
|
+
method: "GET",
|
|
50
|
+
path: `/cli/admin/jev-routing/review?${query.toString()}`,
|
|
51
|
+
body: undefined,
|
|
52
|
+
mutates: false,
|
|
53
|
+
requiresRun: false,
|
|
54
|
+
pagination: {
|
|
55
|
+
collectionKey: "items",
|
|
56
|
+
after: since.seconds,
|
|
57
|
+
summaryKeys: ["collectionWindow", "coverage", "notes"],
|
|
58
|
+
filters: { since: since.text, until: until.text, includeSynthetic },
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function formatJevRoutingReview(data = {}) {
|
|
64
|
+
const window = data.collectionWindow || {};
|
|
65
|
+
const coverage = data.coverage || {};
|
|
66
|
+
const lines = [
|
|
67
|
+
`JEV review: [${window.since ?? "?"}, ${window.until ?? "?"}) · synthetic ${window.includeSynthetic ? "included" : "excluded"}`,
|
|
68
|
+
`${data.items?.length || 0} events returned · page ${coverage.status || "coverage unknown"}${coverage.fromBeginning === false ? " · resumed after a cursor" : ""}`,
|
|
69
|
+
];
|
|
70
|
+
for (const item of data.items || []) {
|
|
71
|
+
lines.push(`#${item.eventId} ${item.route} · ${(item.reviewReasons || []).join(", ")} · selected ${item.selectedSource || "not recorded"} · frozen input ${item.contextAvailable ? "available" : item.contextUnavailableReason || "unavailable"}`);
|
|
72
|
+
}
|
|
73
|
+
if (data.nextCursor) lines.push(`Next cursor: ${data.nextCursor}`);
|
|
74
|
+
lines.push("Collection is not review. Read the JSON source pages; an exhausted resumed page does not prove the whole window was collected.");
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
package/lib/admin-owner-trace.js
CHANGED
|
@@ -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] [--wake] [--path <substring>] [--limit 300] [--json].";
|
|
7
|
+
"Usage: lumine admin owner-trace [--since 2h|30m|3d|<iso>|<unix>] [--until <unix-second>] [--cursor <time:id>] [--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).
|
|
@@ -59,6 +59,15 @@ export function buildOwnerTraceOperation(options) {
|
|
|
59
59
|
const query = new URLSearchParams();
|
|
60
60
|
query.set("since", normalizeSince(options.adminSince));
|
|
61
61
|
query.set("limit", String(limit));
|
|
62
|
+
if (options.adminUntil) {
|
|
63
|
+
const until = Number(options.adminUntil);
|
|
64
|
+
if (!Number.isSafeInteger(until) || until <= 0) throw validationError("--until must be a positive unix second.");
|
|
65
|
+
query.set("until", String(until));
|
|
66
|
+
}
|
|
67
|
+
if (options.adminCursor) {
|
|
68
|
+
if (!/^\d+:\d+$/.test(options.adminCursor)) throw validationError("--cursor must be a returned time:id pair.");
|
|
69
|
+
query.set("cursor", options.adminCursor);
|
|
70
|
+
}
|
|
62
71
|
const types = String(options.adminType || "")
|
|
63
72
|
.split(",")
|
|
64
73
|
.map((type) => type.trim())
|
|
@@ -333,7 +342,7 @@ export function formatOwnerTrace(data) {
|
|
|
333
342
|
const sessions = new Set(events.map((event) => event.sessionId));
|
|
334
343
|
const since = Number(data?.since || 0);
|
|
335
344
|
lines.push(
|
|
336
|
-
`Owner trace since ${since ? `${formatDay(since * 1000)} ${formatClock(since * 1000).slice(0, 5)}` : "?"} · ${events.length} event(s) · ${sessions.size} session(s)${data?.truncated ?
|
|
345
|
+
`Owner trace since ${since ? `${formatDay(since * 1000)} ${formatClock(since * 1000).slice(0, 5)}` : "?"} · ${events.length} event(s) · ${sessions.size} session(s)${data?.truncated ? ` · more pages (keep --since/--until; --cursor ${data.nextCursor || "unavailable"})` : ""}`,
|
|
337
346
|
);
|
|
338
347
|
if (!events.length) {
|
|
339
348
|
lines.push("Nothing recorded in this window.");
|
package/lib/admin.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
readdirSync,
|
|
8
8
|
readFileSync,
|
|
9
9
|
realpathSync,
|
|
10
|
+
statSync,
|
|
10
11
|
writeFileSync,
|
|
11
12
|
} from "node:fs";
|
|
12
13
|
import path from "node:path";
|
|
@@ -48,6 +49,8 @@ import { buildSpendingOperation, completeSpendingReport, formatSpending } from "
|
|
|
48
49
|
import { buildCraftOperation, formatCraftResult } from "./admin-craft.js";
|
|
49
50
|
import { buildNonogramOperation, formatNonogramResult } from './admin-nonogram.js';
|
|
50
51
|
import { buildArenaOperation, formatArenaResult } from "./admin-arena.js";
|
|
52
|
+
import { buildJevRoutingReviewOperation, formatJevRoutingReview } from "./admin-jev-review.js";
|
|
53
|
+
import { summarizeGameDiagnostics } from "./game-diagnostic-summary.js";
|
|
51
54
|
import {
|
|
52
55
|
buildSlidingBlockOperation,
|
|
53
56
|
formatSlidingBlockResult,
|
|
@@ -1314,6 +1317,32 @@ function readEditorialFile(filePath) {
|
|
|
1314
1317
|
}
|
|
1315
1318
|
}
|
|
1316
1319
|
|
|
1320
|
+
function gameDiagnosticSummary(options) {
|
|
1321
|
+
const filePath = String(options.adminFile || "").trim();
|
|
1322
|
+
const outputPath = options.adminOutput ? String(options.adminOutput).trim() : null;
|
|
1323
|
+
if (options.positional?.length !== 2 || !filePath || outputPath === "") {
|
|
1324
|
+
throw cliValidationError("Usage: lumine admin game-diagnostics summarize --file <envelope.json> [--json]. This reads retained evidence locally.");
|
|
1325
|
+
}
|
|
1326
|
+
const maxBytes = 16 * 1024 * 1024;
|
|
1327
|
+
let metadata;
|
|
1328
|
+
try { metadata = statSync(filePath); }
|
|
1329
|
+
catch { throw cliValidationError("Could not read the game diagnostic envelope."); }
|
|
1330
|
+
if (!metadata.isFile() || metadata.size > maxBytes) {
|
|
1331
|
+
throw cliValidationError("The game diagnostic envelope must be a regular file under 16 MB.");
|
|
1332
|
+
}
|
|
1333
|
+
if (outputPath) {
|
|
1334
|
+
let outputMetadata;
|
|
1335
|
+
try { outputMetadata = statSync(outputPath); }
|
|
1336
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
1337
|
+
if (path.resolve(outputPath) === path.resolve(filePath) ||
|
|
1338
|
+
(outputMetadata?.dev === metadata.dev && outputMetadata?.ino === metadata.ino)) {
|
|
1339
|
+
throw cliValidationError("Keep the raw evidence: summary output must differ from the input file, including aliases.");
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
const envelope = readAdminJsonFile(filePath, "game diagnostic envelope", { maxBytes });
|
|
1343
|
+
return { ok: true, status: "success", changed: false, data: summarizeGameDiagnostics(envelope) };
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1317
1346
|
export async function adminCommand(options) {
|
|
1318
1347
|
const [namespace, action] = options.positional || [];
|
|
1319
1348
|
const composite =
|
|
@@ -1323,15 +1352,21 @@ export async function adminCommand(options) {
|
|
|
1323
1352
|
? dailyRunProgress
|
|
1324
1353
|
: namespace === "featured" && action === "candidates"
|
|
1325
1354
|
? featuredCandidates
|
|
1355
|
+
: namespace === "game-diagnostics" && action === "summarize"
|
|
1356
|
+
? gameDiagnosticSummary
|
|
1326
1357
|
: namespace === "report" && action === "serve"
|
|
1327
1358
|
? reportServe
|
|
1328
1359
|
: null;
|
|
1329
1360
|
if (composite) {
|
|
1330
1361
|
const result = await composite(options);
|
|
1331
1362
|
if (options.adminOutput) {
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
}
|
|
1363
|
+
if (composite === gameDiagnosticSummary) {
|
|
1364
|
+
writeAdminJsonFile(options.adminOutput, result, { privateFile: true });
|
|
1365
|
+
} else {
|
|
1366
|
+
writeFileSync(options.adminOutput, JSON.stringify(result), {
|
|
1367
|
+
mode: 0o600,
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1335
1370
|
}
|
|
1336
1371
|
console.log(JSON.stringify(result, null, options.json ? 0 : 2));
|
|
1337
1372
|
if (result && result.ok === false) process.exitCode = 1;
|
|
@@ -2409,6 +2444,7 @@ const FEATURED_RUN_OPERATIONS = new Set([
|
|
|
2409
2444
|
// run's write work without the newspaper, sponsor scan or bank seeding (the
|
|
2410
2445
|
// API holds the same line through the run's scopes).
|
|
2411
2446
|
const APPROVALS_RUN_EXCLUDED_OPERATIONS = new Set([
|
|
2447
|
+
"subject.website-update.post",
|
|
2412
2448
|
"news.claim",
|
|
2413
2449
|
"news.print",
|
|
2414
2450
|
"news.submit",
|
|
@@ -2427,6 +2463,7 @@ export function assertAdminOperationAllowedForRunScope({
|
|
|
2427
2463
|
"news.claim",
|
|
2428
2464
|
"news.submit",
|
|
2429
2465
|
"news.print",
|
|
2466
|
+
"subject.website-update.post",
|
|
2430
2467
|
"daily-run.complete",
|
|
2431
2468
|
"daily-run.fail",
|
|
2432
2469
|
].includes(operation.name)
|
|
@@ -2674,6 +2711,9 @@ export function parseAdminOperation(options) {
|
|
|
2674
2711
|
if (namespace === "owner-trace") {
|
|
2675
2712
|
return buildOwnerTraceOperation(options);
|
|
2676
2713
|
}
|
|
2714
|
+
if (namespace === "jev-routing") {
|
|
2715
|
+
return buildJevRoutingReviewOperation({ action, target, extra, options });
|
|
2716
|
+
}
|
|
2677
2717
|
// Who holds which management level (admin-roles.js); read-only, no run.
|
|
2678
2718
|
if (namespace === "roles") {
|
|
2679
2719
|
return buildRolesOperation(options);
|
|
@@ -3696,7 +3736,8 @@ export function parseAdminOperation(options) {
|
|
|
3696
3736
|
if (days > 30 || userId && !buildId) throw cliValidationError("Use --build with --user; --days must be 1–30.");
|
|
3697
3737
|
return readOperation("reward-diagnostics.read", withQuery("/cli/admin/reward-diagnostics", {
|
|
3698
3738
|
buildId: buildId || undefined, userId: userId || undefined, days,
|
|
3699
|
-
before: options.adminCursor ? parseRequiredInteger(options.adminCursor, "--cursor", 1) : undefined
|
|
3739
|
+
before: options.adminCursor ? parseRequiredInteger(options.adminCursor, "--cursor", 1) : undefined,
|
|
3740
|
+
until: options.adminUntil ? parseRequiredInteger(options.adminUntil, "--until", 1) : undefined
|
|
3700
3741
|
}), { requiresRun: false });
|
|
3701
3742
|
}
|
|
3702
3743
|
|
|
@@ -4162,6 +4203,22 @@ export function parseAdminOperation(options) {
|
|
|
4162
4203
|
}
|
|
4163
4204
|
|
|
4164
4205
|
if (namespace === "subject") {
|
|
4206
|
+
if (action === "post-update") {
|
|
4207
|
+
if (target || extra || !options.adminFile) {
|
|
4208
|
+
throw cliValidationError("Usage: lumine admin subject post-update --file <article.json> [--confirm]. Preview is the default.");
|
|
4209
|
+
}
|
|
4210
|
+
const article = readAdminJsonFile(options.adminFile, "--file", { maxBytes: 256 * 1024 });
|
|
4211
|
+
if (!article || typeof article !== "object" || Array.isArray(article)) {
|
|
4212
|
+
throw cliValidationError("The article file must contain one composed website-update object.");
|
|
4213
|
+
}
|
|
4214
|
+
if (options.adminConfirm && options.adminConfirm !== "true") {
|
|
4215
|
+
throw cliValidationError("Pass --confirm on its own, without a value.");
|
|
4216
|
+
}
|
|
4217
|
+
const confirm = options.adminConfirm === "true";
|
|
4218
|
+
if (confirm && options.adminDryRun) throw cliValidationError("Choose --confirm or --dry-run, not both.");
|
|
4219
|
+
const operation = confirm ? writeOperation : bodyReadOperation;
|
|
4220
|
+
return operation("subject.website-update.post", "POST", "/cli/admin/subjects/website-update", { article, confirm });
|
|
4221
|
+
}
|
|
4165
4222
|
if (action === "get") return subjectGetOperation(target, options);
|
|
4166
4223
|
if (action === "comments") {
|
|
4167
4224
|
const subjectId = parseSubjectId(target);
|
|
@@ -7595,6 +7652,10 @@ function printAdminResult({ operation, result }) {
|
|
|
7595
7652
|
printOwnerTrace(data);
|
|
7596
7653
|
return;
|
|
7597
7654
|
}
|
|
7655
|
+
if (operation.name === "jevRouting.review") {
|
|
7656
|
+
for (const line of formatJevRoutingReview(data)) console.log(line);
|
|
7657
|
+
return;
|
|
7658
|
+
}
|
|
7598
7659
|
if (operation.name === "roles.read") {
|
|
7599
7660
|
printRoles(data);
|
|
7600
7661
|
return;
|
package/lib/commands.js
CHANGED
|
@@ -2474,6 +2474,7 @@ export function parseArgs(args) {
|
|
|
2474
2474
|
const booleanFlags = new Set([
|
|
2475
2475
|
// lumine admin arena import --live
|
|
2476
2476
|
"live",
|
|
2477
|
+
"additive",
|
|
2477
2478
|
"remove",
|
|
2478
2479
|
"fault",
|
|
2479
2480
|
"noAnnounce",
|
|
@@ -2507,6 +2508,7 @@ export function parseArgs(args) {
|
|
|
2507
2508
|
"wake",
|
|
2508
2509
|
"includeLegacy",
|
|
2509
2510
|
"includePrivateEvidence",
|
|
2511
|
+
"includeSynthetic",
|
|
2510
2512
|
"allFlagged",
|
|
2511
2513
|
"reviewed",
|
|
2512
2514
|
"noReviewLoop",
|
|
@@ -2680,6 +2682,8 @@ export function parseArgs(args) {
|
|
|
2680
2682
|
adminApprove: raw.approve ? String(raw.approve) : "",
|
|
2681
2683
|
adminSinceRun: Boolean(raw.sinceRun),
|
|
2682
2684
|
adminSince: raw.since ? String(raw.since) : "",
|
|
2685
|
+
adminUntil: raw.until ? String(raw.until) : "",
|
|
2686
|
+
adminIncludeSynthetic: Boolean(raw.includeSynthetic),
|
|
2683
2687
|
adminPath: raw.path ? String(raw.path) : "",
|
|
2684
2688
|
adminPerf: Boolean(raw.perf),
|
|
2685
2689
|
adminWake: Boolean(raw.wake),
|
|
@@ -2701,6 +2705,7 @@ export function parseArgs(args) {
|
|
|
2701
2705
|
// lumine admin arena (admin-arena.js)
|
|
2702
2706
|
adminArenaAudio: raw.audio ? String(raw.audio) : "",
|
|
2703
2707
|
adminArenaLive: parseBoolean(raw.live, false),
|
|
2708
|
+
adminArenaAdditive: parseBoolean(raw.additive, false),
|
|
2704
2709
|
adminArenaItem: raw.item ? String(raw.item) : "",
|
|
2705
2710
|
adminArenaTo: raw.to ? String(raw.to) : "",
|
|
2706
2711
|
adminClaimFile: raw.claim ? String(raw.claim) : "",
|
|
@@ -2851,6 +2856,10 @@ export function parseArgs(args) {
|
|
|
2851
2856
|
adminTarget: raw.target ? String(raw.target) : "",
|
|
2852
2857
|
adminActions: raw.actions ? String(raw.actions) : "",
|
|
2853
2858
|
adminDate: raw.date ? String(raw.date) : "",
|
|
2859
|
+
adminCostDate: raw.costDate ? String(raw.costDate) : "",
|
|
2860
|
+
adminPhase: raw.phase ? String(raw.phase) : "",
|
|
2861
|
+
adminMaxSteps: raw.maxSteps ?? "",
|
|
2862
|
+
adminMaxSeconds: raw.maxSeconds ?? "",
|
|
2854
2863
|
adminDays: raw.days ? String(raw.days) : "",
|
|
2855
2864
|
adminSpendingMonth: raw.month,
|
|
2856
2865
|
adminSpendingFrom: raw.from,
|
|
@@ -3316,7 +3325,7 @@ export function printHelp() {
|
|
|
3316
3325
|
lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
|
|
3317
3326
|
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)
|
|
3318
3327
|
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)
|
|
3319
|
-
lumine admin arena queue | import --dir <paper folder> [--audio audio/<exam>/<paper>] [--live] [--confirm] | status --item <id> --to live|retired|checked --confirm (Arena's mock-exam bank: import is a validation-only dry run without --confirm; upload audio first with twinkle-api scripts/arena-upload-media.ts)
|
|
3328
|
+
lumine admin arena queue | paper-queue [--cursor <id>] [--limit <1-100>] | import --dir <paper folder> [--audio audio/<exam>/<paper>] [--live] [--confirm] | status --item <id> --to live|retired|checked --confirm (Arena's mock-exam bank: import is a validation-only dry run without --confirm; upload audio first with twinkle-api scripts/arena-upload-media.ts)
|
|
3320
3329
|
lumine admin nonogram status | add --file <pictures.json> | retire <key> | restore <key> [--confirm] (private reviewed pictures; changes are dry runs without --confirm)
|
|
3321
3330
|
lumine admin build-plans stuck [--days <1-90>] [--all] | recheck <buildId> [<buildId> ...] [--confirm] (Lumine Build plans stuck on one milestone: runs, Energy vs estimate, gradings, loop-guard state; recheck re-grades the current milestone with today's evaluator, free for the creator, dry run without --confirm)
|
|
3322
3331
|
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>]
|
|
@@ -3324,6 +3333,7 @@ export function printHelp() {
|
|
|
3324
3333
|
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)
|
|
3325
3334
|
lumine admin reward-activity [--date YYYY-MM-DD] [--days <1..31>] [--build <id>] [--json]
|
|
3326
3335
|
lumine admin reward-diagnostics [--build <id> --user <id>] [--days <1..30>] [--cursor <id>] [--json]
|
|
3336
|
+
lumine admin game-diagnostics summarize --file <envelope.json> [--json] (local retained sharedDb receipts; deduplicates stable events, preserves delivery and coverage limits)
|
|
3327
3337
|
lumine admin chat-reports list [--status pending|open|reviewing|resolved|dismissed|all] [--cursor <id>] [--json]
|
|
3328
3338
|
lumine admin chat-reports show <report-id> [--json] (reported message snapshot + surrounding context)
|
|
3329
3339
|
lumine admin chat-reports set <report-id> --status reviewing|resolved|dismissed|open --note <what was decided> [--json]
|
|
@@ -3339,13 +3349,14 @@ export function printHelp() {
|
|
|
3339
3349
|
lumine admin reward-review reject|revoke <review-id> --reason <text> [--json]
|
|
3340
3350
|
lumine admin recommendations list [--since-run|--after <date>|--include-legacy] [--all --checkpoint <file> [--resume]] [--content-types comment,dailyReflection] [--unviewed|--viewed] [--cursor <cursor>] [--json]
|
|
3341
3351
|
lumine admin builds candidates [--since-run|--after <date>|--include-legacy] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--limit <number>] [--json]
|
|
3342
|
-
lumine admin daily-run gather --output-dir <dir> [--date YYYY-MM-DD] [--json] (
|
|
3352
|
+
lumine admin daily-run gather --output-dir <dir> [--cost-date YYYY-MM-DD] [--phase priority|evidence|all] [--max-steps 200] [--max-seconds 900] [--resume] [--json] (closed UTC cost day; resumable collection, never automatic review)
|
|
3343
3353
|
lumine admin daily-run progress --dir <dir> [--file <duty-evidence.json>] [--json] (reading, canonical effort counts, pending duties)
|
|
3344
3354
|
lumine admin featured candidates [--days 7] [--after <iso>] [--json] (recent, provably never-Featured Subjects with comment counts)
|
|
3345
3355
|
lumine admin report serve --file <report.md> [--port <n>] [--open] [--json] (loopback report page with section nav)
|
|
3346
3356
|
lumine admin builds review <build-url-or-id> [--output-dir <dir>] [--wait-ms <ms>] [--interact <steps.json>] [--browser-path <path>] [--json]
|
|
3347
3357
|
lumine admin subjects candidates [--since-run|--after <date>|--include-legacy] [--effort unassigned] [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
|
|
3348
3358
|
lumine admin subject get|reveal <subject-url-or-id> [--json]
|
|
3359
|
+
lumine admin subject post-update --file <article.json> [--confirm] [--json]
|
|
3349
3360
|
lumine admin subject comments <subject-url-or-id> [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
|
|
3350
3361
|
lumine admin subject effort set <subject-id> --level <1|2|3> [--json]
|
|
3351
3362
|
lumine admin subject creator set-made-by-poster <subject-id> [--json]
|
|
@@ -3372,6 +3383,7 @@ export function printHelp() {
|
|
|
3372
3383
|
lumine admin reward-bank report <buildId> [--days 1] [--json] (question-bank telemetry: generation outcomes, learners, payouts, ladder, questions to review)
|
|
3373
3384
|
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: sessions, 20 s wake profiles, bounded viewport geometry and dropped-event counts after loads or returns; network tags identify the upload network; 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)
|
|
3374
3385
|
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)
|
|
3386
|
+
lumine admin jev-routing review --since <unix|ISO> --until <unix|ISO> [--limit 1..20] [--cursor <cursor> | --all --checkpoint <file> [--resume]] [--include-synthetic] [--json] (bounded frozen evidence for paired disagreements and Auto fallbacks; collection is not review)
|
|
3375
3387
|
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]
|
|
3376
3388
|
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]
|
|
3377
3389
|
lumine admin comment post --draft-id <id> [--json]
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const MAX_BYTES = 16 * 1024 * 1024;
|
|
4
|
+
const MAX_ROWS = 10000;
|
|
5
|
+
const DELIVERY_NUMBERS = new Set([
|
|
6
|
+
"v", "pending", "acknowledged", "requestFailures", "evicted", "expired",
|
|
7
|
+
"exhausted", "storageFailures", "oversized", "invalid", "attempt", "attempts",
|
|
8
|
+
"failures", "localEvictions", "coalescedBatches", "retryInMs"
|
|
9
|
+
]);
|
|
10
|
+
const DELIVERY_FIELDS = new Set([...DELIVERY_NUMBERS, "sending", "lastOutcome"]);
|
|
11
|
+
const KINDS = new Set([
|
|
12
|
+
"lifecycle", "launch-outcome", "save-failed", "save-ok", "save-load-failed",
|
|
13
|
+
"save-protected", "save-recovered", "soundtrack-diagnostics", "error", "startup"
|
|
14
|
+
]);
|
|
15
|
+
const EVENTS = new Set([
|
|
16
|
+
"hidden", "visible", "blur", "focus", "pagehide", "session_started", "action",
|
|
17
|
+
"battle_choice", "board_solved", "board_started", "fight_left", "fight_result",
|
|
18
|
+
"fight_started", "input", "lesson_step", "progress", "puzzle_progress",
|
|
19
|
+
"runtime_error", "save_status", "scene", "stalled"
|
|
20
|
+
]);
|
|
21
|
+
const SOUNDTRACK_COUNTERS = new Set([
|
|
22
|
+
"ready", "recommendations", "recommendations-empty", "tag-load", "tag-search",
|
|
23
|
+
"tag-submit", "tag-remove", "lock-open", "lock-open-failed", "progress-load", "progress-save"
|
|
24
|
+
]);
|
|
25
|
+
const record = value => value !== null && typeof value === "object" &&
|
|
26
|
+
(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
27
|
+
const positiveId = value => Number.isSafeInteger(value) && value > 0;
|
|
28
|
+
const stableId = value => typeof value === "string" && /^[\w:.-]{1,160}$/.test(value);
|
|
29
|
+
const hash = text => createHash("sha256").update(text).digest("hex");
|
|
30
|
+
|
|
31
|
+
// Validate the pure helper too: a caller need not have used the CLI's file cap.
|
|
32
|
+
// Sort keys for semantic comparison, but never mutate the retained source.
|
|
33
|
+
function canonicalJson(value, limit = MAX_BYTES) {
|
|
34
|
+
let bytes = 0;
|
|
35
|
+
const ancestors = new Set();
|
|
36
|
+
const token = text => {
|
|
37
|
+
bytes += Buffer.byteLength(text);
|
|
38
|
+
if (bytes > limit) throw new Error("Game diagnostic JSON exceeds its byte limit.");
|
|
39
|
+
return text;
|
|
40
|
+
};
|
|
41
|
+
function visit(item, depth) {
|
|
42
|
+
if (depth > 32) throw new Error("Game diagnostic JSON exceeds depth 32.");
|
|
43
|
+
if (item === null || typeof item === "boolean") return token(JSON.stringify(item));
|
|
44
|
+
if (typeof item === "number" && Number.isFinite(item)) return token(JSON.stringify(item));
|
|
45
|
+
if (typeof item === "string") {
|
|
46
|
+
if (item.length > limit) throw new Error("Game diagnostic JSON exceeds its byte limit.");
|
|
47
|
+
return token(JSON.stringify(item));
|
|
48
|
+
}
|
|
49
|
+
if ((!Array.isArray(item) && !record(item)) || ancestors.has(item)) throw new Error("Game diagnostics require finite, acyclic JSON values.");
|
|
50
|
+
ancestors.add(item);
|
|
51
|
+
const parts = [];
|
|
52
|
+
if (Array.isArray(item)) {
|
|
53
|
+
parts.push(token("["));
|
|
54
|
+
for (let i = 0; i < item.length; i++) {
|
|
55
|
+
if (i) parts.push(token(","));
|
|
56
|
+
parts.push(visit(item[i], depth + 1));
|
|
57
|
+
}
|
|
58
|
+
parts.push(token("]"));
|
|
59
|
+
} else {
|
|
60
|
+
parts.push(token("{"));
|
|
61
|
+
for (const key of Object.keys(item).sort()) {
|
|
62
|
+
if (parts.length > 1) parts.push(token(","));
|
|
63
|
+
parts.push(token(JSON.stringify(key)), token(":"), visit(item[key], depth + 1));
|
|
64
|
+
}
|
|
65
|
+
parts.push(token("}"));
|
|
66
|
+
}
|
|
67
|
+
ancestors.delete(item);
|
|
68
|
+
return parts.join("");
|
|
69
|
+
}
|
|
70
|
+
return visit(value, 0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function semanticJson(data) {
|
|
74
|
+
if (!record(data.delivery)) return canonicalJson(data, 16384);
|
|
75
|
+
const semantic = { ...data };
|
|
76
|
+
const remaining = Object.entries(data.delivery).filter(([key]) => !DELIVERY_FIELDS.has(key));
|
|
77
|
+
if (remaining.length) semantic.delivery = Object.fromEntries(remaining);
|
|
78
|
+
else delete semantic.delivery;
|
|
79
|
+
return canonicalJson(semantic, 16384);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function deliverySnapshot(data) {
|
|
83
|
+
if (!record(data.delivery)) return null;
|
|
84
|
+
const snapshot = {};
|
|
85
|
+
for (const key of DELIVERY_NUMBERS) {
|
|
86
|
+
const value = data.delivery[key];
|
|
87
|
+
if (Number.isSafeInteger(value) && value >= 0) snapshot[key] = value;
|
|
88
|
+
}
|
|
89
|
+
if (typeof data.delivery.sending === "boolean") snapshot.sending = data.delivery.sending;
|
|
90
|
+
if (["none", "viewer-changed", "acknowledged", "failed"].includes(data.delivery.lastOutcome)) snapshot.lastOutcome = data.delivery.lastOutcome;
|
|
91
|
+
return Object.keys(snapshot).length ? snapshot : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function coverageOf(value) {
|
|
95
|
+
if (value !== undefined && !record(value)) throw new Error("Game diagnostic coverage must be an object.");
|
|
96
|
+
const coverage = { complete: value?.complete === true };
|
|
97
|
+
if (value?.complete !== undefined && typeof value.complete !== "boolean") throw new Error("Coverage complete must be a boolean.");
|
|
98
|
+
for (const key of ["since", "until"]) if (value?.[key] !== undefined) {
|
|
99
|
+
if (!Number.isSafeInteger(value[key]) || value[key] < 0) throw new Error("Coverage boundaries must be nonnegative integers.");
|
|
100
|
+
coverage[key] = value[key];
|
|
101
|
+
}
|
|
102
|
+
if (coverage.since !== undefined && coverage.until !== undefined && coverage.since >= coverage.until) throw new Error("Coverage until must be after since.");
|
|
103
|
+
return coverage;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Local, metadata-only summary. Input rows remain the canonical evidence. */
|
|
107
|
+
export function summarizeGameDiagnostics(envelope) {
|
|
108
|
+
if (!record(envelope) || !positiveId(envelope.buildId) ||
|
|
109
|
+
typeof envelope.topic !== "string" || !/^[\w.-]{1,100}$/.test(envelope.topic) ||
|
|
110
|
+
!Array.isArray(envelope.entries) || envelope.entries.length > MAX_ROWS) {
|
|
111
|
+
throw new Error("Expected {buildId, topic, entries}; buildId must be positive, topic a technical name and entries at most 10000 rows.");
|
|
112
|
+
}
|
|
113
|
+
canonicalJson(envelope);
|
|
114
|
+
const collection = coverageOf(envelope.coverage);
|
|
115
|
+
const receipts = new Map(), groups = new Map(), legacy = [], observations = [];
|
|
116
|
+
let duplicateInputRows = 0, stableRows = 0, unknownArtifactRows = 0;
|
|
117
|
+
for (const entry of envelope.entries) {
|
|
118
|
+
if (!record(entry) || !positiveId(entry.id) || !positiveId(entry.userId) || !positiveId(entry.topicId) || !record(entry.data)) {
|
|
119
|
+
throw new Error("Every diagnostic entry requires canonical positive id, userId, topicId and object data; payload viewerId is not identity.");
|
|
120
|
+
}
|
|
121
|
+
if (entry.buildId !== undefined && entry.buildId !== envelope.buildId) throw new Error("Entry buildId does not match the envelope.");
|
|
122
|
+
const data = entry.data;
|
|
123
|
+
if (data.artifactVersionId != null && !positiveId(data.artifactVersionId)) throw new Error("artifactVersionId must be positive or unknown.");
|
|
124
|
+
for (const field of ["eventId", "batchId"]) if (data[field] != null && !stableId(data[field])) throw new Error(`Invalid diagnostic ${field}.`);
|
|
125
|
+
const exact = canonicalJson(data, 16384);
|
|
126
|
+
const receiptHash = hash(canonicalJson([entry.userId, entry.topicId, data.artifactVersionId ?? null, exact]));
|
|
127
|
+
if (receipts.has(entry.id)) {
|
|
128
|
+
if (receipts.get(entry.id) !== receiptHash) throw new Error("Conflicting copies of one canonical entry ID; inspect the retained raw source.");
|
|
129
|
+
duplicateInputRows++;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
receipts.set(entry.id, receiptHash);
|
|
133
|
+
const artifactVersionId = data.artifactVersionId ?? null;
|
|
134
|
+
if (artifactVersionId === null) unknownArtifactRows++;
|
|
135
|
+
const fields = { entryId: entry.id, userId: entry.userId, topicId: entry.topicId, artifactVersionId };
|
|
136
|
+
const snapshot = deliverySnapshot(data);
|
|
137
|
+
if (snapshot) observations.push({ ...fields, delivery: snapshot });
|
|
138
|
+
const idType = data.eventId != null ? "eventId" : data.batchId != null ? "batchId" : null;
|
|
139
|
+
if (!idType) { legacy.push(data); continue; }
|
|
140
|
+
stableRows++;
|
|
141
|
+
const key = canonicalJson([envelope.buildId, envelope.topic, entry.topicId, entry.userId, artifactVersionId, idType, data[idType]]);
|
|
142
|
+
let group = groups.get(key);
|
|
143
|
+
if (!group) {
|
|
144
|
+
group = { ...fields, idType, stableId: data[idType], hashes: new Map(), rows: 0 };
|
|
145
|
+
groups.set(key, group);
|
|
146
|
+
}
|
|
147
|
+
group.rows++;
|
|
148
|
+
const fingerprint = hash(semanticJson(data));
|
|
149
|
+
let variant = group.hashes.get(fingerprint);
|
|
150
|
+
if (!variant) { variant = { data, entryIds: [] }; group.hashes.set(fingerprint, variant); }
|
|
151
|
+
variant.entryIds.push(entry.id);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const counted = [...legacy], collisions = [];
|
|
155
|
+
let uniqueStableEvents = 0, retryDuplicateReceipts = 0, collisionGroups = 0, collidingReceipts = 0;
|
|
156
|
+
for (const group of groups.values()) {
|
|
157
|
+
if (group.hashes.size > 1) {
|
|
158
|
+
collisionGroups++; collidingReceipts += group.rows;
|
|
159
|
+
if (collisions.length < 100) collisions.push({ userId: group.userId, topicId: group.topicId,
|
|
160
|
+
artifactVersionId: group.artifactVersionId, idType: group.idType, stableId: group.stableId,
|
|
161
|
+
receipts: group.rows, variants: [...group.hashes].slice(0, 20).map(([sha256, variant]) => ({ sha256,
|
|
162
|
+
entryIds: variant.entryIds.slice(0, 20), omittedEntryIds: Math.max(0, variant.entryIds.length - 20) })),
|
|
163
|
+
omittedVariants: Math.max(0, group.hashes.size - 20) });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
uniqueStableEvents++; retryDuplicateReceipts += group.rows - 1;
|
|
167
|
+
counted.push(group.hashes.values().next().value.data);
|
|
168
|
+
}
|
|
169
|
+
const byKind = {}, alzebraEventNames = {}, soundtrackCounters = {};
|
|
170
|
+
let invalidCounterMetrics = 0;
|
|
171
|
+
for (const data of counted) {
|
|
172
|
+
const kind = data.batchId && !data.kind ? "alzebra-batch" : KINDS.has(data.kind) ? data.kind : "other";
|
|
173
|
+
byKind[kind] = (byKind[kind] || 0) + 1;
|
|
174
|
+
if (kind === "alzebra-batch" && Array.isArray(data.events)) for (const event of data.events) {
|
|
175
|
+
const name = EVENTS.has(event?.name) ? event.name : "other";
|
|
176
|
+
alzebraEventNames[name] = (alzebraEventNames[name] || 0) + 1;
|
|
177
|
+
}
|
|
178
|
+
if (kind === "soundtrack-diagnostics" && record(data.counters)) for (const name of SOUNDTRACK_COUNTERS) {
|
|
179
|
+
const value = data.counters[name];
|
|
180
|
+
if (value === undefined) continue;
|
|
181
|
+
if (!record(value) || !["count", "failed", "durationMs", "maxMs"].every(key => Number.isSafeInteger(value[key]) && value[key] >= 0 && value[key] <= 1e11) || value.failed > value.count) {
|
|
182
|
+
invalidCounterMetrics++;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const total = soundtrackCounters[name] ||= { count: 0, failed: 0, durationMs: 0, maxMs: 0 };
|
|
186
|
+
total.count += value.count; total.failed += value.failed; total.durationMs += value.durationMs;
|
|
187
|
+
total.maxMs = Math.max(total.maxMs, value.maxMs);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
observations.sort((a, b) => b.entryId - a.entryId);
|
|
191
|
+
return {
|
|
192
|
+
schema: 1, buildId: envelope.buildId, topic: envelope.topic, collection,
|
|
193
|
+
rows: { input: envelope.entries.length, canonical: receipts.size, duplicateInputRows,
|
|
194
|
+
stableRows, legacyRows: legacy.length, unknownArtifactRows },
|
|
195
|
+
deduplication: { coverage: receipts.size === 0 ? "empty" : legacy.length || unknownArtifactRows || collisionGroups ? "partial" : "complete",
|
|
196
|
+
uniqueStableEvents, retryDuplicateReceipts, collisionGroups, collidingReceipts,
|
|
197
|
+
countedEvents: counted.length, collisions, omittedCollisions: Math.max(0, collisionGroups - collisions.length) },
|
|
198
|
+
counts: { byKind, alzebraEventNames, soundtrackCounters, invalidCounterMetrics },
|
|
199
|
+
delivery: { observations: observations.slice(0, 100), totalObservations: observations.length,
|
|
200
|
+
omittedObservations: Math.max(0, observations.length - 100),
|
|
201
|
+
note: "Per-receipt client snapshots; cumulative counters are not summed. An acknowledged count concerns earlier client receipts, not this row's gameplay save." },
|
|
202
|
+
note: "Preserve and read the raw receipts. Legacy rows remain separate and may contain retries; unknown artifacts are not version evidence. Collisions are excluded from event counts. Collection completeness describes supplied server rows, not successful delivery of every client event."
|
|
203
|
+
};
|
|
204
|
+
}
|