@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-arena.js
CHANGED
|
@@ -14,7 +14,7 @@ import path from "node:path";
|
|
|
14
14
|
// re-importing a folder retires that paper's earlier items
|
|
15
15
|
// status --item <id> --to live|retired|checked
|
|
16
16
|
export const ARENA_USAGE =
|
|
17
|
-
"Usage: lumine admin arena queue | marking [--cursor <id>] [--limit <1-200>] | import --dir <paper folder> [--audio audio/<exam>/<paper>] [--live] [--confirm] | status --item <id> --to live|retired|checked [--confirm] | today-queue [--cursor <id>] [--limit <1-100>] | today-member --user <id> | today-import --file <reviewed-questions.json> [--confirm] [--json]";
|
|
17
|
+
"Usage: lumine admin arena queue | paper-queue [--cursor <id>] [--limit <1-100>] | marking [--cursor <id>] [--limit <1-200>] | import (--dir <paper folder> | --additive --file <reviewed-items.json>) [--audio audio/<exam>/<paper>] [--live] [--confirm] | status --item <id> --to live|retired|checked [--confirm] | today-queue [--cursor <id>] [--limit <1-100>] | today-member --user <id> | today-import --file <reviewed-questions.json> [--confirm] [--json]";
|
|
18
18
|
const MAX_ITEMS = 300;
|
|
19
19
|
|
|
20
20
|
function validationError(message) {
|
|
@@ -49,7 +49,26 @@ function readPaper(dir) {
|
|
|
49
49
|
.sort((a, b) => a.number - b.number);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function readAdditiveItems(file) {
|
|
53
|
+
const resolved = path.resolve(file);
|
|
54
|
+
if (fs.statSync(resolved).size > 5_000_000) {
|
|
55
|
+
throw validationError("The additive import file must be under 5 MB.");
|
|
56
|
+
}
|
|
57
|
+
const items = JSON.parse(fs.readFileSync(resolved, "utf8"));
|
|
58
|
+
if (!Array.isArray(items) || !items.length || items.length > MAX_ITEMS ||
|
|
59
|
+
items.some((item) => !item || typeof item !== "object" || Array.isArray(item))) {
|
|
60
|
+
throw validationError(`An additive import file must contain an array of 1 to ${MAX_ITEMS} reviewed items.`);
|
|
61
|
+
}
|
|
62
|
+
return items;
|
|
63
|
+
}
|
|
64
|
+
|
|
52
65
|
export function buildArenaOperation({ action, options }) {
|
|
66
|
+
if (action === "paper-queue") {
|
|
67
|
+
const cursor = Number(options.adminCursor || 0);
|
|
68
|
+
const limit = Number(options.adminLimit ?? 50);
|
|
69
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0 || !Number.isInteger(limit) || limit < 1 || limit > 100) throw validationError("Use a non-negative --cursor and --limit from 1 to 100.");
|
|
70
|
+
return { name: "arena.paper.queue", method: "GET", path: `/cli/admin/arena/paper-queue?cursor=${cursor}&limit=${limit}`, mutates: false, requiresRun: false };
|
|
71
|
+
}
|
|
53
72
|
if (action === "today-queue") {
|
|
54
73
|
const cursor = Number(options.adminCursor || 0);
|
|
55
74
|
const limit = Number(options.adminLimit ?? 50);
|
|
@@ -92,7 +111,11 @@ export function buildArenaOperation({ action, options }) {
|
|
|
92
111
|
};
|
|
93
112
|
}
|
|
94
113
|
if (action === "import") {
|
|
95
|
-
const
|
|
114
|
+
const additive = Boolean(options.adminArenaAdditive);
|
|
115
|
+
if ((options.adminFile && options.adminDir) || (options.adminFile && !additive)) {
|
|
116
|
+
throw validationError("Use --dir for a complete paper, or --additive with exactly one of --file or --dir for a refill.");
|
|
117
|
+
}
|
|
118
|
+
const items = options.adminFile ? readAdditiveItems(options.adminFile) : readPaper(options.adminDir);
|
|
96
119
|
const confirm = confirmed(options);
|
|
97
120
|
return {
|
|
98
121
|
name: confirm ? "arena.import" : "arena.import.preview",
|
|
@@ -103,8 +126,9 @@ export function buildArenaOperation({ action, options }) {
|
|
|
103
126
|
live: Boolean(options.adminArenaLive),
|
|
104
127
|
dryRun: !confirm,
|
|
105
128
|
...(options.adminArenaAudio ? { audioPrefix: options.adminArenaAudio } : {}),
|
|
106
|
-
//
|
|
107
|
-
paper
|
|
129
|
+
// Full papers retain their replacement semantics. Additive refills
|
|
130
|
+
// preserve the existing bank without retiring another paper's items.
|
|
131
|
+
...(!additive ? { paper: path.basename(path.resolve(options.adminDir)) } : {}),
|
|
108
132
|
},
|
|
109
133
|
mutates: confirm,
|
|
110
134
|
requiresRun: false,
|
|
@@ -133,6 +157,11 @@ export function buildArenaOperation({ action, options }) {
|
|
|
133
157
|
}
|
|
134
158
|
|
|
135
159
|
export function formatArenaResult({ operation, data }) {
|
|
160
|
+
if (operation.name === "arena.paper.queue") return [
|
|
161
|
+
`Checked ${data?.scannedMembers ?? 0} members; ${data?.entries?.length ?? 0} exam tracks are waiting for fresh papers on this page.`,
|
|
162
|
+
...(data?.entries || []).map(row => `${row.username} (${row.userId}): ${row.examKey} ${row.difficulty}/${row.length}${row.reason === "solo_mastery_paper" ? " — solo mastery ready" : ""}`),
|
|
163
|
+
data?.nextCursor ? `Continue: lumine admin arena paper-queue --cursor ${data.nextCursor} --json` : "End of waiting-member queue.",
|
|
164
|
+
];
|
|
136
165
|
if (operation.name === "arena.today.queue") return [
|
|
137
166
|
...(data?.members || []).map(m => `#${m.userId} ${m.username}: ${m.ready} questions ready${m.refillDue ? `; refill to ${m.target}` : ""} (preferences v${m.revision})`),
|
|
138
167
|
data?.nextCursor ? `Continue: lumine admin arena today-queue --cursor ${data.nextCursor} --json` : "End of Today’s Q queue.",
|
package/lib/admin-build-plans.js
CHANGED
|
@@ -74,6 +74,12 @@ export function buildBuildPlansOperation({ action, options }) {
|
|
|
74
74
|
throw validationError(BUILD_PLANS_USAGE);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function formatStepCoverage(row) {
|
|
78
|
+
if (!Number.isFinite(row.steps)) return ""; // Older API response.
|
|
79
|
+
if (row.stepsPassed == null) return `; steps ?/${row.steps} verified (no step grading yet)`;
|
|
80
|
+
return `; steps ${row.stepsPassed}/${row.steps} verified, ${row.stepsUnverified ?? 0} unverified, ${row.stepsNeedWork ?? 0} need work`;
|
|
81
|
+
}
|
|
82
|
+
|
|
77
83
|
function formatRow(row) {
|
|
78
84
|
const ratio = row.energyRatio == null ? "?" : `${row.energyRatio}x`;
|
|
79
85
|
const milestone =
|
|
@@ -86,7 +92,7 @@ function formatRow(row) {
|
|
|
86
92
|
: row.wouldStall
|
|
87
93
|
? ` would stall (${row.wouldStall})`
|
|
88
94
|
: "";
|
|
89
|
-
return `Build ${row.buildId}: milestone ${milestone} (${row.milestonesDone} done), ${row.runs} runs, Energy ${ratio} of estimate, ${row.evaluations ?? "?"} gradings, non-pass streak ${row.consecutiveNonPass ?? "?"}, ${row.passed}/${row.criteria} passing, ${row.unverified} unverified${state};${coverage}${row.flags?.length ? ` [${row.flags.join(", ")}]` : ""}`;
|
|
95
|
+
return `Build ${row.buildId}: milestone ${milestone} (${row.milestonesDone} done), ${row.runs} runs, Energy ${ratio} of estimate, ${row.evaluations ?? "?"} gradings, non-pass streak ${row.consecutiveNonPass ?? "?"}, ${row.passed}/${row.criteria} passing, ${row.unverified} unverified${formatStepCoverage(row)}${state};${coverage}${row.flags?.length ? ` [${row.flags.join(", ")}]` : ""}`;
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
export function formatBuildPlansResult({ operation, data }) {
|
|
@@ -110,7 +116,7 @@ export function formatBuildPlansResult({ operation, data }) {
|
|
|
110
116
|
for (const result of data?.results || []) {
|
|
111
117
|
lines.push(
|
|
112
118
|
result?.ok
|
|
113
|
-
? `${result.replayed ? "(already rechecked by this request) " : ""}Build ${result.buildId}: milestone ${result.milestoneIndex + 1} ${result.verdict}, ${result.passed}/${result.criteria} passing, ${result.unverified} unverified${result.milestoneCompleted ? ", milestone completed" : ""}${result.stalled ? `, stalled (${result.stalled.reason})` : ""}${result.fileCoverage ? `; files ${result.fileCoverage.shownFiles}/${result.fileCoverage.totalFiles} + ${result.fileCoverage.toolFilesRead || 0} read in ${result.fileCoverage.toolRounds || 0} rounds` : ""}${result.usage ? `; cost ${result.usage.energyUnits} Energy units (${result.usage.inputTokens} in / ${result.usage.outputTokens} out tokens), not charged to the creator` : ""}`
|
|
119
|
+
? `${result.replayed ? "(already rechecked by this request) " : ""}Build ${result.buildId}: milestone ${result.milestoneIndex + 1} ${result.verdict}, ${result.passed}/${result.criteria} passing, ${result.unverified} unverified${formatStepCoverage(result)}${result.milestoneCompleted ? ", milestone completed" : ""}${result.stalled ? `, stalled (${result.stalled.reason})` : ""}${result.fileCoverage ? `; files ${result.fileCoverage.shownFiles}/${result.fileCoverage.totalFiles} + ${result.fileCoverage.toolFilesRead || 0} read in ${result.fileCoverage.toolRounds || 0} rounds` : ""}${result.usage ? `; cost ${result.usage.energyUnits} Energy units (${result.usage.inputTokens} in / ${result.usage.outputTokens} out tokens), not charged to the creator` : ""}`
|
|
114
120
|
: `Build ${result?.buildId}: not rechecked (${result?.reason === "run_in_progress" ? "a Lumine run is in progress; try again later" : result?.reason || "unknown"})`,
|
|
115
121
|
);
|
|
116
122
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
function invalid(message) {
|
|
6
|
+
const error = new Error(message);
|
|
7
|
+
error.code = "CLI_ADMIN_CLI_VALIDATION";
|
|
8
|
+
throw error;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const sha = (value) => createHash("sha256").update(value).digest("hex");
|
|
12
|
+
|
|
13
|
+
// Resuming reuses explicitly dated, byte-verified receipts. It does not assert
|
|
14
|
+
// that old observations are fresh, read, or an editorial duty is complete.
|
|
15
|
+
export function createDailyCollection(dir, options, { now = Date.now, clock = () => performance.now() } = {}) {
|
|
16
|
+
const started = clock();
|
|
17
|
+
const today = new Date(now()).toISOString().slice(0, 10);
|
|
18
|
+
const costDate = options.adminCostDate || options.adminDate || new Date(now() - 86400000).toISOString().slice(0, 10);
|
|
19
|
+
if (options.adminCostDate && options.adminDate && options.adminCostDate !== options.adminDate)
|
|
20
|
+
invalid("--cost-date and legacy --date must identify the same closed UTC cost day.");
|
|
21
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(costDate) || !Number.isFinite(Date.parse(`${costDate}T00:00:00Z`)) ||
|
|
22
|
+
new Date(`${costDate}T00:00:00Z`).toISOString().slice(0, 10) !== costDate || costDate >= today)
|
|
23
|
+
invalid("Use --cost-date YYYY-MM-DD for a completed UTC day, not today's run date.");
|
|
24
|
+
const phase = options.adminPhase || "all";
|
|
25
|
+
if (!["all", "priority", "evidence"].includes(phase)) invalid("--phase must be priority, evidence, or all.");
|
|
26
|
+
const maxSteps = Number(options.adminMaxSteps === "" || options.adminMaxSteps == null ? 200 : options.adminMaxSteps);
|
|
27
|
+
const maxSeconds = Number(options.adminMaxSeconds === "" || options.adminMaxSeconds == null ? 900 : options.adminMaxSeconds);
|
|
28
|
+
if (!Number.isInteger(maxSteps) || maxSteps < 1 || maxSteps > 1000 ||
|
|
29
|
+
!Number.isInteger(maxSeconds) || maxSeconds < 1 || maxSeconds > 14400)
|
|
30
|
+
invalid("Use --max-steps 1–1000 and --max-seconds 1–14400.");
|
|
31
|
+
const file = path.join(dir, "gather-checkpoint.json");
|
|
32
|
+
let checkpoint = { protocol: 1, collectionId: randomUUID(), startedAt: new Date(now()).toISOString(), costDate, entries: {} };
|
|
33
|
+
if (options.adminResume) {
|
|
34
|
+
if (!fs.existsSync(file)) invalid("No gather checkpoint exists in this directory; run without --resume first.");
|
|
35
|
+
checkpoint = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
36
|
+
if (checkpoint.protocol !== 1 || checkpoint.costDate !== costDate || !checkpoint.entries || typeof checkpoint.entries !== "object")
|
|
37
|
+
invalid("The checkpoint belongs to a different cost day or format. Use its --cost-date or start a new directory.");
|
|
38
|
+
}
|
|
39
|
+
const save = () => {
|
|
40
|
+
const temporary = `${file}.${process.pid}.next`;
|
|
41
|
+
fs.writeFileSync(temporary, JSON.stringify(checkpoint, null, 2) + "\n", { mode: 0o600 });
|
|
42
|
+
fs.renameSync(temporary, file);
|
|
43
|
+
};
|
|
44
|
+
save();
|
|
45
|
+
let executed = 0;
|
|
46
|
+
return {
|
|
47
|
+
costDate, phase, collectionId: checkpoint.collectionId, startedAt: checkpoint.startedAt,
|
|
48
|
+
warnings: options.adminDate ? ["--date is a legacy alias for --cost-date (a completed UTC day), not the management run date."] : [],
|
|
49
|
+
invalidate(name) {
|
|
50
|
+
if (checkpoint.entries[name]) { checkpoint.entries[name].ok = false; save(); }
|
|
51
|
+
},
|
|
52
|
+
collect(name, args, outputName, stage, run) {
|
|
53
|
+
const outputFile = path.join(dir, outputName);
|
|
54
|
+
const key = sha(JSON.stringify(args));
|
|
55
|
+
const cached = checkpoint.entries[name];
|
|
56
|
+
if (options.adminResume && cached?.ok && cached.requestHash === key && cached.file === outputName) {
|
|
57
|
+
try {
|
|
58
|
+
const bytes = fs.readFileSync(outputFile);
|
|
59
|
+
const json = JSON.parse(bytes);
|
|
60
|
+
if (sha(bytes) === cached.sha256 && json.ok === true && typeof json.status === "string")
|
|
61
|
+
return { ok: true, json, attempts: 0, durationMs: 0, capturedAt: cached.capturedAt, reused: true };
|
|
62
|
+
} catch { /* Missing/changed bytes require a fresh read, never a cached success. */ }
|
|
63
|
+
}
|
|
64
|
+
const remainingMs = maxSeconds * 1000 - (clock() - started);
|
|
65
|
+
const reason = phase !== "all" && phase !== stage ? `outside-${phase}-phase`
|
|
66
|
+
: executed >= maxSteps ? "step-budget" : remainingMs <= 0 ? "time-budget" : null;
|
|
67
|
+
if (reason) return { ok: false, deferred: true, reason, attempts: 0, durationMs: 0 };
|
|
68
|
+
executed++;
|
|
69
|
+
let result;
|
|
70
|
+
try { result = run(args, { outputFile, retryRead: true, timeoutMs: Math.max(1, Math.min(120000, Math.floor(remainingMs))) }); }
|
|
71
|
+
catch (error) { result = { ok: false, attempts: 1, json: { error: { message: String(error.message || error) } } }; }
|
|
72
|
+
const capturedAt = new Date(now()).toISOString();
|
|
73
|
+
let digest;
|
|
74
|
+
if (result.ok) {
|
|
75
|
+
try {
|
|
76
|
+
const bytes = fs.readFileSync(outputFile);
|
|
77
|
+
if (JSON.stringify(JSON.parse(bytes)) !== JSON.stringify(result.json)) throw new Error("receipt mismatch");
|
|
78
|
+
digest = sha(bytes);
|
|
79
|
+
} catch {
|
|
80
|
+
result = { ...result, ok: false, json: { error: { code: "CLI_DAILY_RECEIPT_INVALID", message: "Successful collection did not save its matching receipt." } } };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
checkpoint.entries[name] = { ok: result.ok, requestHash: key, file: outputName, capturedAt, ...(digest ? { sha256: digest } : {}) };
|
|
84
|
+
save();
|
|
85
|
+
return { ...result, capturedAt, reused: false };
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
package/lib/admin-daily.js
CHANGED
|
@@ -8,6 +8,8 @@ import { adminWorkDirectory } from "./admin-work-directory.js";
|
|
|
8
8
|
import { readAdminJsonFile, writeAdminJsonFile } from "./admin-news.js";
|
|
9
9
|
import { createDailyReviewState, dailyRunProgress } from "./admin-duties.js";
|
|
10
10
|
import { summarizeOwnerViewport, formatOwnerViewportReport } from "./admin-owner-viewport.js";
|
|
11
|
+
import { createDailyCollection } from "./admin-daily-collection.js";
|
|
12
|
+
import { collectDiagnosticPages, diagnosticCollectionSummary } from "./admin-diagnostic-collection.js";
|
|
11
13
|
export { dailyRunProgress };
|
|
12
14
|
|
|
13
15
|
// Multi-step conveniences for the full daily management run. Each one runs
|
|
@@ -22,7 +24,7 @@ const MARKED_ASSET = fileURLToPath(
|
|
|
22
24
|
|
|
23
25
|
export function runAdmin(
|
|
24
26
|
args,
|
|
25
|
-
{ outputFile, extraEnv, retryRead = false, spawnImpl = spawnSync } = {},
|
|
27
|
+
{ outputFile, extraEnv, retryRead = false, spawnImpl = spawnSync, timeoutMs } = {},
|
|
26
28
|
) {
|
|
27
29
|
const startedAt = performance.now();
|
|
28
30
|
const attemptFile = outputFile
|
|
@@ -33,6 +35,7 @@ export function runAdmin(
|
|
|
33
35
|
const result = spawnImpl(process.execPath, [LUMINE_BIN, ...full], {
|
|
34
36
|
encoding: "utf8",
|
|
35
37
|
maxBuffer: 256 * 1024 * 1024,
|
|
38
|
+
...(timeoutMs ? { timeout: timeoutMs } : {}),
|
|
36
39
|
env: { ...process.env, ...(extraEnv || {}) },
|
|
37
40
|
});
|
|
38
41
|
let parsed = null;
|
|
@@ -84,6 +87,9 @@ export function runAdmin(
|
|
|
84
87
|
sponsor: ["applications"],
|
|
85
88
|
"bot-output": [null],
|
|
86
89
|
"math-lab": ["queue"],
|
|
90
|
+
arena: ["queue", "today-queue", "paper-queue"],
|
|
91
|
+
"word-games": ["status"],
|
|
92
|
+
nonogram: ["status"],
|
|
87
93
|
subjects: ["candidates"],
|
|
88
94
|
subject: ["get"],
|
|
89
95
|
};
|
|
@@ -98,8 +104,9 @@ export function runAdmin(
|
|
|
98
104
|
result.error ||
|
|
99
105
|
result.signal ||
|
|
100
106
|
parsed?.error?.code === "lumine_http_invalid_response";
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
const remainingMs = timeoutMs == null ? undefined : Math.floor(timeoutMs - (performance.now() - startedAt));
|
|
108
|
+
if (!ok && retryRead && readOnly && transient && (remainingMs == null || remainingMs > 0)) {
|
|
109
|
+
const retried = runAdmin(args, { outputFile, extraEnv, spawnImpl, timeoutMs: remainingMs });
|
|
103
110
|
return {
|
|
104
111
|
...retried,
|
|
105
112
|
durationMs: Math.round(performance.now() - startedAt),
|
|
@@ -166,16 +173,24 @@ export function renderBotOutput({ chatMessages, comments }, file) {
|
|
|
166
173
|
fs.writeFileSync(file, lines.join("\n") + "\n", { mode: 0o600 });
|
|
167
174
|
}
|
|
168
175
|
|
|
169
|
-
export async function dailyRunGather(options, { run = runAdmin } = {}) {
|
|
176
|
+
export async function dailyRunGather(options, { run = runAdmin, now, clock } = {}) {
|
|
170
177
|
const startedAt = performance.now();
|
|
171
178
|
const dir = ensureDir(
|
|
172
179
|
path.resolve(options.adminOutputDir || options.adminDir || "."),
|
|
173
180
|
);
|
|
174
|
-
const
|
|
181
|
+
const collection = createDailyCollection(dir, options, { now, clock });
|
|
182
|
+
const yesterday = collection.costDate;
|
|
183
|
+
// Resume keeps the original observation boundary; no page slides its window
|
|
184
|
+
// forward while the operator works through a long diagnostic queue.
|
|
185
|
+
const diagnosticUntil = Math.floor(Date.parse(collection.startedAt) / 1000);
|
|
175
186
|
const steps = [];
|
|
187
|
+
const failLast = (error) => {
|
|
188
|
+
collection.invalidate(steps.at(-1).name);
|
|
189
|
+
Object.assign(steps.at(-1), { ok: false, error });
|
|
190
|
+
};
|
|
191
|
+
const evidenceNames = /^(ai-costs|media-costs|reward-activity|reward-diagnostics|jev-review|owner-wake|bot-output)/;
|
|
176
192
|
const step = (name, args, file) => {
|
|
177
|
-
const
|
|
178
|
-
const res = run(args, { outputFile, retryRead: true });
|
|
193
|
+
const res = collection.collect(name, args, file, evidenceNames.test(name) ? "evidence" : "priority", run);
|
|
179
194
|
steps.push({
|
|
180
195
|
name,
|
|
181
196
|
file,
|
|
@@ -183,14 +198,72 @@ export async function dailyRunGather(options, { run = runAdmin } = {}) {
|
|
|
183
198
|
attempts: res.attempts,
|
|
184
199
|
durationMs: res.durationMs,
|
|
185
200
|
recovered: Boolean(res.recovered),
|
|
186
|
-
|
|
201
|
+
capturedAt: res.capturedAt || null,
|
|
202
|
+
reused: Boolean(res.reused),
|
|
203
|
+
...(res.deferred ? { deferred: true, reason: res.reason } : res.ok ? {} : { error: res.json?.error || res.stderr }),
|
|
187
204
|
});
|
|
188
205
|
return res;
|
|
189
206
|
};
|
|
190
207
|
step("energy-budget", ["energy-budget"], "energy-budget.json");
|
|
191
208
|
step("news", ["news"], "news.json");
|
|
192
209
|
step("brief", ["brief"], "brief.json");
|
|
210
|
+
step("todos", ["todo", "list"], "todos.json");
|
|
211
|
+
step("escalations", ["escalation", "list"], "escalations.json");
|
|
212
|
+
const paperQueue = { complete: false, pages: 0, waitingTracks: 0 };
|
|
213
|
+
let paperCursor = 0;
|
|
214
|
+
for (;;) {
|
|
215
|
+
paperQueue.pages++;
|
|
216
|
+
const res = step(`arena-paper-page-${paperQueue.pages}`,
|
|
217
|
+
["arena", "paper-queue", "--cursor", String(paperCursor), "--limit", "100"],
|
|
218
|
+
`arena-paper-queue-${paperQueue.pages}.json`);
|
|
219
|
+
if (!res.ok) break;
|
|
220
|
+
const data = res.json?.data;
|
|
221
|
+
const next = data?.nextCursor;
|
|
222
|
+
if (data?.demandBasis !== "waiting-published-papers-v1" || !Array.isArray(data?.entries) ||
|
|
223
|
+
!(next === null || (Number.isSafeInteger(next) && next > paperCursor))) {
|
|
224
|
+
failLast("Invalid or non-advancing Arena paper-waiting page.");
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
paperQueue.waitingTracks += data.entries.length;
|
|
228
|
+
if (next === null) { paperQueue.complete = true; break; }
|
|
229
|
+
if (paperQueue.pages >= 100) {
|
|
230
|
+
failLast("Arena paper-waiting queue reached its 100-page bound.");
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
paperCursor = next;
|
|
234
|
+
}
|
|
193
235
|
step("math-lab-queue", ["math-lab", "queue"], "math-lab-queue.json");
|
|
236
|
+
step("arena-queue", ["arena", "queue"], "arena-queue.json");
|
|
237
|
+
step("word-games-status", ["word-games", "status"], "word-games-status.json");
|
|
238
|
+
step("nonogram-status", ["nonogram", "status"], "nonogram-status.json");
|
|
239
|
+
const personalQueue = { complete: false, pages: 0, members: 0 };
|
|
240
|
+
let personalCursor = 0;
|
|
241
|
+
const personalCursors = new Set();
|
|
242
|
+
for (;;) {
|
|
243
|
+
personalQueue.pages++;
|
|
244
|
+
const res = step(`arena-today-page-${personalQueue.pages}`,
|
|
245
|
+
["arena", "today-queue", "--cursor", String(personalCursor), "--limit", "100"],
|
|
246
|
+
`arena-today-queue-${personalQueue.pages}.json`);
|
|
247
|
+
if (!res.ok) break;
|
|
248
|
+
const data = res.json?.data;
|
|
249
|
+
const rawNext = data?.nextCursor;
|
|
250
|
+
const next = Number(rawNext || 0);
|
|
251
|
+
if (!Array.isArray(data?.members) || !Object.hasOwn(data, "nextCursor") ||
|
|
252
|
+
!(rawNext === null || (typeof rawNext === "number" && Number.isSafeInteger(rawNext)) || (typeof rawNext === "string" && /^\d+$/.test(rawNext))) ||
|
|
253
|
+
!Number.isSafeInteger(next) || next < 0) {
|
|
254
|
+
failLast("Invalid Arena personal queue page.");
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
personalQueue.members += data.members.length;
|
|
258
|
+
if (!next) { personalQueue.complete = true; break; }
|
|
259
|
+
if (personalCursors.has(next) || personalQueue.pages >= 100) {
|
|
260
|
+
collection.invalidate(steps.at(-1).name);
|
|
261
|
+
steps.push({ name: "arena-today-pagination", ok: false, error: "Arena personal queue repeated a cursor or reached its 100-page bound." });
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
personalCursors.add(next);
|
|
265
|
+
personalCursor = next;
|
|
266
|
+
}
|
|
194
267
|
step("ai-costs-monthly", ["ai-costs", "monthly"], "ai-monthly.json");
|
|
195
268
|
step("ai-costs-day", ["ai-costs", "day", yesterday], "ai-day.json");
|
|
196
269
|
step("media-costs", ["media-costs", "monthly"], "media.json");
|
|
@@ -199,7 +272,17 @@ export async function dailyRunGather(options, { run = runAdmin } = {}) {
|
|
|
199
272
|
["reward-activity", "--date", yesterday, "--days", "7"],
|
|
200
273
|
"reward-activity.json",
|
|
201
274
|
);
|
|
202
|
-
|
|
275
|
+
const rewardDiagnostics = collectDiagnosticPages({ name: "reward-diagnostics",
|
|
276
|
+
command: ["reward-diagnostics", "--days", "1"], since: diagnosticUntil - 86400,
|
|
277
|
+
until: diagnosticUntil, step, failLast });
|
|
278
|
+
writeAdminJsonFile(path.join(dir, "reward-diagnostics-summary.json"), diagnosticCollectionSummary(rewardDiagnostics), { privateFile: true });
|
|
279
|
+
steps.push({ name: "reward-diagnostics-summary", file: "reward-diagnostics-summary.json", ok: true });
|
|
280
|
+
const jevUntil = Math.floor(diagnosticUntil / 86400) * 86400;
|
|
281
|
+
const jevReview = collectDiagnosticPages({ name: "jev-review", jevReview: true,
|
|
282
|
+
command: ["jev-routing", "review", "--since", String(jevUntil - 86400), "--limit", "20"],
|
|
283
|
+
since: jevUntil - 86400, until: jevUntil, step, failLast });
|
|
284
|
+
writeAdminJsonFile(path.join(dir, "jev-review-summary.json"), diagnosticCollectionSummary(jevReview), { privateFile: true });
|
|
285
|
+
steps.push({ name: "jev-review-summary", file: "jev-review-summary.json", ok: true });
|
|
203
286
|
// One pending queue covers every Build approval type. Exhaust its cursor;
|
|
204
287
|
// an empty/malformed receipt must not look like an empty queue.
|
|
205
288
|
const buildReviews = { complete: false, pages: 0, items: 0 };
|
|
@@ -222,10 +305,7 @@ export async function dailyRunGather(options, { run = runAdmin } = {}) {
|
|
|
222
305
|
!(data.nextCursor === null ||
|
|
223
306
|
(typeof data.nextCursor === "string" && /^\d+(\.\d\.\d+)?$/.test(data.nextCursor)))
|
|
224
307
|
) {
|
|
225
|
-
|
|
226
|
-
ok: false,
|
|
227
|
-
error: "Invalid pending Build review page: expected filter, items and nextCursor.",
|
|
228
|
-
});
|
|
308
|
+
failLast("Invalid pending Build review page: expected filter, items and nextCursor.");
|
|
229
309
|
break;
|
|
230
310
|
}
|
|
231
311
|
buildReviews.items += data.items.length;
|
|
@@ -234,6 +314,7 @@ export async function dailyRunGather(options, { run = runAdmin } = {}) {
|
|
|
234
314
|
break;
|
|
235
315
|
}
|
|
236
316
|
if (reviewCursors.has(data.nextCursor) || buildReviews.pages >= 100) {
|
|
317
|
+
collection.invalidate(steps.at(-1).name);
|
|
237
318
|
steps.push({
|
|
238
319
|
name: "build-reviews-pagination",
|
|
239
320
|
ok: false,
|
|
@@ -249,20 +330,22 @@ export async function dailyRunGather(options, { run = runAdmin } = {}) {
|
|
|
249
330
|
// management approvals (birthdates, teacher signups, meetups) with their
|
|
250
331
|
// flags: a required report section (agent-guides/lumine-admin.md)
|
|
251
332
|
step("approvals-pending", ["approvals", "list", "--status", "pending"], "approvals-pending.json");
|
|
252
|
-
step("todos", ["todo", "list"], "todos.json");
|
|
253
333
|
step("personal-followups", ["followup", "list"], "personal-followups.json");
|
|
254
|
-
|
|
255
|
-
|
|
334
|
+
const wake = collectDiagnosticPages({ name: "owner-wake", ownerTrace: true,
|
|
335
|
+
command: ["owner-trace", "--since", String(diagnosticUntil - 86400), "--wake", "--limit", "2000"],
|
|
336
|
+
since: diagnosticUntil - 86400, until: diagnosticUntil, step, failLast });
|
|
256
337
|
// A phone tab can live longer than the daily window. Retain its version,
|
|
257
338
|
// browser and device context instead of silently calling it unknown.
|
|
258
|
-
const wakeSessions =
|
|
339
|
+
const wakeSessions = collectDiagnosticPages({ name: "owner-wake-sessions", ownerTrace: true,
|
|
340
|
+
command: ["owner-trace", "--since", String(diagnosticUntil - 30 * 86400), "--type", "session", "--limit", "2000"],
|
|
341
|
+
since: diagnosticUntil - 30 * 86400, until: diagnosticUntil, step, failLast });
|
|
259
342
|
const ownerWake = summarizeOwnerViewport(
|
|
260
|
-
wake
|
|
261
|
-
wakeSessions
|
|
343
|
+
wake,
|
|
344
|
+
wakeSessions,
|
|
262
345
|
);
|
|
263
346
|
ownerWake.collectionFailures = [
|
|
264
|
-
...(!wake.
|
|
265
|
-
...(!wakeSessions.
|
|
347
|
+
...(!wake.complete ? ["owner-wake"] : []),
|
|
348
|
+
...(!wakeSessions.complete ? ["owner-wake-sessions"] : []),
|
|
266
349
|
];
|
|
267
350
|
if (ownerWake.collectionFailures.length) {
|
|
268
351
|
ownerWake.coverage = ownerWake.usableSamples ? "partial" : "unavailable";
|
|
@@ -284,6 +367,7 @@ export async function dailyRunGather(options, { run = runAdmin } = {}) {
|
|
|
284
367
|
let cursor = "";
|
|
285
368
|
let pages = 0;
|
|
286
369
|
let botOk = true;
|
|
370
|
+
const botCursors = new Set();
|
|
287
371
|
for (;;) {
|
|
288
372
|
pages += 1;
|
|
289
373
|
const file = `bot-output-${pages}.json`;
|
|
@@ -297,12 +381,18 @@ export async function dailyRunGather(options, { run = runAdmin } = {}) {
|
|
|
297
381
|
break;
|
|
298
382
|
}
|
|
299
383
|
const data = res.json?.data || {};
|
|
384
|
+
if (!Array.isArray(data.chatMessages) || !Array.isArray(data.comments) || typeof data.pagination?.exhausted !== "boolean") {
|
|
385
|
+
botOk = false;
|
|
386
|
+
failLast("Invalid bot-output page: rows and explicit exhaustion are required.");
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
300
389
|
chatMessages.push(...(data.chatMessages || []));
|
|
301
390
|
comments.push(...(data.comments || []));
|
|
302
391
|
const p = data.pagination || {};
|
|
303
392
|
if (p.exhausted && !data.chatTruncated && !data.commentsTruncated) break;
|
|
304
393
|
cursor = p.nextCursor || "";
|
|
305
|
-
if (!cursor || pages
|
|
394
|
+
if (!cursor || botCursors.has(cursor) || pages >= 50) {
|
|
395
|
+
collection.invalidate(steps.at(-1).name);
|
|
306
396
|
botOk = false;
|
|
307
397
|
steps.push({
|
|
308
398
|
name: "bot-output",
|
|
@@ -311,6 +401,7 @@ export async function dailyRunGather(options, { run = runAdmin } = {}) {
|
|
|
311
401
|
});
|
|
312
402
|
break;
|
|
313
403
|
}
|
|
404
|
+
botCursors.add(cursor);
|
|
314
405
|
}
|
|
315
406
|
if (botOk) renderBotOutput({ chatMessages, comments }, path.join(dir, "bot-chats.txt"));
|
|
316
407
|
|
|
@@ -319,12 +410,20 @@ export async function dailyRunGather(options, { run = runAdmin } = {}) {
|
|
|
319
410
|
durationMs: Math.round(performance.now() - startedAt),
|
|
320
411
|
dir,
|
|
321
412
|
yesterdayUtc: yesterday,
|
|
413
|
+
costDateUtc: yesterday,
|
|
414
|
+
collection: { id: collection.collectionId, startedAt: collection.startedAt, phase: collection.phase, warnings: collection.warnings },
|
|
415
|
+
personalQueue,
|
|
416
|
+
paperQueue,
|
|
322
417
|
buildReviews,
|
|
418
|
+
rewardDiagnostics: diagnosticCollectionSummary(rewardDiagnostics),
|
|
419
|
+
jevReview: diagnosticCollectionSummary(jevReview),
|
|
323
420
|
ownerWake: {
|
|
324
421
|
coverage: ownerWake.coverage,
|
|
325
422
|
truncated: ownerWake.truncated,
|
|
326
423
|
largeGapCycles: ownerWake.largeGapCycles,
|
|
327
424
|
reportFile: path.join(dir, "owner-wake-summary.md"),
|
|
425
|
+
collection: diagnosticCollectionSummary(wake),
|
|
426
|
+
sessionCollection: diagnosticCollectionSummary(wakeSessions),
|
|
328
427
|
},
|
|
329
428
|
botOutput: {
|
|
330
429
|
complete: botOk,
|
|
@@ -334,7 +433,8 @@ export async function dailyRunGather(options, { run = runAdmin } = {}) {
|
|
|
334
433
|
readFile: botOk ? path.join(dir, "bot-chats.txt") : null,
|
|
335
434
|
},
|
|
336
435
|
steps,
|
|
337
|
-
failed: steps.filter((s) => !s.ok).map((s) => s.name),
|
|
436
|
+
failed: steps.filter((s) => !s.ok && !s.deferred).map((s) => s.name),
|
|
437
|
+
deferred: steps.filter((s) => s.deferred).map((s) => ({ name: s.name, reason: s.reason })),
|
|
338
438
|
};
|
|
339
439
|
fs.writeFileSync(
|
|
340
440
|
path.join(dir, "gather.json"),
|
|
@@ -351,8 +451,8 @@ export async function dailyRunGather(options, { run = runAdmin } = {}) {
|
|
|
351
451
|
{ privateFile: true },
|
|
352
452
|
);
|
|
353
453
|
return {
|
|
354
|
-
ok: summary.failed.length === 0,
|
|
355
|
-
status: summary.failed.length ? "partial_failure" : "success",
|
|
454
|
+
ok: summary.failed.length === 0 && summary.deferred.length === 0,
|
|
455
|
+
status: summary.failed.length ? "partial_failure" : summary.deferred.length ? "deferred" : "success",
|
|
356
456
|
data: summary,
|
|
357
457
|
};
|
|
358
458
|
}
|
|
@@ -450,10 +550,37 @@ nav{position:fixed;top:0;left:0;bottom:0;width:250px;overflow:auto;background:va
|
|
|
450
550
|
nav h4{margin:0 0 10px;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}nav a{display:block;padding:4px 6px;border-radius:6px;color:var(--fg);text-decoration:none;font-size:13px}nav a:hover{background:var(--line)}
|
|
451
551
|
main{margin-left:250px;padding:28px 40px;max-width:1100px}a{color:var(--link)}table{border-collapse:collapse;margin:10px 0;display:block;overflow-x:auto}th,td{border:1px solid var(--line);padding:6px 9px;text-align:left;vertical-align:top}
|
|
452
552
|
code{background:var(--code);padding:1px 5px;border-radius:4px;font-size:.9em}pre{background:var(--code);padding:12px;border-radius:8px;overflow:auto}h2{border-bottom:1px solid var(--line);padding-bottom:4px;margin-top:34px}
|
|
553
|
+
.relay-actions{border:1px solid var(--line);border-radius:12px;padding:16px;margin:20px 0;background:var(--nav)}
|
|
554
|
+
.relay-actions h3{margin:0 0 8px}.relay-actions p{margin:8px 0}.relay-buttons{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,230px),1fr));gap:8px}
|
|
555
|
+
.relay-actions button{font:inherit;font-size:1rem;text-align:left;min-height:44px;padding:10px 12px;border:1px solid var(--line);border-radius:8px;background:var(--bg);color:var(--fg);cursor:pointer;overflow-wrap:anywhere}
|
|
556
|
+
.relay-actions button:hover{border-color:var(--link)}.relay-actions button:focus-visible{outline:2px solid var(--link);outline-offset:2px}.relay-actions button:disabled{cursor:wait;opacity:.65}
|
|
557
|
+
.relay-actions .relay-master{grid-column:1/-1;font-weight:650;border-color:var(--link);color:var(--link)}.relay-actions [role=status]{min-height:1.55em;color:var(--muted);font-size:1rem}
|
|
558
|
+
.relay-prompt-source{width:100%;min-height:180px;padding:12px;font:inherit;color:var(--fg);background:var(--bg);border:1px solid var(--line);border-radius:8px}
|
|
453
559
|
@media (max-width:760px){nav{position:static;width:auto;height:auto;border-right:0;border-bottom:1px solid var(--line)}main{margin-left:0;padding:16px}}
|
|
454
560
|
</style></head><body><nav><h4>Sections</h4><div id="toc"></div><h4 style="margin-top:18px">Source</h4><a href="source.html">Markdown</a></nav><main id="content">Loading…</main>
|
|
455
561
|
<script src="marked.min.js"></script><script>
|
|
456
562
|
fetch('daily-management-report.md',{cache:'no-store'}).then(r=>{if(!r.ok)throw new Error('Report source unavailable');return r.text()}).then(md=>{const el=document.getElementById('content');el.innerHTML=marked.parse(md);const toc=document.getElementById('toc');let i=0;el.querySelectorAll('h1,h2').forEach(h=>{h.id='s'+(i++);if(h.tagName==='H2'){const a=document.createElement('a');a.href='#'+h.id;a.textContent=h.textContent;toc.appendChild(a)}})}).catch(()=>{document.getElementById('content').textContent='The report could not load. Open Markdown under Source, or reload this page.'});
|
|
563
|
+
document.getElementById('content').addEventListener('click',async event=>{
|
|
564
|
+
const button=event.target.closest('button[data-copy-prompt]');
|
|
565
|
+
if(!button||button.disabled)return;
|
|
566
|
+
const group=button.closest('.relay-actions');
|
|
567
|
+
const source=document.getElementById(button.dataset.copyPrompt);
|
|
568
|
+
const status=group?.querySelector('[role=status]');
|
|
569
|
+
if(!group||!status||!source?.matches('textarea.relay-prompt-source')||!group.contains(source))return;
|
|
570
|
+
const buttons=Array.from(group.querySelectorAll('button[data-copy-prompt]'));
|
|
571
|
+
buttons.forEach(b=>{b.disabled=true});
|
|
572
|
+
try{
|
|
573
|
+
await navigator.clipboard.writeText(source.value);
|
|
574
|
+
status.textContent=button.textContent.replace(/^Copy: /,'')+' copied. Paste it into the agent chat you choose.';
|
|
575
|
+
}catch{
|
|
576
|
+
source.hidden=false;
|
|
577
|
+
source.focus();
|
|
578
|
+
source.select();
|
|
579
|
+
status.textContent='Automatic copy is unavailable. The prompt is selected below; copy it with your keyboard.';
|
|
580
|
+
}finally{
|
|
581
|
+
buttons.forEach(b=>{b.disabled=false});
|
|
582
|
+
}
|
|
583
|
+
});
|
|
457
584
|
</script></body></html>`;
|
|
458
585
|
}
|
|
459
586
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Fixed-window keyset collection. Every page stays a separate acknowledged
|
|
2
|
+
// source; aggregates are conveniences, never proof that the source was read.
|
|
3
|
+
function jevPosition(cursor, since, until) {
|
|
4
|
+
try {
|
|
5
|
+
if (typeof cursor !== "string" || !/^[A-Za-z0-9_-]{1,1024}$/.test(cursor)) return null;
|
|
6
|
+
const value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
7
|
+
if (value.version !== 1 || value.kind !== "jev-routing-review" ||
|
|
8
|
+
value.since !== since || value.until !== until || value.includeSynthetic !== false ||
|
|
9
|
+
!Number.isSafeInteger(value.createdAt) || value.createdAt < since || value.createdAt >= until ||
|
|
10
|
+
!Number.isSafeInteger(value.id) || value.id <= 0) return null;
|
|
11
|
+
return value;
|
|
12
|
+
} catch { return null; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function collectDiagnosticPages({ name, command, since, until, ownerTrace = false, jevReview = false, step, failLast }) {
|
|
16
|
+
const events = [];
|
|
17
|
+
const ids = new Set();
|
|
18
|
+
const cursors = new Set();
|
|
19
|
+
const sourceFiles = [];
|
|
20
|
+
let cursor = null;
|
|
21
|
+
let pages = 0;
|
|
22
|
+
let duplicateRows = 0;
|
|
23
|
+
let complete = false;
|
|
24
|
+
let deferred = false;
|
|
25
|
+
let failure = null;
|
|
26
|
+
const pageLimit = ownerTrace ? 20 : 100;
|
|
27
|
+
for (;;) {
|
|
28
|
+
pages++;
|
|
29
|
+
const args = [...command, "--until", String(until)];
|
|
30
|
+
if (cursor !== null) args.push("--cursor", String(cursor));
|
|
31
|
+
const file = `${name}${pages === 1 ? "" : `-${pages}`}.json`;
|
|
32
|
+
const result = step(`${name}${pages === 1 ? "" : `-page-${pages}`}`, args, file);
|
|
33
|
+
sourceFiles.push(file);
|
|
34
|
+
if (!result.ok) {
|
|
35
|
+
deferred = Boolean(result.deferred);
|
|
36
|
+
failure = deferred ? result.reason : "source-read-failed";
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
const data = result.json?.data;
|
|
40
|
+
const next = data?.nextCursor;
|
|
41
|
+
const rows = jevReview ? data?.items : data?.events;
|
|
42
|
+
const window = jevReview ? data?.collectionWindow : data;
|
|
43
|
+
const validCursor = next === null || (jevReview ? Boolean(jevPosition(next, since, until)) : ownerTrace
|
|
44
|
+
? typeof next === "string" && /^\d+:\d+$/.test(next)
|
|
45
|
+
: Number.isSafeInteger(next) && next > 0 && (cursor === null || next < cursor));
|
|
46
|
+
if (!Array.isArray(rows) || window?.since !== since || window?.until !== until ||
|
|
47
|
+
!validCursor || data.coverage === "unavailable" ||
|
|
48
|
+
(jevReview && (window.includeSynthetic !== false || data.after !== cursor ||
|
|
49
|
+
data.hasMore !== (next !== null) || data.coverage?.scope !== "page" ||
|
|
50
|
+
data.coverage?.status !== (next === null ? "exhausted" : "partial"))) ||
|
|
51
|
+
(ownerTrace && data.truncated !== (next !== null))) {
|
|
52
|
+
failure = "Invalid diagnostic page or fixed-window coverage; deploy the matching API before collecting.";
|
|
53
|
+
failLast(failure);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
const idOf = row => jevReview ? row.eventId : row.id;
|
|
57
|
+
if (rows.some(row => !Number.isSafeInteger(idOf(row)) || idOf(row) <= 0)) {
|
|
58
|
+
failure = "Diagnostic page contains an invalid event ID.";
|
|
59
|
+
failLast(failure);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
if (jevReview && cursor !== null && next !== null) {
|
|
63
|
+
const old = jevPosition(cursor, since, until), current = jevPosition(next, since, until);
|
|
64
|
+
if (!(current.createdAt > old.createdAt || current.createdAt === old.createdAt && current.id > old.id)) {
|
|
65
|
+
failure = "JEV review cursor did not advance.";
|
|
66
|
+
failLast(failure);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (ownerTrace && cursor !== null && next !== null) {
|
|
71
|
+
const [oldTime, oldId] = cursor.split(":").map(Number);
|
|
72
|
+
const [time, id] = next.split(":").map(Number);
|
|
73
|
+
if (!(time < oldTime || time === oldTime && id < oldId)) {
|
|
74
|
+
failure = "Owner trace cursor did not advance.";
|
|
75
|
+
failLast(failure);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const row of rows) {
|
|
80
|
+
if (ids.has(idOf(row))) { duplicateRows++; continue; }
|
|
81
|
+
ids.add(idOf(row));
|
|
82
|
+
events.push(row);
|
|
83
|
+
}
|
|
84
|
+
if (next === null) { complete = true; break; }
|
|
85
|
+
if (cursors.has(next) || pages >= pageLimit) {
|
|
86
|
+
failure = `Diagnostic pagination repeated a cursor or reached its ${pageLimit}-page bound.`;
|
|
87
|
+
failLast(failure);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
cursors.add(next);
|
|
91
|
+
cursor = next;
|
|
92
|
+
}
|
|
93
|
+
if (ownerTrace) events.sort((a, b) => a.serverTime - b.serverTime || a.id - b.id);
|
|
94
|
+
return { since, until, complete, pages, rows: events.length, duplicateRows,
|
|
95
|
+
sourceFiles, deferred, failure, events, truncated: !complete };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function diagnosticCollectionSummary(collection) {
|
|
99
|
+
const { events, ...summary } = collection;
|
|
100
|
+
const outcomes = {};
|
|
101
|
+
for (const row of events) {
|
|
102
|
+
const key = `${row.source || row.type || row.route || "unknown"}:${row.operation || ""}:${row.outcome || row.selectedSource || ""}`;
|
|
103
|
+
outcomes[key] = (outcomes[key] || 0) + 1;
|
|
104
|
+
}
|
|
105
|
+
return { ...summary, outcomes, note: "Metadata only. Read the source pages; this summary does not acknowledge review or prove member outcomes." };
|
|
106
|
+
}
|