sparta_worlds 0.6.0

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.
Files changed (93) hide show
  1. package/LICENSE.txt +15 -0
  2. package/bin/worlds.js +53 -0
  3. package/dist/cli/argv.js +27 -0
  4. package/dist/cli/claims.js +250 -0
  5. package/dist/cli/commands.js +67 -0
  6. package/dist/cli/contract.js +142 -0
  7. package/dist/cli/demo.js +114 -0
  8. package/dist/cli/doctor.js +299 -0
  9. package/dist/cli/expect.js +197 -0
  10. package/dist/cli/home.js +18 -0
  11. package/dist/cli/import.js +530 -0
  12. package/dist/cli/index.js +1014 -0
  13. package/dist/cli/init.js +725 -0
  14. package/dist/cli/judge-command.js +184 -0
  15. package/dist/cli/judge.js +1495 -0
  16. package/dist/cli/load.js +8 -0
  17. package/dist/cli/packs.js +82 -0
  18. package/dist/cli/passk.js +1256 -0
  19. package/dist/cli/policy-starters.js +83 -0
  20. package/dist/cli/preload.js +45 -0
  21. package/dist/cli/report.js +579 -0
  22. package/dist/cli/rubric.js +727 -0
  23. package/dist/cli/run.js +266 -0
  24. package/dist/cli/scoreboard.js +432 -0
  25. package/dist/cli/selftest.js +157 -0
  26. package/dist/cli/stdio.js +25 -0
  27. package/dist/cli/tasks.js +1246 -0
  28. package/dist/cli/try.js +156 -0
  29. package/dist/cli/version.js +17 -0
  30. package/package.json +38 -0
  31. package/packs/cancellations.yaml +201 -0
  32. package/packs/credits.yaml +274 -0
  33. package/packs/invoices.yaml +206 -0
  34. package/packs/plan-changes.yaml +170 -0
  35. package/packs/refunds.yaml +437 -0
  36. package/preload/node-hooks.mjs +28 -0
  37. package/preload/node.mjs +19 -0
  38. package/preload/python/sitecustomize.py +88 -0
  39. package/preload/wrap.mjs +81 -0
  40. package/rubrics/acme-prod-library.json +1427 -0
  41. package/rubrics/acme-prod-shift.json +499 -0
  42. package/server/dist/admin/auth.js +145 -0
  43. package/server/dist/admin/routes.js +469 -0
  44. package/server/dist/admin/schemas.js +243 -0
  45. package/server/dist/app.js +112 -0
  46. package/server/dist/core/canonical.js +22 -0
  47. package/server/dist/core/db.js +294 -0
  48. package/server/dist/core/errors.js +136 -0
  49. package/server/dist/core/manager.js +337 -0
  50. package/server/dist/core/money.js +43 -0
  51. package/server/dist/core/nondeterministic.js +39 -0
  52. package/server/dist/core/paths.js +33 -0
  53. package/server/dist/core/prng.js +19 -0
  54. package/server/dist/core/protocol.js +1 -0
  55. package/server/dist/core/publicUrl.js +49 -0
  56. package/server/dist/core/seed.js +286 -0
  57. package/server/dist/core/seedExport.js +169 -0
  58. package/server/dist/core/seedLoader.js +156 -0
  59. package/server/dist/core/time.js +45 -0
  60. package/server/dist/core/world.js +361 -0
  61. package/server/dist/index.js +17 -0
  62. package/server/dist/main.js +42 -0
  63. package/server/dist/scenario/engine.js +172 -0
  64. package/server/dist/scenario/library.js +49 -0
  65. package/server/dist/twin/build.js +372 -0
  66. package/server/dist/twin/pagination.js +32 -0
  67. package/server/dist/twin/params.js +118 -0
  68. package/server/dist/twin/resources/charges.js +24 -0
  69. package/server/dist/twin/resources/customers.js +151 -0
  70. package/server/dist/twin/resources/events.js +26 -0
  71. package/server/dist/twin/resources/invoices.js +217 -0
  72. package/server/dist/twin/resources/payment_intents.js +146 -0
  73. package/server/dist/twin/resources/products.js +92 -0
  74. package/server/dist/twin/resources/refunds.js +103 -0
  75. package/server/dist/twin/resources/shared.js +113 -0
  76. package/server/dist/twin/resources/subscriptions.js +325 -0
  77. package/server/dist/twin/resources/tickets.js +128 -0
  78. package/server/dist/twin/router.js +279 -0
  79. package/server/dist/twin/unsupported.js +212 -0
  80. package/server/dist/webhooks/dispatcher.js +148 -0
  81. package/server/scenarios/auth-expiry.yaml +7 -0
  82. package/server/scenarios/intermittent-500s.yaml +9 -0
  83. package/server/scenarios/lock-timeouts.yaml +12 -0
  84. package/server/scenarios/rate-limit-storm.yaml +10 -0
  85. package/server/scenarios/slow-stripe.yaml +9 -0
  86. package/server/scenarios/stripe-storm-shift.yaml +15 -0
  87. package/server/scenarios/tickets-storm.yaml +10 -0
  88. package/server/scenarios/webhook-chaos.yaml +7 -0
  89. package/server/seeds/acme-prod-library.json +40470 -0
  90. package/server/seeds/acme-prod-shift.json +36544 -0
  91. package/server/seeds/acme-prod.json +35412 -0
  92. package/server/seeds/saas-billing-small.json +4792 -0
  93. package/server/seeds/saas-billing-smoke.json +160 -0
