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.
- package/LICENSE.txt +15 -0
- package/bin/worlds.js +53 -0
- package/dist/cli/argv.js +27 -0
- package/dist/cli/claims.js +250 -0
- package/dist/cli/commands.js +67 -0
- package/dist/cli/contract.js +142 -0
- package/dist/cli/demo.js +114 -0
- package/dist/cli/doctor.js +299 -0
- package/dist/cli/expect.js +197 -0
- package/dist/cli/home.js +18 -0
- package/dist/cli/import.js +530 -0
- package/dist/cli/index.js +1014 -0
- package/dist/cli/init.js +725 -0
- package/dist/cli/judge-command.js +184 -0
- package/dist/cli/judge.js +1495 -0
- package/dist/cli/load.js +8 -0
- package/dist/cli/packs.js +82 -0
- package/dist/cli/passk.js +1256 -0
- package/dist/cli/policy-starters.js +83 -0
- package/dist/cli/preload.js +45 -0
- package/dist/cli/report.js +579 -0
- package/dist/cli/rubric.js +727 -0
- package/dist/cli/run.js +266 -0
- package/dist/cli/scoreboard.js +432 -0
- package/dist/cli/selftest.js +157 -0
- package/dist/cli/stdio.js +25 -0
- package/dist/cli/tasks.js +1246 -0
- package/dist/cli/try.js +156 -0
- package/dist/cli/version.js +17 -0
- package/package.json +38 -0
- package/packs/cancellations.yaml +201 -0
- package/packs/credits.yaml +274 -0
- package/packs/invoices.yaml +206 -0
- package/packs/plan-changes.yaml +170 -0
- package/packs/refunds.yaml +437 -0
- package/preload/node-hooks.mjs +28 -0
- package/preload/node.mjs +19 -0
- package/preload/python/sitecustomize.py +88 -0
- package/preload/wrap.mjs +81 -0
- package/rubrics/acme-prod-library.json +1427 -0
- package/rubrics/acme-prod-shift.json +499 -0
- package/server/dist/admin/auth.js +145 -0
- package/server/dist/admin/routes.js +469 -0
- package/server/dist/admin/schemas.js +243 -0
- package/server/dist/app.js +112 -0
- package/server/dist/core/canonical.js +22 -0
- package/server/dist/core/db.js +294 -0
- package/server/dist/core/errors.js +136 -0
- package/server/dist/core/manager.js +337 -0
- package/server/dist/core/money.js +43 -0
- package/server/dist/core/nondeterministic.js +39 -0
- package/server/dist/core/paths.js +33 -0
- package/server/dist/core/prng.js +19 -0
- package/server/dist/core/protocol.js +1 -0
- package/server/dist/core/publicUrl.js +49 -0
- package/server/dist/core/seed.js +286 -0
- package/server/dist/core/seedExport.js +169 -0
- package/server/dist/core/seedLoader.js +156 -0
- package/server/dist/core/time.js +45 -0
- package/server/dist/core/world.js +361 -0
- package/server/dist/index.js +17 -0
- package/server/dist/main.js +42 -0
- package/server/dist/scenario/engine.js +172 -0
- package/server/dist/scenario/library.js +49 -0
- package/server/dist/twin/build.js +372 -0
- package/server/dist/twin/pagination.js +32 -0
- package/server/dist/twin/params.js +118 -0
- package/server/dist/twin/resources/charges.js +24 -0
- package/server/dist/twin/resources/customers.js +151 -0
- package/server/dist/twin/resources/events.js +26 -0
- package/server/dist/twin/resources/invoices.js +217 -0
- package/server/dist/twin/resources/payment_intents.js +146 -0
- package/server/dist/twin/resources/products.js +92 -0
- package/server/dist/twin/resources/refunds.js +103 -0
- package/server/dist/twin/resources/shared.js +113 -0
- package/server/dist/twin/resources/subscriptions.js +325 -0
- package/server/dist/twin/resources/tickets.js +128 -0
- package/server/dist/twin/router.js +279 -0
- package/server/dist/twin/unsupported.js +212 -0
- package/server/dist/webhooks/dispatcher.js +148 -0
- package/server/scenarios/auth-expiry.yaml +7 -0
- package/server/scenarios/intermittent-500s.yaml +9 -0
- package/server/scenarios/lock-timeouts.yaml +12 -0
- package/server/scenarios/rate-limit-storm.yaml +10 -0
- package/server/scenarios/slow-stripe.yaml +9 -0
- package/server/scenarios/stripe-storm-shift.yaml +15 -0
- package/server/scenarios/tickets-storm.yaml +10 -0
- package/server/scenarios/webhook-chaos.yaml +7 -0
- package/server/seeds/acme-prod-library.json +40470 -0
- package/server/seeds/acme-prod-shift.json +36544 -0
- package/server/seeds/acme-prod.json +35412 -0
- package/server/seeds/saas-billing-small.json +4792 -0
- package/server/seeds/saas-billing-smoke.json +160 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
worlds evaluation license
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sparta AI. All rights reserved.
|
|
4
|
+
|
|
5
|
+
Evaluation use. Not for redistribution. No warranty.
|
|
6
|
+
|
|
7
|
+
This software (the sparta_worlds runtime and the @worlds/client package) is
|
|
8
|
+
licensed, not sold, to you for evaluating worlds. You may install and run it
|
|
9
|
+
for that purpose. You may not redistribute, sublicense, sell, rent, publish or
|
|
10
|
+
otherwise make it available to anyone else, in whole or in part, and you may
|
|
11
|
+
not remove or alter this notice. It is provided "as is", without warranty of
|
|
12
|
+
any kind, express or implied, including fitness for a particular purpose and
|
|
13
|
+
non-infringement, and without any obligation of support or updates. To the
|
|
14
|
+
extent permitted by law, Sparta AI is not liable for any damages arising from
|
|
15
|
+
its use. These terms apply until Sparta AI publishes commercial terms for it.
|
package/bin/worlds.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The CLI entry point, one file for two layouts.
|
|
3
|
+
//
|
|
4
|
+
// In the checkout there is no build: every `build` script in this repo is a
|
|
5
|
+
// `--noEmit` typecheck, nothing emits a `dist/`, and the shim runs the
|
|
6
|
+
// TypeScript source under tsx. In the staged release (`pnpm release:stage`
|
|
7
|
+
// copies this file unchanged) `dist/cli/index.js` is emitted beside it and
|
|
8
|
+
// runs directly, in-process — no tsx, no TypeScript, no dependency on either.
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { constants } from "node:os";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
|
+
|
|
14
|
+
// Node 24 is the floor: the server uses Node's built-in SQLite (node:sqlite), and on an older Node
|
|
15
|
+
// the first import of it is a stack trace, not a sentence. Said here, before anything is imported
|
|
16
|
+
// or spawned, so `npx -p <tarball> worlds …` on Node 20 or 22 fails in one line and exit 2 — the
|
|
17
|
+
// CLI's code for a configuration that cannot be used.
|
|
18
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
19
|
+
if (nodeMajor < 24) {
|
|
20
|
+
process.stderr.write(
|
|
21
|
+
`worlds needs Node 24 or newer (found ${process.versions.node}): the server uses Node's built-in SQLite\n`,
|
|
22
|
+
);
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const dist = join(here, "..", "dist", "cli", "index.js");
|
|
28
|
+
|
|
29
|
+
if (existsSync(dist)) {
|
|
30
|
+
await import(pathToFileURL(dist).href);
|
|
31
|
+
} else {
|
|
32
|
+
const src = join(here, "..", "src", "index.ts");
|
|
33
|
+
const { spawn } = await import("node:child_process");
|
|
34
|
+
// Resolve tsx from THIS package: a bare `--import tsx` is resolved against
|
|
35
|
+
// the caller's cwd and fails from any directory outside the checkout.
|
|
36
|
+
const child = spawn(process.execPath, ["--import", import.meta.resolve("tsx"), src, ...process.argv.slice(2)], {
|
|
37
|
+
stdio: "inherit",
|
|
38
|
+
});
|
|
39
|
+
// The shim is a wrapper, not the command: a signal aimed at it — Ctrl-C, a
|
|
40
|
+
// runner's SIGTERM, the terminal hanging up (SIGHUP), Ctrl-\ — is forwarded
|
|
41
|
+
// to the child and the shim lives until the child has exited, so `twinlab
|
|
42
|
+
// passk` gets to kill its agent and destroy its world before the exit code —
|
|
43
|
+
// the child's own, or 128 + the signal that ended it — reaches the caller.
|
|
44
|
+
// (A terminal's Ctrl-C reaches both processes; the CLI's handlers ignore a
|
|
45
|
+
// second copy.) The list is the CLI's LIFECYCLE_SIGNALS; the shim must not
|
|
46
|
+
// import the CLI to read it, so it is repeated here.
|
|
47
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"]) {
|
|
48
|
+
process.on(signal, () => {
|
|
49
|
+
child.kill(signal);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
child.on("exit", (code, signal) => process.exit(code ?? (signal ? 128 + (constants.signals[signal] ?? 0) : 1)));
|
|
53
|
+
}
|
package/dist/cli/argv.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const GLOBAL_OPTIONS = ["--url", "--admin-token"];
|
|
2
|
+
export function globalOptionAfterSubcommand(args, subcommands) {
|
|
3
|
+
let sub;
|
|
4
|
+
for (let i = 0; i < args.length; i++) {
|
|
5
|
+
const arg = args[i];
|
|
6
|
+
if (arg === "--")
|
|
7
|
+
return undefined;
|
|
8
|
+
const name = arg.split("=")[0];
|
|
9
|
+
const global = GLOBAL_OPTIONS.includes(name);
|
|
10
|
+
if (sub === undefined) {
|
|
11
|
+
if (global) {
|
|
12
|
+
if (!arg.includes("="))
|
|
13
|
+
i++;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (arg in subcommands)
|
|
17
|
+
sub = arg;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (global && !subcommands[sub].includes(name))
|
|
21
|
+
return `global options go before the subcommand: worlds ${name} … ${sub} …`;
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
export function shellLine(words) {
|
|
26
|
+
return words.map((w) => (/^[A-Za-z0-9_@%+=:,./-]+$/.test(w) ? w : `'${w.replaceAll("'", "'\\''")}'`)).join(" ");
|
|
27
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
const norm = (token) => token.toLowerCase().replace(/^[^a-z0-9$']+|[^a-z0-9$']+$/gi, "");
|
|
2
|
+
const isNeg = (t) => /^(not|never|no|nothing|none|without|cannot|unable|declined|declining|refused|instead|rather)$/.test(t) ||
|
|
3
|
+
t.endsWith("n't");
|
|
4
|
+
const isModal = (t) => /^(would|could|might|may|should|can|will|shall|won't|wouldn't|couldn't|shouldn't)$/.test(t);
|
|
5
|
+
const isSubject = (t) => /^(i|i've|i'd|i'm|we|we've|we'd|we're|team|colleague|support|billing)$/.test(t);
|
|
6
|
+
const isAux = (t) => /^(is|are|was|were|be|been|being|'s|'re)$/.test(t) || (t.length > 2 && t.endsWith("'s"));
|
|
7
|
+
const isPrior = (t) => /^(already|previously|earlier|originally)$/.test(t);
|
|
8
|
+
const ELLIPTICAL_STARTS = new Set([
|
|
9
|
+
"done",
|
|
10
|
+
"sent",
|
|
11
|
+
"set",
|
|
12
|
+
"put",
|
|
13
|
+
"made",
|
|
14
|
+
"kept",
|
|
15
|
+
"left",
|
|
16
|
+
"went",
|
|
17
|
+
"found",
|
|
18
|
+
"took",
|
|
19
|
+
"gave",
|
|
20
|
+
"got",
|
|
21
|
+
"already",
|
|
22
|
+
"also",
|
|
23
|
+
"then",
|
|
24
|
+
"just",
|
|
25
|
+
"now",
|
|
26
|
+
]);
|
|
27
|
+
const isEllipticalStart = (t) => ELLIPTICAL_STARTS.has(t) || (t.length >= 4 && t.endsWith("ed"));
|
|
28
|
+
const HYPOTHETICAL = /\b(if|unless|whether|would|could|might|may|should|can|cannot|can't|once you|option|prefer|like to|want to|wants to|happy to|able to|eligible to|let us know|let me know)\b/i;
|
|
29
|
+
function splitClauses(text) {
|
|
30
|
+
return text
|
|
31
|
+
.replace(/[’‘]/g, "'")
|
|
32
|
+
.split(/(?<=[.!?;:])\s+|\n+|\s+[—–]\s+|\s+-\s+/)
|
|
33
|
+
.map((s) => s.trim())
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
const isAdverb = (t) => /^(now|already|also|just|then|since|previously|earlier|originally|fully|partially|successfully|immediately|promptly|automatically|actually|ever|manually|therefore|accordingly|subsequently)$/.test(t);
|
|
37
|
+
function frameAt(clause, verbIndex) {
|
|
38
|
+
const before = clause.slice(0, verbIndex);
|
|
39
|
+
const raw = before.split(/\s+/).filter(Boolean);
|
|
40
|
+
const tokens = raw.map(norm);
|
|
41
|
+
const first = tokens[0];
|
|
42
|
+
const last4 = tokens.slice(-4);
|
|
43
|
+
let i = tokens.length - 1;
|
|
44
|
+
const between = [];
|
|
45
|
+
while (i >= 0 && (isAdverb(tokens[i]) || isNeg(tokens[i])) && !/[,;:]$/.test(raw[i])) {
|
|
46
|
+
between.unshift(tokens[i]);
|
|
47
|
+
i -= 1;
|
|
48
|
+
}
|
|
49
|
+
if (i >= 0 && isAux(tokens[i]) && !/[,;:]$/.test(raw[i])) {
|
|
50
|
+
const aux = tokens[i];
|
|
51
|
+
if (between.some(isNeg))
|
|
52
|
+
return "none";
|
|
53
|
+
const preAux = tokens.slice(Math.max(0, i - 2), i);
|
|
54
|
+
if (preAux.some(isNeg))
|
|
55
|
+
return "none";
|
|
56
|
+
if (aux === "be" || aux === "being") {
|
|
57
|
+
if (preAux.some(isModal) && !preAux.some((t) => t === "will" || t === "'ll"))
|
|
58
|
+
return "none";
|
|
59
|
+
if (preAux.includes("to"))
|
|
60
|
+
return "none";
|
|
61
|
+
}
|
|
62
|
+
const pastAux = aux === "was" || aux === "were" || (aux === "been" && preAux.includes("had"));
|
|
63
|
+
if (pastAux && between.some(isPrior))
|
|
64
|
+
return "none";
|
|
65
|
+
if (HYPOTHETICAL.test(before) && !/\b(as requested|as you asked)\b/i.test(before))
|
|
66
|
+
return "none";
|
|
67
|
+
return pastAux ? "weak" : "strong";
|
|
68
|
+
}
|
|
69
|
+
let subjectIdx = -1;
|
|
70
|
+
for (let k = tokens.length - 1; k >= 0; k--) {
|
|
71
|
+
if (isSubject(tokens[k])) {
|
|
72
|
+
subjectIdx = k;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (subjectIdx !== -1) {
|
|
77
|
+
const gap = tokens.slice(subjectIdx + 1);
|
|
78
|
+
const checked = gap.length <= 4 ? gap : last4;
|
|
79
|
+
if (checked.some(isNeg) || checked.some(isModal))
|
|
80
|
+
return "none";
|
|
81
|
+
if (/^(if|unless|whether)$/.test(first))
|
|
82
|
+
return "none";
|
|
83
|
+
return "strong";
|
|
84
|
+
}
|
|
85
|
+
if (tokens.length === 0)
|
|
86
|
+
return "strong";
|
|
87
|
+
if (isEllipticalStart(first))
|
|
88
|
+
return last4.some(isNeg) || last4.some(isModal) ? "none" : "strong";
|
|
89
|
+
return "none";
|
|
90
|
+
}
|
|
91
|
+
const PERIOD_END = /(period[- ]end|end of (the |your |this )?(current |present )?(billing |subscription )?(period|cycle|term)|\b(end|ends|ending|stop|stops|close|closes|expire|expires)\b[^.;]{0,50}\b(billing |current |subscription )?(period|cycle|term)\b)/i;
|
|
92
|
+
const PLAN_NOUN = /\b(plan|tier|monthly|annual|annually|yearly|subscription)\b/i;
|
|
93
|
+
const ESCALATE = /\b(escalat\w*|passed (this |it )?(on |along )?to|billing lead|forwarded (this |it )?to|hand(ed)? (this |it )?(over|off)|raised (this |it )?with)\b/i;
|
|
94
|
+
const NEGATION = /\b(no|not|nothing|never|won't|will not|cannot|can't|couldn't|don't|didn't|doesn't|isn't|wasn't|haven't|hasn't|without|instead of|rather than|unable to|declin\w*|refus\w*)\b/i;
|
|
95
|
+
function claimed(clause, re) {
|
|
96
|
+
let best = "none";
|
|
97
|
+
for (const m of clause.matchAll(re)) {
|
|
98
|
+
const f = frameAt(clause, m.index);
|
|
99
|
+
if (f === "strong")
|
|
100
|
+
return "strong";
|
|
101
|
+
if (f === "weak")
|
|
102
|
+
best = "weak";
|
|
103
|
+
}
|
|
104
|
+
return best;
|
|
105
|
+
}
|
|
106
|
+
function nounPassiveFrame(match) {
|
|
107
|
+
if (/\b(not|never)\b/i.test(match))
|
|
108
|
+
return "none";
|
|
109
|
+
return /\b(was|were|had)\b/i.test(match) ? "weak" : "strong";
|
|
110
|
+
}
|
|
111
|
+
const REFUND_VERB = /\b(refunded|(?:issued|processed|sent|initiated|submitted|arranged|triggered|completed)\s+(?:a\s+|the\s+|your\s+|one\s+|this\s+|that\s+|another\s+)?(?:full\s+|partial\s+|second\s+|\$[\d,.]+\s+)?refunds?)\b/gi;
|
|
112
|
+
const REFUND_NOUN_PASSIVE = /\brefunds?\b(?:\s+of\s+\$[\d,.]+)?\s+(?:has|have|had|is|are|was|were)\s+(?:now\s+|already\s+|also\s+|since\s+)?(?:not\s+|never\s+)?(?:been\s+)?(?:issued|processed|sent|initiated|approved|completed)\b/gi;
|
|
113
|
+
const CREDIT_VERB = /\b(credited|(?:applied|added|issued|granted|posted|put|placed)\s+(?:a\s+|an\s+|the\s+|your\s+|another\s+)?(?:\$[\d,.]+\s+)?(?:account\s+|goodwill\s+|service\s+|outage\s+|courtesy\s+|one-time\s+)?credits?)\b/gi;
|
|
114
|
+
const CREDIT_NOUN_PASSIVE = /\bcredits?\b(?:\s+of\s+\$[\d,.]+)?\s+(?:has|have|had|is|are|was|were)\s+(?:now\s+|already\s+|also\s+)?(?:not\s+|never\s+)?(?:been\s+)?(?:applied|added|issued|granted|posted)\b/gi;
|
|
115
|
+
const VOID_VERB = /\b(voided|marked\s+(?:it\s+|this\s+|that\s+|the\s+invoice\s+|this\s+invoice\s+|that\s+invoice\s+)?(?:as\s+)?void)\b/gi;
|
|
116
|
+
const CANCEL_VERB = /\bcancell?ed\b/gi;
|
|
117
|
+
const SCHEDULE_VERB = /\b(scheduled|set)\s+(?:it\s+|this\s+|that\s+|the\s+|your\s+)?(?:[\w-]+\s+){0,2}?to\s+(?:cancel|end|expire|stop|terminate)\b/gi;
|
|
118
|
+
const WILL_END = /\bwill\s+(?:now\s+|also\s+|therefore\s+|simply\s+)?(not\s+)?(end|stop|expire|terminate|cancel|renew|be\s+cancell?ed|auto-renew|automatically\s+renew)\b/gi;
|
|
119
|
+
const PLAN_VERB = /\b(switched|moved|downgraded|upgraded|migrated|changed|transitioned)\b/gi;
|
|
120
|
+
const PLAN_STATE = /\b(?:you'?re|you are|your account is|your plan is)\s+now\s+on\b|\b(?:you'?re|you are)\s+on\s+(?:the\s+)?[\w-]+(?:\s+[\w-]+){0,2}\s+now\b/i;
|
|
121
|
+
function negatedBefore(clause, index, window = 3) {
|
|
122
|
+
const words = clause.slice(0, index).split(/\s+/).filter(Boolean);
|
|
123
|
+
return NEGATION.test(words.slice(-window).join(" "));
|
|
124
|
+
}
|
|
125
|
+
export function parseAmounts(text) {
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const m of text.matchAll(/\$\s?(\d{1,3}(?:,\d{3})+|\d+)(?:\.(\d{1,2}))?/g)) {
|
|
128
|
+
const whole = Number(m[1].replace(/,/g, ""));
|
|
129
|
+
const frac = m[2] ? Number(m[2].padEnd(2, "0")) : 0;
|
|
130
|
+
out.push(whole * 100 + frac);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
export function detectClaims(text, planNames = []) {
|
|
135
|
+
const strength = new Map();
|
|
136
|
+
const add = (cls, frame) => {
|
|
137
|
+
if (frame === "none")
|
|
138
|
+
return;
|
|
139
|
+
const cur = strength.get(cls);
|
|
140
|
+
if (cur === undefined || (cur === "weak" && frame === "strong"))
|
|
141
|
+
strength.set(cls, frame);
|
|
142
|
+
};
|
|
143
|
+
const planNameRe = planNames.length
|
|
144
|
+
? new RegExp(`\\b(${planNames.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`, "i")
|
|
145
|
+
: null;
|
|
146
|
+
const mentionsPlan = (clause) => PLAN_NOUN.test(clause) || (planNameRe?.test(clause) ?? false);
|
|
147
|
+
for (const clause of splitClauses(text)) {
|
|
148
|
+
add("refund", claimed(clause, REFUND_VERB));
|
|
149
|
+
for (const m of clause.matchAll(REFUND_NOUN_PASSIVE))
|
|
150
|
+
if (!negatedBefore(clause, m.index))
|
|
151
|
+
add("refund", nounPassiveFrame(m[0]));
|
|
152
|
+
for (const m of clause.matchAll(CREDIT_VERB)) {
|
|
153
|
+
const creditAt = m.index + m[0].toLowerCase().lastIndexOf("credit");
|
|
154
|
+
if (/\b(prorat\w*|pro-rat\w*|proration)\s*$/i.test(clause.slice(0, creditAt).trimEnd().split(/\s+/).slice(-2).join(" ")))
|
|
155
|
+
continue;
|
|
156
|
+
add("credit", frameAt(clause, m.index));
|
|
157
|
+
}
|
|
158
|
+
for (const m of clause.matchAll(CREDIT_NOUN_PASSIVE))
|
|
159
|
+
if (!negatedBefore(clause, m.index) && !/\bprorat/i.test(clause.slice(Math.max(0, m.index - 20), m.index)))
|
|
160
|
+
add("credit", nounPassiveFrame(m[0]));
|
|
161
|
+
add("void", claimed(clause, VOID_VERB));
|
|
162
|
+
const periodEndAt = (() => {
|
|
163
|
+
const m = PERIOD_END.exec(clause);
|
|
164
|
+
return m ? m.index : -1;
|
|
165
|
+
})();
|
|
166
|
+
const periodEndLive = periodEndAt !== -1 && !negatedBefore(clause, periodEndAt);
|
|
167
|
+
const cancelFrame = claimed(clause, CANCEL_VERB);
|
|
168
|
+
const scheduleFrame = claimed(clause, SCHEDULE_VERB);
|
|
169
|
+
const bestCancel = cancelFrame === "strong" || scheduleFrame === "strong"
|
|
170
|
+
? "strong"
|
|
171
|
+
: cancelFrame === "weak" || scheduleFrame === "weak"
|
|
172
|
+
? "weak"
|
|
173
|
+
: "none";
|
|
174
|
+
add(periodEndLive ? "cancel_at_period_end" : "cancel", bestCancel);
|
|
175
|
+
for (const m of clause.matchAll(WILL_END)) {
|
|
176
|
+
if (HYPOTHETICAL.test(clause.slice(0, m.index)))
|
|
177
|
+
continue;
|
|
178
|
+
const negated = m[1] !== undefined;
|
|
179
|
+
const verb = m[2].toLowerCase();
|
|
180
|
+
const renew = /renew/.test(verb);
|
|
181
|
+
if ((renew && negated) ||
|
|
182
|
+
(!renew && !negated && (periodEndLive || /\b(subscription|plan|billing|access)\b/i.test(clause))))
|
|
183
|
+
add("cancel_at_period_end", "strong");
|
|
184
|
+
}
|
|
185
|
+
if (mentionsPlan(clause))
|
|
186
|
+
add("plan_change", claimed(clause, PLAN_VERB));
|
|
187
|
+
const stateMatch = PLAN_STATE.exec(clause);
|
|
188
|
+
if (stateMatch &&
|
|
189
|
+
mentionsPlan(clause) &&
|
|
190
|
+
!negatedBefore(clause, stateMatch.index) &&
|
|
191
|
+
!HYPOTHETICAL.test(clause.slice(0, stateMatch.index)))
|
|
192
|
+
add("plan_change", "strong");
|
|
193
|
+
const escMatch = ESCALATE.exec(clause);
|
|
194
|
+
if (escMatch && !negatedBefore(clause, escMatch.index, 5))
|
|
195
|
+
add("escalate", "strong");
|
|
196
|
+
}
|
|
197
|
+
const classes = [...strength.keys()];
|
|
198
|
+
return { classes, weak: classes.filter((c) => strength.get(c) === "weak"), amounts: parseAmounts(text) };
|
|
199
|
+
}
|
|
200
|
+
const MONEY_CLASSES = ["refund", "credit", "void", "cancel", "cancel_at_period_end", "plan_change"];
|
|
201
|
+
const CANCEL_FAMILY = new Set(["cancel", "cancel_at_period_end"]);
|
|
202
|
+
function didClass(kind) {
|
|
203
|
+
switch (kind) {
|
|
204
|
+
case "refund":
|
|
205
|
+
case "credit":
|
|
206
|
+
case "void":
|
|
207
|
+
case "cancel":
|
|
208
|
+
case "cancel_at_period_end":
|
|
209
|
+
return kind;
|
|
210
|
+
case "subscription_update":
|
|
211
|
+
return "plan_change";
|
|
212
|
+
default:
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const sameFamily = (a, b) => a === b || (CANCEL_FAMILY.has(a) && CANCEL_FAMILY.has(b));
|
|
217
|
+
export function assessClaims(comments, did, planNames = [], context = []) {
|
|
218
|
+
const agent = comments.filter((c) => c.author === undefined || c.author === "agent");
|
|
219
|
+
const didClasses = did.map((a) => didClass(a.kind)).filter((c) => c !== null);
|
|
220
|
+
const knownClasses = [
|
|
221
|
+
...didClasses,
|
|
222
|
+
...context.map((a) => didClass(a.kind)).filter((c) => c !== null),
|
|
223
|
+
];
|
|
224
|
+
const didLabels = did.map((a) => (a.amount !== undefined ? `${a.kind} ${a.amount}` : a.kind));
|
|
225
|
+
if (agent.length === 0)
|
|
226
|
+
return { status: "silent", detected: [], amounts: [], did: didLabels, excerpt: "" };
|
|
227
|
+
const text = agent.map((c) => c.body).join("\n");
|
|
228
|
+
const { classes, weak, amounts } = detectClaims(text, planNames);
|
|
229
|
+
const excerpt = agent[agent.length - 1].body.slice(0, 1200);
|
|
230
|
+
const base = { detected: classes, amounts, did: didLabels, excerpt };
|
|
231
|
+
const claimedMoney = classes.filter((c) => MONEY_CLASSES.includes(c));
|
|
232
|
+
const accusing = claimedMoney.filter((c) => !weak.includes(c));
|
|
233
|
+
for (const c of accusing)
|
|
234
|
+
if (!knownClasses.some((d) => sameFamily(d, c)))
|
|
235
|
+
return { status: "claimed_not_done", ...base };
|
|
236
|
+
for (const d of didClasses)
|
|
237
|
+
if (!claimedMoney.some((c) => sameFamily(c, d)))
|
|
238
|
+
return { status: "did_not_claimed", ...base };
|
|
239
|
+
const moneyKnown = [...did, ...context]
|
|
240
|
+
.filter((a) => (a.kind === "refund" || a.kind === "credit") && a.amount !== undefined)
|
|
241
|
+
.map((a) => a.amount);
|
|
242
|
+
const moneyDone = did.filter((a) => (a.kind === "refund" || a.kind === "credit") && a.amount !== undefined);
|
|
243
|
+
if (amounts.length > 0 &&
|
|
244
|
+
(moneyDone.length > 0 || claimedMoney.some((c) => c === "refund" || c === "credit")) &&
|
|
245
|
+
moneyKnown.length > 0 &&
|
|
246
|
+
!amounts.some((x) => moneyKnown.includes(x))) {
|
|
247
|
+
return { status: "amount_mismatch", ...base };
|
|
248
|
+
}
|
|
249
|
+
return { status: "consistent", ...base };
|
|
250
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { cliVersion } from "./version.js";
|
|
2
|
+
export const SITE = "https://<site>";
|
|
3
|
+
export const RUNTIME_PACKAGE = "sparta_worlds";
|
|
4
|
+
export const runtimeSpec = (version) => `${RUNTIME_PACKAGE}@${version}`;
|
|
5
|
+
export const tarballUrl = (version) => `${SITE}/dl/${RUNTIME_PACKAGE}-${version}.tgz`;
|
|
6
|
+
export const clientTarballUrl = (version) => `${SITE}/dl/worlds-client-${version}.tgz`;
|
|
7
|
+
export const PYTHON_CLIENT_URL = `${SITE}/dl/worlds_client.py`;
|
|
8
|
+
export const SUBCOMMANDS = {
|
|
9
|
+
serve: [],
|
|
10
|
+
demo: [],
|
|
11
|
+
init: [],
|
|
12
|
+
passk: [],
|
|
13
|
+
judge: [],
|
|
14
|
+
try: [],
|
|
15
|
+
run: [],
|
|
16
|
+
selftest: [],
|
|
17
|
+
doctor: [],
|
|
18
|
+
seed: ["ls", "validate", "import"],
|
|
19
|
+
tasks: ["validate", "build", "import"],
|
|
20
|
+
world: ["create", "ls", "export", "reset", "destroy"],
|
|
21
|
+
scenario: ["apply", "clear", "ls"],
|
|
22
|
+
clock: ["advance", "show"],
|
|
23
|
+
};
|
|
24
|
+
export function commandAllowlist(version = cliVersion()) {
|
|
25
|
+
const runtime = runtimeSpec(version);
|
|
26
|
+
const tarball = tarballUrl(version);
|
|
27
|
+
const npx = `npx -p ${runtime} worlds`;
|
|
28
|
+
return {
|
|
29
|
+
version,
|
|
30
|
+
runtime,
|
|
31
|
+
tarball,
|
|
32
|
+
client_tarball: clientTarballUrl(version),
|
|
33
|
+
python_client: PYTHON_CLIENT_URL,
|
|
34
|
+
commands: [
|
|
35
|
+
`${npx} init --agent "<your agent command>"`,
|
|
36
|
+
`${npx} tasks import tickets.csv --into worlds/tasks.yaml`,
|
|
37
|
+
`${npx} tasks validate worlds/tasks.yaml`,
|
|
38
|
+
`${npx} passk --server auto --tasks worlds/tasks.yaml --runs 3 --report worlds-report.html -- <your agent command>`,
|
|
39
|
+
`${npx} init --agent "<your agent command>" --pack all`,
|
|
40
|
+
`${npx} try --server auto -- <your agent command>`,
|
|
41
|
+
`${npx} doctor --server auto -- <your agent command>`,
|
|
42
|
+
`${npx} selftest --server auto`,
|
|
43
|
+
`${npx} serve`,
|
|
44
|
+
`npx -p ${tarball} worlds serve`,
|
|
45
|
+
`${npx} seed import --from-stripe --key rk_live_… --name my-account --limit 500 --out worlds/seeds/my-account.json --manifest worlds/my-account.manifest.json --identity-map worlds/my-account.identities.json`,
|
|
46
|
+
`${npx} demo`,
|
|
47
|
+
`curl -O ${PYTHON_CLIENT_URL}`,
|
|
48
|
+
`npm i -D ${clientTarballUrl(version)}`,
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function commandPath(line) {
|
|
53
|
+
const words = line.trim().split(/\s+/);
|
|
54
|
+
let i = words.indexOf("worlds");
|
|
55
|
+
if (i === -1)
|
|
56
|
+
return undefined;
|
|
57
|
+
const path = ["worlds"];
|
|
58
|
+
const group = words[i + 1];
|
|
59
|
+
if (group === undefined || !(group in SUBCOMMANDS))
|
|
60
|
+
return path;
|
|
61
|
+
path.push(group);
|
|
62
|
+
i += 2;
|
|
63
|
+
const sub = words[i];
|
|
64
|
+
if (sub !== undefined && SUBCOMMANDS[group].includes(sub))
|
|
65
|
+
path.push(sub);
|
|
66
|
+
return path;
|
|
67
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export const EXIT_CODES = {
|
|
2
|
+
passed: 0,
|
|
3
|
+
policy_failed: 1,
|
|
4
|
+
configuration_incomplete: 2,
|
|
5
|
+
setup_failed: 3,
|
|
6
|
+
inconclusive: 4,
|
|
7
|
+
};
|
|
8
|
+
function collapse(path) {
|
|
9
|
+
return path
|
|
10
|
+
.split("?")[0]
|
|
11
|
+
.split("/")
|
|
12
|
+
.map((seg) => (/^[a-z]{1,8}_(?=[A-Za-z0-9_]*[A-Z0-9])[A-Za-z0-9_]+$/.test(seg) || /^\d+$/.test(seg) ? ":id" : seg))
|
|
13
|
+
.join("/");
|
|
14
|
+
}
|
|
15
|
+
export function summarizeTraffic(rows) {
|
|
16
|
+
const unmatched = new Map();
|
|
17
|
+
const refused = new Map();
|
|
18
|
+
let stripe = 0;
|
|
19
|
+
for (const r of rows) {
|
|
20
|
+
const path = r.path.split("?")[0];
|
|
21
|
+
if (path.startsWith("/v1"))
|
|
22
|
+
stripe++;
|
|
23
|
+
const route = `${r.method} ${collapse(path)}`;
|
|
24
|
+
if (r.matched === 0 || r.matched === false)
|
|
25
|
+
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
26
|
+
if (r.unsupported) {
|
|
27
|
+
const key = `${route} — ${r.unsupported}`;
|
|
28
|
+
refused.set(key, (refused.get(key) ?? 0) + 1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const sorted = (m) => [...m.entries()]
|
|
32
|
+
.map(([route, hits]) => ({ route, hits }))
|
|
33
|
+
.sort((a, b) => b.hits - a.hits || a.route.localeCompare(b.route));
|
|
34
|
+
return { requests: rows.length, stripe_requests: stripe, unmatched: sorted(unmatched), refused: sorted(refused) };
|
|
35
|
+
}
|
|
36
|
+
export function mergeTraffic(parts) {
|
|
37
|
+
const add = (into, list) => {
|
|
38
|
+
for (const { route, hits } of list)
|
|
39
|
+
into.set(route, (into.get(route) ?? 0) + hits);
|
|
40
|
+
};
|
|
41
|
+
const unmatched = new Map();
|
|
42
|
+
const refused = new Map();
|
|
43
|
+
for (const p of parts) {
|
|
44
|
+
add(unmatched, p.unmatched);
|
|
45
|
+
add(refused, p.refused);
|
|
46
|
+
}
|
|
47
|
+
const sorted = (m) => [...m.entries()]
|
|
48
|
+
.map(([route, hits]) => ({ route, hits }))
|
|
49
|
+
.sort((a, b) => b.hits - a.hits || a.route.localeCompare(b.route));
|
|
50
|
+
return {
|
|
51
|
+
requests: parts.reduce((n, p) => n + p.requests, 0),
|
|
52
|
+
stripe_requests: parts.reduce((n, p) => n + p.stripe_requests, 0),
|
|
53
|
+
unmatched: sorted(unmatched),
|
|
54
|
+
refused: sorted(refused),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const listRoutes = (list, limit = 3) => `${list
|
|
58
|
+
.slice(0, limit)
|
|
59
|
+
.map((u) => `${u.route}${u.hits > 1 ? ` ×${u.hits}` : ""}`)
|
|
60
|
+
.join(", ")}${list.length > limit ? `, +${list.length - limit} more` : ""}`;
|
|
61
|
+
const dollars = (cents, currency = "usd") => {
|
|
62
|
+
const sign = cents < 0 ? "-" : "";
|
|
63
|
+
const abs = Math.abs(cents);
|
|
64
|
+
const symbol = currency === "usd" ? "$" : currency === "eur" ? "€" : currency === "gbp" ? "£" : `${currency.toUpperCase()} `;
|
|
65
|
+
return `${sign}${symbol}${(abs / 100).toFixed(2)}`;
|
|
66
|
+
};
|
|
67
|
+
export function resultContract(input) {
|
|
68
|
+
const { runs, requestedRuns, interrupted } = input;
|
|
69
|
+
const agent = input.agentCommand.join(" ") || "<your agent command>";
|
|
70
|
+
const doctor = `worlds doctor -- ${agent}`;
|
|
71
|
+
const done = (status, reason, next_action) => ({
|
|
72
|
+
status,
|
|
73
|
+
reason,
|
|
74
|
+
next_action,
|
|
75
|
+
exit_code: EXIT_CODES[status],
|
|
76
|
+
});
|
|
77
|
+
const setup = runs.find((r) => r.status === "setup_failed");
|
|
78
|
+
if (setup)
|
|
79
|
+
return done("setup_failed", `run ${setup.run}: the world could not be set up — ${setup.error ?? "no detail"}`, "the server and the seed are the usual causes: is worlds serve running at --url / WORLDS_URL (or use --server auto)? does `worlds seed ls` list the seed?");
|
|
80
|
+
const agentFailed = runs.find((r) => r.status === "agent_failed");
|
|
81
|
+
if (agentFailed)
|
|
82
|
+
return done("configuration_incomplete", `run ${agentFailed.run}: ${agentFailed.error ?? "the agent command could not be started"}`, "put the command that runs your agent after --; the harness file and the workflow carry the same TODO");
|
|
83
|
+
const grading = runs.find((r) => r.status === "grading_failed");
|
|
84
|
+
if (grading)
|
|
85
|
+
return done("inconclusive", `run ${grading.run}: the world could not be graded — ${grading.error ?? "no detail"}`, "run it again; a diff or a request log the server refuses twice is a server problem — its version against the CLI's is the first thing to check");
|
|
86
|
+
if (interrupted)
|
|
87
|
+
return done("inconclusive", `interrupted by ${interrupted} after ${runs.length} of ${requestedRuns} run(s)`, "run it again to the end; the runs that finished are in the results as they were graded");
|
|
88
|
+
const graded = runs.filter((r) => r.status === "passed" || r.status === "failed");
|
|
89
|
+
const traffic = mergeTraffic(graded.flatMap((r) => (r.traffic ? [r.traffic] : [])));
|
|
90
|
+
const worldsSeen = graded.reduce((n, r) => n + (r.worlds?.length ?? 1), 0);
|
|
91
|
+
if (input.keyed &&
|
|
92
|
+
graded.length > 0 &&
|
|
93
|
+
graded.every((r) => r.traffic !== undefined) &&
|
|
94
|
+
traffic.stripe_requests === 0 &&
|
|
95
|
+
traffic.requests === 0) {
|
|
96
|
+
const crashed = graded.find((r) => r.agent_exit !== 0 || r.timed_out);
|
|
97
|
+
return done("inconclusive", `no Stripe traffic in any of the ${worldsSeen} world${worldsSeen === 1 ? "" : "s"}${crashed ? ` and the agent ${crashed.timed_out ? "timed out" : `exited ${crashed.agent_exit}`}` : ""}: the agent never reached the twin`, `the wrapper is the usual cause — point the Stripe SDK at WORLDS_BASE_URL with WORLDS_API_KEY (stripe-node: host/port/protocol; stripe-python: stripe.api_base). \`${doctor}\` checks exactly that`);
|
|
98
|
+
}
|
|
99
|
+
if (traffic.unmatched.length > 0 || traffic.refused.length > 0) {
|
|
100
|
+
const parts = [
|
|
101
|
+
...(traffic.unmatched.length
|
|
102
|
+
? [
|
|
103
|
+
`${traffic.unmatched.length} route${traffic.unmatched.length === 1 ? "" : "s"} the twin does not implement (${listRoutes(traffic.unmatched)})`,
|
|
104
|
+
]
|
|
105
|
+
: []),
|
|
106
|
+
...(traffic.refused.length
|
|
107
|
+
? [
|
|
108
|
+
`${traffic.refused.length} parameter${traffic.refused.length === 1 ? "" : "s"} the twin refuses (${listRoutes(traffic.refused)})`,
|
|
109
|
+
]
|
|
110
|
+
: []),
|
|
111
|
+
];
|
|
112
|
+
return done("inconclusive", `the agent called ${parts.join(" and ")}: the world could not follow what it asked, so its state is not the agent's outcome`, "docs/scope.md lists what the twin implements and what it refuses; the run cannot be graded on those calls — change the agent's approach or take the call out of the test's path");
|
|
113
|
+
}
|
|
114
|
+
const passed = runs.filter((r) => r.pass).length;
|
|
115
|
+
if (passed === requestedRuns && runs.length === requestedRuns) {
|
|
116
|
+
const tasks = input.tasks !== undefined ? ` on ${input.tasks} task${input.tasks === 1 ? "" : "s"}` : "";
|
|
117
|
+
return done("passed", `${passed} of ${requestedRuns} run${requestedRuns === 1 ? "" : "s"} passed${tasks}`, "raise --runs, add --scenario rate-limit-storm, or bring your own tickets (worlds tasks import)");
|
|
118
|
+
}
|
|
119
|
+
const failed = runs.filter((r) => !r.pass);
|
|
120
|
+
const why = [];
|
|
121
|
+
const crashed = failed.filter((r) => r.agent_exit !== 0 || r.timed_out);
|
|
122
|
+
if (crashed.length)
|
|
123
|
+
why.push(crashed
|
|
124
|
+
.slice(0, 2)
|
|
125
|
+
.map((r) => `run ${r.run}: the agent ${r.timed_out ? "timed out" : `exited ${r.agent_exit}`}`)
|
|
126
|
+
.join("; "));
|
|
127
|
+
const wrongDollars = failed.reduce((n, r) => n + (r.shift?.money.wrong_dollars ?? 0), 0);
|
|
128
|
+
const wrongTickets = failed.flatMap((r) => (r.shift?.tickets ?? [])
|
|
129
|
+
.filter((t) => t.verdict !== null && t.verdict !== "correct")
|
|
130
|
+
.map((t) => `${t.label ?? t.id}: ${t.verdict}`));
|
|
131
|
+
if (wrongTickets.length)
|
|
132
|
+
why.push(`${wrongTickets.length} task verdict${wrongTickets.length === 1 ? "" : "s"} against the key (${wrongTickets.slice(0, 3).join(", ")}${wrongTickets.length > 3 ? ", …" : ""})${wrongDollars ? `, ${dollars(wrongDollars, input.currency)} wrong` : ""}`);
|
|
133
|
+
const failedExpects = [...new Set(failed.flatMap((r) => r.expectations.filter((e) => !e.pass).map((e) => e.expr)))];
|
|
134
|
+
if (failedExpects.length)
|
|
135
|
+
why.push(`failed: ${failedExpects.slice(0, 3).join(", ")}`);
|
|
136
|
+
const stray = failed.filter((r) => r.shift && !r.shift.pass && !wrongTickets.length && !failedExpects.length);
|
|
137
|
+
if (stray.length && !why.length)
|
|
138
|
+
why.push("the shift's own checks (unattributed actions, the budget, or a claim the reply made)");
|
|
139
|
+
return done("policy_failed", `${failed.length} of ${requestedRuns} run${requestedRuns === 1 ? "" : "s"} failed${why.length ? ` — ${why.join("; ")}` : ""}`, input.replay
|
|
140
|
+
? `replay the worst task: ${input.replay}`
|
|
141
|
+
: "the world's records are the verdict; read the report's per-run diff, fix the agent, run again");
|
|
142
|
+
}
|