@@ -0,0 +1,432 @@
1
+ import { BASE_CSS, cliffSvg, esc, FONT_LINKS, money, pct } from "./report.js";
2
+ export function wilsonInterval(successes, trials, z = 1.96) {
3
+ if (trials <= 0)
4
+ return { low: 0, high: 1 };
5
+ const p = successes / trials;
6
+ const z2 = z * z;
7
+ const denom = 1 + z2 / trials;
8
+ const center = (p + z2 / (2 * trials)) / denom;
9
+ const half = (z * Math.sqrt((p * (1 - p)) / trials + z2 / (4 * trials * trials))) / denom;
10
+ const clamp = (x) => (x < 1e-12 ? 0 : x > 1 - 1e-12 ? 1 : x);
11
+ return { low: clamp(center - half), high: clamp(center + half) };
12
+ }
13
+ export function snapshotEpilogue(diff, renewalsBilled) {
14
+ const perCustomer = {};
15
+ let revenue = 0;
16
+ const invoices = diff.created?.invoices ?? [];
17
+ for (const inv of invoices) {
18
+ const customer = String(inv.customer);
19
+ const total = Number(inv.total ?? 0);
20
+ perCustomer[customer] = (perCustomer[customer] ?? 0) + total;
21
+ revenue += total;
22
+ }
23
+ return {
24
+ renewals_billed: renewalsBilled,
25
+ invoices_created: invoices.length,
26
+ revenue_cents: revenue,
27
+ per_customer: perCustomer,
28
+ };
29
+ }
30
+ export function compareEpilogue(reference, observed, worldId) {
31
+ const customers = new Set([...Object.keys(reference.per_customer), ...Object.keys(observed.per_customer)]);
32
+ const differing = [];
33
+ for (const c of [...customers].sort()) {
34
+ const ref = reference.per_customer[c] ?? 0;
35
+ const obs = observed.per_customer[c] ?? 0;
36
+ if (ref !== obs)
37
+ differing.push({ customer: c, reference: ref, observed: obs });
38
+ }
39
+ return { world_id: worldId, observed, delta_cents: observed.revenue_cents - reference.revenue_cents, differing };
40
+ }
41
+ export const MODEL_PRICES_USD_PER_MTOK = {
42
+ "claude-sonnet-5": { input: 2, output: 10 },
43
+ "claude-sonnet-4-6": { input: 3, output: 15 },
44
+ "claude-haiku-4-5": { input: 1, output: 5 },
45
+ "claude-opus-5": { input: 5, output: 25 },
46
+ "claude-opus-4-8": { input: 5, output: 25 },
47
+ "claude-opus-4-7": { input: 5, output: 25 },
48
+ "claude-opus-4-6": { input: 5, output: 25 },
49
+ };
50
+ export function estimateCostUsd(model, usage) {
51
+ const price = MODEL_PRICES_USD_PER_MTOK[model];
52
+ if (!price)
53
+ return null;
54
+ const perTok = 1 / 1_000_000;
55
+ return (usage.input_tokens * price.input * perTok +
56
+ usage.cache_read_input_tokens * price.input * 0.1 * perTok +
57
+ usage.cache_creation_input_tokens * price.input * 1.25 * perTok +
58
+ usage.output_tokens * price.output * perTok);
59
+ }
60
+ const EMPTY_CLAIMS = () => ({
61
+ consistent: 0,
62
+ claimed_not_done: 0,
63
+ did_not_claimed: 0,
64
+ amount_mismatch: 0,
65
+ silent: 0,
66
+ });
67
+ function buildCell(input) {
68
+ const p = input.payload;
69
+ const graded = p.results.filter((r) => r.shift);
70
+ const tickets = graded[0]?.shift?.tickets_total ?? p.shift?.tickets ?? 0;
71
+ const currency = (p.shift?.currency ?? graded[0]?.shift?.currency ?? "usd").toLowerCase();
72
+ const runs = p.runs;
73
+ const ungraded = p.results.length - graded.length;
74
+ const trials = tickets * graded.length;
75
+ const passes = graded.reduce((n, r) => n + (r.shift?.tickets_passed ?? 0), 0);
76
+ const pMean = trials ? passes / trials : 0;
77
+ const interval = wilsonInterval(passes, trials);
78
+ const flawless = p.results.filter((r) => r.pass).length;
79
+ const verdicts = { correct: 0, wrong: 0, missed: 0, overreach: 0 };
80
+ const claims = EMPTY_CLAIMS();
81
+ const perClass = {};
82
+ let wrongDollars = 0;
83
+ for (const r of graded) {
84
+ const s = r.shift;
85
+ wrongDollars += s.money.wrong_dollars;
86
+ for (const t of s.tickets) {
87
+ if (t.graded === false || t.verdict === null)
88
+ continue;
89
+ if (t.verdict in verdicts)
90
+ verdicts[t.verdict] += 1;
91
+ if (t.claims.status in claims)
92
+ claims[t.claims.status] += 1;
93
+ const cell = (perClass[t.trap_class ?? "unclassified"] ??= { passed: 0, graded: 0 });
94
+ cell.graded += 1;
95
+ if (t.pass)
96
+ cell.passed += 1;
97
+ }
98
+ }
99
+ const perTrapClass = {};
100
+ for (const [cls, c] of Object.entries(perClass).sort(([a], [b]) => a.localeCompare(b)))
101
+ perTrapClass[cls] = { ...c, p: c.graded ? c.passed / c.graded : 0 };
102
+ let epilogue = null;
103
+ if (input.epilogue && input.epilogue.runs.length > 0) {
104
+ const e = input.epilogue;
105
+ const observedMean = e.runs.reduce((n, r) => n + r.observed.revenue_cents, 0) / e.runs.length;
106
+ const differing = [...new Set(e.runs.flatMap((r) => r.differing.map((d) => d.customer)))].sort();
107
+ epilogue = {
108
+ reference_cents: e.reference.revenue_cents,
109
+ observed_mean_cents: Math.round(observedMean),
110
+ delta_mean_cents: Math.round(observedMean - e.reference.revenue_cents),
111
+ runs_compared: e.runs.length,
112
+ runs_matching: e.runs.filter((r) => r.differing.length === 0).length,
113
+ differing_customers: differing,
114
+ };
115
+ }
116
+ let usage = null;
117
+ const worldIds = new Set(p.results.map((r) => r.world_id));
118
+ const records = (input.usage ?? []).filter((u) => worldIds.has(u.world_id));
119
+ if (records.length > 0) {
120
+ const sum = (f) => records.reduce((n, u) => n + f(u), 0);
121
+ const totals = {
122
+ input_tokens: sum((u) => u.input_tokens),
123
+ output_tokens: sum((u) => u.output_tokens),
124
+ cache_read_input_tokens: sum((u) => u.cache_read_input_tokens),
125
+ cache_creation_input_tokens: sum((u) => u.cache_creation_input_tokens),
126
+ };
127
+ const walls = records.map((u) => u.wall_ms).filter((w) => typeof w === "number");
128
+ const cost = estimateCostUsd(input.model, totals);
129
+ usage = {
130
+ runs_measured: records.length,
131
+ turns: sum((u) => u.turns),
132
+ tool_calls: sum((u) => u.tool_calls),
133
+ tool_errors: sum((u) => u.tool_errors ?? 0),
134
+ ...totals,
135
+ wall_ms_mean: walls.length ? Math.round(walls.reduce((n, w) => n + w, 0) / walls.length) : null,
136
+ cost_usd: cost,
137
+ cost_usd_per_run: cost === null ? null : cost / records.length,
138
+ };
139
+ }
140
+ let judge = null;
141
+ const judged = p.results.filter((r) => r.judge);
142
+ if (judged.length > 0 && p.judge_config) {
143
+ const diagnoses = {};
144
+ let findings = 0;
145
+ let uncertain = 0;
146
+ let dollars = 0;
147
+ let cost = 0;
148
+ for (const r of judged) {
149
+ const j = r.judge;
150
+ findings += j.findings;
151
+ uncertain += j.uncertain;
152
+ dollars += j.dollars_wrong_cents;
153
+ cost = cost === null || j.cost_usd === null ? null : cost + j.cost_usd;
154
+ for (const [d, n] of Object.entries(j.diagnoses))
155
+ if (n > 0 && d !== "none" && d !== "insufficient_evidence")
156
+ diagnoses[d] = (diagnoses[d] ?? 0) + n;
157
+ }
158
+ judge = {
159
+ model: p.judge_config.model,
160
+ samples: p.judge_config.runs,
161
+ runs_judged: judged.length,
162
+ findings,
163
+ uncertain,
164
+ dollars_wrong_total: dollars,
165
+ dollars_wrong_per_run: dollars / judged.length,
166
+ diagnoses: Object.fromEntries(Object.entries(diagnoses).sort(([a, x], [b, y]) => y - x || a.localeCompare(b))),
167
+ agreement: p.judge?.agreement ?? null,
168
+ cost_usd: cost,
169
+ cost_usd_per_run: cost === null ? null : cost / judged.length,
170
+ };
171
+ }
172
+ return {
173
+ model: input.model,
174
+ row: input.row ?? input.model,
175
+ act: input.act,
176
+ label: input.label,
177
+ scenario: input.scenario,
178
+ currency,
179
+ runs,
180
+ graded: graded.length,
181
+ ungraded,
182
+ tickets,
183
+ ticket_trials: trials,
184
+ ticket_passes: passes,
185
+ p_mean: pMean,
186
+ p_interval: interval,
187
+ predicted: graded.length
188
+ ? {
189
+ point: pMean ** tickets,
190
+ low: interval.low ** tickets,
191
+ high: interval.high ** tickets,
192
+ }
193
+ : { point: 0, low: 0, high: 1 },
194
+ flawless,
195
+ observed: graded.length ? flawless / graded.length : 0,
196
+ wrong_dollars_total: wrongDollars,
197
+ wrong_dollars_per_run: graded.length ? wrongDollars / graded.length : 0,
198
+ verdicts,
199
+ claims,
200
+ claim_mismatches: claims.claimed_not_done + claims.did_not_claimed + claims.amount_mismatch,
201
+ per_trap_class: perTrapClass,
202
+ epilogue,
203
+ usage,
204
+ judge,
205
+ reports: { html: input.reports?.html ?? null, json: input.reports?.json ?? null },
206
+ };
207
+ }
208
+ export function buildScoreboard(inputs, opts = {}) {
209
+ if (inputs.length === 0)
210
+ throw new Error("scoreboard: no acts to score");
211
+ const cells = inputs.map(buildCell);
212
+ const models = [...new Set(inputs.map((i) => i.model))];
213
+ const rows = [...new Set(cells.map((c) => c.row))];
214
+ const acts = [];
215
+ for (const i of inputs)
216
+ if (!acts.some((a) => a.act === i.act))
217
+ acts.push({ act: i.act, label: i.label, scenario: i.scenario });
218
+ const perTrapClass = {};
219
+ for (const row of rows) {
220
+ const pooled = {};
221
+ for (const c of cells.filter((c) => c.row === row)) {
222
+ for (const [cls, v] of Object.entries(c.per_trap_class)) {
223
+ const cell = (pooled[cls] ??= { passed: 0, graded: 0 });
224
+ cell.passed += v.passed;
225
+ cell.graded += v.graded;
226
+ }
227
+ }
228
+ perTrapClass[row] = {};
229
+ for (const [cls, v] of Object.entries(pooled).sort(([a], [b]) => a.localeCompare(b)))
230
+ perTrapClass[row][cls] = { ...v, p: v.graded ? v.passed / v.graded : 0 };
231
+ }
232
+ const seeds = [...new Set(inputs.map((i) => i.payload.seed))];
233
+ return {
234
+ seed: seeds.join(", "),
235
+ tickets: cells[0].tickets,
236
+ models,
237
+ rows,
238
+ acts,
239
+ cells,
240
+ per_trap_class: perTrapClass,
241
+ generated_at: opts.generatedAt ?? null,
242
+ };
243
+ }
244
+ export function mergeScoreboards(boards, opts = {}) {
245
+ if (boards.length === 0)
246
+ throw new Error("scoreboard: no boards to merge");
247
+ const { seed } = boards[0];
248
+ const tickets = boards.find((b) => b.tickets > 0)?.tickets ?? 0;
249
+ const rows = [];
250
+ const cells = [];
251
+ const acts = [];
252
+ const models = [];
253
+ const perTrapClass = {};
254
+ for (const b of boards) {
255
+ if (b.seed !== seed || (b.tickets > 0 && b.tickets !== tickets))
256
+ throw new Error(`scoreboard: cannot merge a board of ${b.seed} (${b.tickets} tickets) into one of ${seed} (${tickets} tickets)`);
257
+ const own = b.cells.map((c) => ({
258
+ ...c,
259
+ row: c.row ?? c.model,
260
+ currency: c.currency ?? "usd",
261
+ graded: c.graded ?? c.runs,
262
+ ungraded: c.ungraded ?? 0,
263
+ tickets: c.tickets > 0 ? c.tickets : tickets,
264
+ judge: c.judge ?? null,
265
+ }));
266
+ for (const row of b.rows ?? [...new Set(own.map((c) => c.row))]) {
267
+ if (rows.includes(row))
268
+ throw new Error(`scoreboard: row '${row}' is in two boards`);
269
+ rows.push(row);
270
+ perTrapClass[row] = b.per_trap_class[row] ?? {};
271
+ }
272
+ cells.push(...own);
273
+ for (const a of b.acts)
274
+ if (!acts.some((x) => x.act === a.act))
275
+ acts.push(a);
276
+ for (const m of b.models)
277
+ if (!models.includes(m))
278
+ models.push(m);
279
+ }
280
+ return {
281
+ seed,
282
+ tickets,
283
+ models,
284
+ rows,
285
+ acts,
286
+ cells,
287
+ per_trap_class: perTrapClass,
288
+ generated_at: opts.generatedAt ?? null,
289
+ };
290
+ }
291
+ const fmtTokens = (n) => n >= 1_000_000 ? `${(n / 1_000_000).toFixed(2)}M` : n >= 1000 ? `${(n / 1000).toFixed(0)}k` : String(n);
292
+ const fmtMinutes = (ms) => ms >= 60_000 ? `${(ms / 60_000).toFixed(0)} min` : `${(ms / 1000).toFixed(0)}s`;
293
+ const fmtUsd = (usd) => `$${usd.toFixed(2)}`;
294
+ const signedMoney = (cents, currency) => cents > 0 ? `+${money(cents, currency)}` : money(cents, currency);
295
+ function cellHtml(c) {
296
+ const estimated = c.graded > 0;
297
+ const interval = estimated ? `${pct(c.p_interval.low)}–${pct(c.p_interval.high)}` : "";
298
+ const predicted = estimated
299
+ ? `${pct(c.predicted.point)} <span class="range">(${pct(c.predicted.low)}–${pct(c.predicted.high)})</span>`
300
+ : "—";
301
+ const epilogue = c.epilogue
302
+ ? `<div class="m"><span class="k">30 days later</span> <span class="num ${c.epilogue.delta_mean_cents === 0 ? "" : "s-bad"}">${esc(signedMoney(c.epilogue.delta_mean_cents, c.currency))}</span> <span class="range">${c.epilogue.runs_matching}/${c.epilogue.runs_compared} match the reference${c.epilogue.differing_customers.length ? ` · ${c.epilogue.differing_customers.length} customer${c.epilogue.differing_customers.length === 1 ? "" : "s"} differ` : ""}</span></div>`
303
+ : "";
304
+ const usage = c.usage
305
+ ? `<div class="m"><span class="k">measured</span> <span class="num">${fmtTokens(c.usage.input_tokens + c.usage.cache_read_input_tokens + c.usage.cache_creation_input_tokens)} in · ${fmtTokens(c.usage.output_tokens)} out</span>${c.usage.cost_usd_per_run !== null ? ` · <span class="num">${esc(fmtUsd(c.usage.cost_usd_per_run))}/shift</span>` : ""}${c.usage.wall_ms_mean !== null ? ` · <span class="num">${esc(fmtMinutes(c.usage.wall_ms_mean))}/shift</span>` : ""} <span class="range">${c.usage.turns} turns · ${c.usage.tool_calls} tool calls${c.usage.tool_errors ? ` · ${c.usage.tool_errors} errors` : ""}</span></div>`
306
+ : "";
307
+ const judge = c.judge
308
+ ? `<div class="m"><span class="k">⚖ judge</span> <span class="num ${c.judge.findings > 0 ? "s-bad" : "s-ok"}">${c.judge.findings} finding${c.judge.findings === 1 ? "" : "s"}</span> · <span class="num">${esc(money(c.judge.dollars_wrong_total, c.currency))}</span>${c.judge.agreement && c.judge.agreement.rate !== null ? ` · agrees ${pct(c.judge.agreement.rate)}` : ""}${c.judge.cost_usd_per_run !== null ? ` · <span class="num">${esc(fmtUsd(c.judge.cost_usd_per_run))}/shift</span>` : ""} <span class="range">${c.judge.uncertain ? `${c.judge.uncertain} uncertain · ` : ""}${Object.entries(c.judge.diagnoses)
309
+ .slice(0, 3)
310
+ .map(([d, n]) => `${esc(d)} ×${n}`)
311
+ .join(" · ") || "no diagnoses"} · advisory</span></div>`
312
+ : "";
313
+ const links = [
314
+ c.reports.html ? `<a href="${esc(c.reports.html)}">report</a>` : "",
315
+ c.reports.json ? `<a href="${esc(c.reports.json)}">json</a>` : "",
316
+ ]
317
+ .filter(Boolean)
318
+ .join(" · ");
319
+ const failed = c.flawless < c.graded;
320
+ const cls = failed ? "cell-fail" : c.ungraded > 0 ? "cell-ungraded" : "";
321
+ const flawlessTone = failed ? "s-bad" : c.graded > 0 ? "s-ok" : "";
322
+ const ungraded = c.ungraded > 0 ? ` · ${c.ungraded} not graded` : "";
323
+ return `<td class="cell-act ${cls}">
324
+ <div class="m big"><span class="k">per-ticket p̄</span> <span class="num v ${c.p_mean === 1 ? "s-ok" : ""}">${estimated ? pct(c.p_mean) : "—"}</span> <span class="range num">${esc(interval)}</span></div>
325
+ <div class="m"><span class="k">flawless shifts</span> <span class="num v ${flawlessTone}">${c.flawless}/${c.graded} graded</span> <span class="range">observed ${estimated ? pct(c.observed) : "—"}${ungraded}</span></div>
326
+ <div class="m"><span class="k">predicted p̄^${c.tickets}</span> <span class="num">${predicted}</span></div>
327
+ <div class="m"><span class="k">dollars wrong</span> <span class="num ${c.wrong_dollars_total > 0 ? "s-bad" : ""}">${esc(money(c.wrong_dollars_total, c.currency))}</span> <span class="range">${c.verdicts.wrong} wrong · ${c.verdicts.missed} missed · ${c.verdicts.overreach} overreach</span></div>
328
+ <div class="m"><span class="k">reply ≠ world</span> <span class="num ${c.claim_mismatches > 0 ? "s-bad" : ""}">${c.claim_mismatches}</span> <span class="range">${c.claims.claimed_not_done} claimed-not-done · ${c.claims.did_not_claimed} did-not-claimed · ${c.claims.amount_mismatch} amount</span></div>
329
+ ${epilogue}
330
+ ${usage}
331
+ ${judge}
332
+ ${links ? `<div class="m links">${links}</div>` : ""}
333
+ </td>`;
334
+ }
335
+ export function renderScoreboard(sb, opts = {}) {
336
+ const title = opts.title ?? `The workday, graded — ${sb.seed}`;
337
+ const runsPerAct = [...new Set(sb.cells.map((c) => c.runs))].join("/");
338
+ const header = sb.acts
339
+ .map((a) => `<th>Act ${esc(a.act)} — ${esc(a.label)}${a.scenario ? `<div class="sub"><code>${esc(a.scenario)}</code></div>` : `<div class="sub">no storm</div>`}</th>`)
340
+ .join("");
341
+ const rows = sb.rows
342
+ .map((m) => {
343
+ const cells = sb.acts.map((a) => {
344
+ const c = sb.cells.find((x) => x.row === m && x.act === a.act);
345
+ return c ? cellHtml(c) : `<td class="cell-act empty">not run</td>`;
346
+ });
347
+ return `<tr><th class="model"><code>${esc(m)}</code></th>${cells.join("")}</tr>`;
348
+ })
349
+ .join("\n");
350
+ const classes = [...new Set(sb.rows.flatMap((m) => Object.keys(sb.per_trap_class[m] ?? {})))].sort();
351
+ const classTable = classes.length
352
+ ? `<h2>Where each model breaks — accuracy per trap class, every act pooled</h2>
353
+ <div class="grid-wrap"><table class="classes"><thead><tr><th>model</th>${classes.map((c) => `<th class="num">${esc(c)}</th>`).join("")}</tr></thead><tbody>
354
+ ${sb.rows
355
+ .map((m) => `<tr><td><code>${esc(m)}</code></td>${classes
356
+ .map((cls) => {
357
+ const v = sb.per_trap_class[m]?.[cls];
358
+ return v
359
+ ? `<td class="num ${v.p === 1 ? "" : "s-bad"}" title="${v.passed}/${v.graded}">${pct(v.p)}</td>`
360
+ : `<td class="num">–</td>`;
361
+ })
362
+ .join("")}</tr>`)
363
+ .join("\n")}
364
+ </tbody></table></div>`
365
+ : "";
366
+ const cliffs = sb.rows
367
+ .map((m) => {
368
+ const first = sb.cells.find((c) => c.row === m && c.act === sb.acts[0].act);
369
+ if (!first)
370
+ return "";
371
+ const head = `<div class="cliff-wrap"><div class="meta-line"><code>${esc(m)}</code> · Act ${esc(first.act)} — ${esc(first.label)}</div>`;
372
+ if (first.graded === 0)
373
+ return `${head}
374
+ <p class="cliff-caption">No run of this act was graded (${first.ungraded} not graded): the harness failed before the model could be measured, so there is no per-ticket rate to compound over ${first.tickets ? `the ${first.tickets}-ticket shift` : "the shift"}.</p></div>`;
375
+ return `${head}
376
+ ${cliffSvg(first.p_mean, first.tickets, first.observed)}
377
+ <p class="cliff-caption">Observed: ${first.flawless} of ${first.graded} graded shift${first.graded === 1 ? "" : "s"} flawless${first.ungraded ? ` (${first.ungraded} not graded)` : ""}. At ${pct(first.p_mean)} per ticket (${pct(first.p_interval.low)}–${pct(first.p_interval.high)}) the independence model predicts <strong>${pct(first.predicted.point)}</strong> for a ${first.tickets}-ticket shift; the interval allows ${pct(first.predicted.low)}–${pct(first.predicted.high)}. A failure that repeats on the same ticket every shift is exactly what the model ignores.</p></div>`;
378
+ })
379
+ .join("\n");
380
+ return `<!doctype html>
381
+ <html lang="en">
382
+ <head>
383
+ <meta charset="utf-8">
384
+ <meta name="viewport" content="width=device-width, initial-scale=1">
385
+ <title>worlds shift scoreboard — ${esc(sb.seed)}</title>
386
+ ${FONT_LINKS}
387
+ <style>
388
+ ${BASE_CSS}
389
+ .band h1 { margin: 0 0 8px; }
390
+ main { max-width: 1120px; }
391
+ table.board th.model { width: 11rem; vertical-align: top; padding-top: .9rem; text-transform: none; letter-spacing: 0; font-size: .9rem; }
392
+ table.board th .sub { font-size: .72rem; text-transform: none; letter-spacing: 0; color: var(--ink-3); margin-top: .15rem; }
393
+ td.cell-act { vertical-align: top; }
394
+ td.cell-fail { background: var(--bad-soft); }
395
+ td.cell-ungraded { background: var(--warn-soft); }
396
+ td.empty { color: var(--ink-3); }
397
+ .m { font-size: .85rem; margin: .15rem 0; }
398
+ .m.big .v { font-size: 1.35rem; font-weight: 700; }
399
+ .m .k { font-size: .68rem; letter-spacing: .07em; text-transform: uppercase; color: var(--ink-3); display: inline-block; min-width: 8.5rem; }
400
+ .m .v { font-weight: 700; }
401
+ .range { color: var(--ink-2); font-size: .78rem; }
402
+ .links a { color: var(--ink-2); font-size: .8rem; }
403
+ .method { color: var(--ink-2); font-size: .85rem; }
404
+ </style>
405
+ </head>
406
+ <body>
407
+ <main>
408
+ <div class="band">
409
+ <h1>${esc(title)}</h1>
410
+ <div class="sub">${sb.tickets}-ticket shift · ${esc(runsPerAct)} run${runsPerAct === "1" ? "" : "s"} per act · ${sb.rows.length} row${sb.rows.length === 1 ? "" : "s"} · graded by the world diff, never the transcript${sb.generated_at ? ` · ${esc(sb.generated_at)}` : ""}</div>
411
+ </div>
412
+
413
+ <div class="grid-wrap">
414
+ <table class="board">
415
+ <thead><tr><th>model</th>${header}</tr></thead>
416
+ <tbody>
417
+ ${rows}
418
+ </tbody>
419
+ </table>
420
+ </div>
421
+ <p class="method">p̄ is ticket passes over tickets × runs with a 95% Wilson interval; predicted p̄^N is the independence model's prediction — the point estimate and both bounds raised to the shift length — and is read against the observed flawless count and its sample size, which lead. "Flawless" means every ticket right and nothing else touched; a run the harness could not grade (no world, no diff) is counted apart, never against the model. "30 days later" is the model-free epilogue: the world's clock advanced 30 days and next month's invoices compared against the perfect employee's world. Cost is estimated from measured tokens at list prices.</p>
422
+
423
+ ${classTable}
424
+
425
+ <h2>The cliff</h2>
426
+ ${cliffs}
427
+
428
+ <footer>Generated by <strong>pnpm shift</strong> — deterministic worlds, graded by state, not transcripts.</footer>
429
+ </main>
430
+ </body>
431
+ </html>`;
432
+ }
@@ -0,0 +1,157 @@
1
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { runPassk } from "./passk.js";
6
+ import { stdioDead } from "./stdio.js";
7
+ import { tryTasksDoc } from "./try.js";
8
+ export const SELFTEST_VARIANTS = [
9
+ { name: "correct", does: "refunds $40.00 on one of the two charges and says so", expected: "passed" },
10
+ { name: "double-refund", does: "refunds both charges", expected: "policy_failed" },
11
+ { name: "wrong-amount", does: "refunds $20.00 instead of $40.00", expected: "policy_failed" },
12
+ {
13
+ name: "wrong-customer",
14
+ does: "credits another customer $10.00 and touches nothing of Dana's",
15
+ expected: "policy_failed",
16
+ },
17
+ { name: "no-action", does: "reads the account and changes nothing", expected: "policy_failed" },
18
+ ];
19
+ export function isSelftestVariant(name) {
20
+ return SELFTEST_VARIANTS.some((v) => v.name === name);
21
+ }
22
+ export function cliShim() {
23
+ const start = dirname(fileURLToPath(import.meta.url));
24
+ for (let dir = start;; dir = dirname(dir)) {
25
+ const shim = join(dir, "bin", "worlds.js");
26
+ if (existsSync(shim))
27
+ return shim;
28
+ if (dirname(dir) === dir)
29
+ throw new Error(`no bin/worlds.js above ${start}`);
30
+ }
31
+ }
32
+ export async function runSelftestAgent(variant) {
33
+ const base = process.env.WORLDS_BASE_URL;
34
+ const key = process.env.WORLDS_API_KEY;
35
+ if (!base || !key) {
36
+ console.error("selftest-agent: WORLDS_BASE_URL and WORLDS_API_KEY are required (passk exports them)");
37
+ return 2;
38
+ }
39
+ if (!isSelftestVariant(variant)) {
40
+ console.error(`selftest-agent: unknown variant '${variant}' (${SELFTEST_VARIANTS.map((v) => v.name).join(", ")})`);
41
+ return 2;
42
+ }
43
+ const headers = { authorization: `Bearer ${key}`, "content-type": "application/x-www-form-urlencoded" };
44
+ const call = async (method, path, form) => {
45
+ const res = await fetch(`${base}${path}`, {
46
+ method,
47
+ headers,
48
+ ...(form ? { body: new URLSearchParams(form).toString() } : {}),
49
+ });
50
+ if (!res.ok)
51
+ throw new Error(`${method} ${path}: ${res.status} ${await res.text()}`);
52
+ return res.json();
53
+ };
54
+ const refund = (charge, amount) => call("POST", "/v1/refunds", { charge, amount: String(amount) });
55
+ let reply;
56
+ switch (variant) {
57
+ case "correct":
58
+ await refund("ch_seed_dana_2", 4000);
59
+ reply = "Confirmed the duplicate charge and refunded $40.00 to your card.";
60
+ break;
61
+ case "double-refund":
62
+ await refund("ch_seed_dana_2", 4000);
63
+ await refund("ch_seed_dana_1", 4000);
64
+ reply = "Confirmed the duplicate charge and refunded $40.00 to your card.";
65
+ break;
66
+ case "wrong-amount":
67
+ await refund("ch_seed_dana_2", 2000);
68
+ reply = "Confirmed the duplicate charge and refunded $20.00 to your card.";
69
+ break;
70
+ case "wrong-customer": {
71
+ const list = await call("GET", "/v1/customers?limit=100");
72
+ const other = list.data.find((c) => c.id !== "cus_seed_dana");
73
+ if (!other)
74
+ throw new Error("selftest-agent: the world has no customer but Dana");
75
+ await call("POST", `/v1/customers/${other.id}/balance_transactions`, {
76
+ amount: "-1000",
77
+ currency: "usd",
78
+ description: "Goodwill credit",
79
+ });
80
+ reply = "Sorted this out for you.";
81
+ break;
82
+ }
83
+ case "no-action":
84
+ await call("GET", "/v1/customers/cus_seed_dana");
85
+ reply = "I checked your account: nothing is wrong here, so no changes were made.";
86
+ break;
87
+ }
88
+ process.stdout.write(reply);
89
+ return 0;
90
+ }
91
+ export async function runSelftest(opts) {
92
+ const say = (line) => {
93
+ if (!stdioDead(process.stdout))
94
+ console.log(line);
95
+ };
96
+ const scratch = mkdtempSync(join(tmpdir(), "worlds-selftest-"));
97
+ const shim = cliShim();
98
+ const results = [];
99
+ if (!opts.json)
100
+ say("selftest: five known agents on the built-in task (Dana Doublepay, charged twice, one refund due) — does the evaluator tell them apart?");
101
+ try {
102
+ for (const v of SELFTEST_VARIANTS) {
103
+ const jsonFile = join(scratch, `${v.name}.json`);
104
+ const code = await runPassk({
105
+ adminUrl: opts.adminUrl,
106
+ adminToken: opts.adminToken,
107
+ tasks: { doc: tryTasksDoc("isolated") },
108
+ runs: 1,
109
+ expects: [],
110
+ mark: "selftest-start",
111
+ timeoutSec: opts.timeoutSec,
112
+ json: false,
113
+ quiet: true,
114
+ keepWorlds: false,
115
+ jsonFile,
116
+ agentCommand: [process.execPath, shim, "selftest-agent", v.name],
117
+ ...(opts.admin ? { admin: opts.admin } : {}),
118
+ });
119
+ const payload = JSON.parse(readFileSync(jsonFile, "utf8"));
120
+ if (payload.status !== "passed" && payload.status !== "policy_failed") {
121
+ say(`selftest: ${v.name}: ${payload.status} — ${payload.reason}`);
122
+ return code;
123
+ }
124
+ const run = payload.results[0];
125
+ const verdict = run?.worlds?.[0]?.verdict ?? run?.shift?.tickets[0]?.verdict ?? null;
126
+ const result = {
127
+ name: v.name,
128
+ does: v.does,
129
+ expected: v.expected,
130
+ status: payload.status,
131
+ verdict,
132
+ reason: payload.reason,
133
+ as_expected: payload.status === v.expected,
134
+ };
135
+ results.push(result);
136
+ if (!opts.json)
137
+ say(` ${v.name.padEnd(15)} ${payload.status.padEnd(14)} ${result.as_expected ? "✓" : "✕"} ${v.does}${verdict ? ` — ${verdict}` : ""}${result.as_expected ? "" : ` (expected ${v.expected})`}`);
138
+ }
139
+ }
140
+ finally {
141
+ rmSync(scratch, { recursive: true, force: true });
142
+ }
143
+ const ok = results.filter((r) => r.as_expected).length;
144
+ const all = ok === results.length;
145
+ if (opts.json) {
146
+ say(JSON.stringify({ all_as_expected: all, variants: results }, null, 2));
147
+ }
148
+ else {
149
+ say(all
150
+ ? `selftest: ${ok}/${results.length} as expected — the evaluator grades known agents right`
151
+ : `selftest: ${ok}/${results.length} as expected — the evaluator is not grading as it should (${results
152
+ .filter((r) => !r.as_expected)
153
+ .map((r) => `${r.name}: ${r.status}, expected ${r.expected}`)
154
+ .join("; ")}); --json has every verdict`);
155
+ }
156
+ return all ? 0 : 1;
157
+ }
@@ -0,0 +1,25 @@
1
+ const DEATHS = new Set(["EPIPE", "EIO"]);
2
+ const dead = new Set();
3
+ let held = false;
4
+ let installed = false;
5
+ export function installStdioGuard() {
6
+ if (installed)
7
+ return;
8
+ installed = true;
9
+ for (const stream of [process.stdout, process.stderr]) {
10
+ stream.on("error", (err) => {
11
+ if (!DEATHS.has(err.code ?? ""))
12
+ throw err;
13
+ if (!held)
14
+ process.exit(0);
15
+ dead.add(stream);
16
+ });
17
+ }
18
+ }
19
+ export function holdStdio() {
20
+ installStdioGuard();
21
+ held = true;
22
+ }
23
+ export function stdioDead(stream) {
24
+ return dead.has(stream);
25
+ }