@tangle-network/agent-eval 0.186.1 → 0.187.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/CHANGELOG.md +21 -0
- package/dist/analyst/index.d.ts +1 -1
- package/dist/analyst/index.js +1 -1
- package/dist/{benchmark-command-lwSFWbc6.js → benchmark-command-j1YV50m1.js} +2 -2
- package/dist/{benchmark-command-lwSFWbc6.js.map → benchmark-command-j1YV50m1.js.map} +1 -1
- package/dist/cli.js +1 -1
- package/dist/diagnosis/index.d.ts.map +1 -1
- package/dist/diagnosis/index.js +8 -8
- package/dist/diagnosis/index.js.map +1 -1
- package/dist/{exec-D9WpA2p-.js → exec-ChJ6d5if.js} +10 -6
- package/dist/exec-ChJ6d5if.js.map +1 -0
- package/dist/experiment/index.d.ts +155 -1
- package/dist/experiment/index.d.ts.map +1 -1
- package/dist/experiment/index.js +268 -2
- package/dist/experiment/index.js.map +1 -1
- package/dist/index.d.ts +45 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +53 -1
- package/dist/index.js.map +1 -1
- package/dist/openapi.json +1 -1
- package/dist/{steps-CiNVJry_.d.ts → steps-Bg5-KVx1.d.ts} +4 -4
- package/dist/{steps-CiNVJry_.d.ts.map → steps-Bg5-KVx1.d.ts.map} +1 -1
- package/dist/trace-repair/index.d.ts +1 -1
- package/dist/trace-repair/index.js +1 -1
- package/dist/trajectory-replay/index.d.ts +11 -11
- package/dist/trajectory-replay/index.d.ts.map +1 -1
- package/dist/trajectory-replay/index.js +35 -16
- package/dist/trajectory-replay/index.js.map +1 -1
- package/docs/experiment.md +94 -0
- package/docs/trajectory-replay.md +9 -4
- package/package.json +1 -1
- package/dist/exec-D9WpA2p-.js.map +0 -1
|
@@ -13,15 +13,19 @@ function parseObservationOutput(observation) {
|
|
|
13
13
|
return m ? m[1] : observation;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* Stable failure-signature candidate: the first line
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Stable failure-signature candidate: the first error line, or a shell
|
|
17
|
+
* command-not-found suffix. Null when neither exists — a verdict then falls
|
|
18
|
+
* back to returncode-only matching and says so.
|
|
19
19
|
* Pass an explicit signature to override (compiler quote glyphs vary with
|
|
20
20
|
* locale, so a hand-picked ASCII substring is often more robust).
|
|
21
21
|
*/
|
|
22
22
|
function deriveFailureSignature(observation) {
|
|
23
|
-
const line
|
|
24
|
-
|
|
23
|
+
for (const line of parseObservationOutput(observation).split("\n")) {
|
|
24
|
+
if (/\berror\b/i.test(line)) return line.trim().slice(0, 200);
|
|
25
|
+
const missingCommand = /([A-Za-z0-9_.+/-]+: (?:command )?not found)\b/.exec(line);
|
|
26
|
+
if (missingCommand) return missingCommand[1].slice(0, 200);
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
25
29
|
}
|
|
26
30
|
/** mini-SWE's end-of-run submit convention: the agent echoes this sentinel
|
|
27
31
|
* and dumps the diff. A label on this step marks a bad SUBMIT DECISION, not a
|
|
@@ -201,4 +205,4 @@ function wrapActionForExec(action, cwd) {
|
|
|
201
205
|
//#endregion
|
|
202
206
|
export { assertReplayableTrajectory as a, deriveFailureSignature as c, isRecordedTimeout as d, isSubmitAction as f, unreadableExitCount as g, parseRecordedReturncode as h, TIMEOUT_OBSERVATION_MARKER as i, finalRecordedOutcome as l, parseObservationOutput as m, FORMAT_ERROR_OBSERVATION_PREFIX as n, classifyObservation as o, isSubmitOnlyAction as p, SUBMIT_ACTION_SIGNATURE as r, decodeRecordedTurns as s, wrapActionForExec as t, isElidedField as u };
|
|
203
207
|
|
|
204
|
-
//# sourceMappingURL=exec-
|
|
208
|
+
//# sourceMappingURL=exec-ChJ6d5if.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exec-ChJ6d5if.js","names":[],"sources":["../src/trajectory-replay/steps.ts","../src/trajectory-replay/exec.ts"],"sourcesContent":["/**\n * Recorded shell-trajectory steps and the observation grammar they carry.\n *\n * A recorded trajectory is the action/observation sequence an agent actually\n * ran. Scaffolds that execute one shell command per step (mini-SWE and the\n * CodeTracer-normalized corpora built from it) tag each observation with the\n * command's returncode and its combined output:\n *\n * <returncode>2</returncode>\n * <output>\n * …command output…\n * </output>\n *\n * That is one of four shapes a recorded turn carries, and the other three are\n * not command results at all:\n *\n * - a timeout notice, when the environment killed the command at its bound;\n * - a format-error notice, when the scaffold rejected the turn and ran\n * nothing;\n * - an elision marker `$<hex>`, when the published dump dropped the string.\n *\n * A turn also carries no observation when it is the run's last turn, because\n * the scaffold records an observation only when it hands one back to the model.\n *\n * The parsers here are the only place that grammar is decoded. Everything\n * downstream — replay verdicts, corpus enumeration, admission funnels, fix\n * prompts — reads the returncode, the output, and the failure signature through\n * these functions. A second decoder elsewhere is how a corpus reads as\n * unreplayable when it is not.\n */\n\n/**\n * One step of a recorded shell trajectory. Structural: any richer step record\n * (file refs, thinking text, tool type) satisfies it.\n */\nexport interface RecordedTrajectoryStep {\n /** 1-based position in the trajectory. */\n readonly step_id: number\n readonly action: string\n /** Null when the step recorded no observation (terminal submit steps). */\n readonly observation: string | null\n}\n\n/** Recorded returncode of a step, or null when the observation carries none. */\nexport function parseRecordedReturncode(observation: string | null): number | null {\n if (!observation) return null\n const m = /<returncode>(-?\\d+)<\\/returncode>/.exec(observation)\n return m ? Number(m[1]) : null\n}\n\n/** Text between the observation's <output> tags, or the raw observation when\n * the tags are absent. */\nexport function parseObservationOutput(observation: string | null): string {\n if (!observation) return ''\n const m = /<output>\\n?([\\s\\S]*?)\\n?<\\/output>/.exec(observation)\n return m ? m[1]! : observation\n}\n\n/**\n * Stable failure-signature candidate: the first error line, or a shell\n * command-not-found suffix. Null when neither exists — a verdict then falls\n * back to returncode-only matching and says so.\n * Pass an explicit signature to override (compiler quote glyphs vary with\n * locale, so a hand-picked ASCII substring is often more robust).\n */\nexport function deriveFailureSignature(observation: string | null): string | null {\n for (const line of parseObservationOutput(observation).split('\\n')) {\n if (/\\berror\\b/i.test(line)) return line.trim().slice(0, 200)\n // The shell prefix differs between recorded bash and replayed /bin/sh.\n const missingCommand = /([A-Za-z0-9_.+/-]+: (?:command )?not found)\\b/.exec(line)\n if (missingCommand) return missingCommand[1]!.slice(0, 200)\n }\n return null\n}\n\n/** mini-SWE's end-of-run submit convention: the agent echoes this sentinel\n * and dumps the diff. A label on this step marks a bad SUBMIT DECISION, not a\n * failed command — there is no executable failure to reproduce, so it is\n * never a counterfactual replay target. */\nexport const SUBMIT_ACTION_SIGNATURE = 'COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT'\n\nexport function isSubmitAction(action: string): boolean {\n return action.includes(SUBMIT_ACTION_SIGNATURE)\n}\n\n/**\n * True when the action is the sentinel echo and nothing else.\n *\n * The distinction decides whether a step may be dropped. An agent is told to\n * issue the sentinel alone, and 5.7% of recorded runs end on a command that\n * writes files or edits them and then echoes it. Dropping such a step because\n * it holds the sentinel would remove the run's last state change from the\n * replay, so the recorded end state and the replayed one would differ.\n */\nexport function isSubmitOnlyAction(action: string): boolean {\n return /^echo\\s+(?:\"COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT\"|'COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT'|COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT)$/.test(\n action.trim(),\n )\n}\n\n// ── Fields the published dump dropped ────────────────────────────────\n\n/**\n * A field the dump replaced with a counter instead of its text.\n *\n * The counter is hexadecimal and rises by one per dropped string in document\n * order, so a decimal-only reading (`$12`) accepts every marker that carries a\n * letter (`$3a`) as if it were real text. A command read that way replays as\n * the literal two-to-four characters `$3a`, which is not the command the run\n * executed.\n *\n * Nothing in the dump maps a marker back to its text: the same marker carries\n * different content in different rows, so there is no dictionary to read. A\n * field that matches is unrecoverable, so a caller rejects the row that holds\n * it rather than replaying the marker.\n */\nconst RECORDED_ELISION_PATTERN = /^\\$[0-9a-f]+$/\n\nexport function isElidedField(value: string | null | undefined): boolean {\n return typeof value === 'string' && RECORDED_ELISION_PATTERN.test(value)\n}\n\n// ── Observation grammar ──────────────────────────────────────────────\n\n/** Substring the timeout notice always carries, whatever the command was. */\nexport const TIMEOUT_OBSERVATION_MARKER = 'timed out and has been killed'\n\n/** Opening of the notice the scaffold writes when a turn held no single action. */\nexport const FORMAT_ERROR_OBSERVATION_PREFIX =\n 'Please always provide EXACTLY ONE action in triple backticks, found '\n\n/**\n * What a recorded observation is.\n *\n * `command-result` is the only kind that carries an exit status. `timeout` and\n * `format-error` are the scaffold speaking rather than a command: the first\n * says the environment killed the command, the second says no command ran at\n * all. `elided` and `absent` carry no information about the step, and\n * `unreadable` is a shape this grammar does not know — never assumed to be\n * anything.\n */\nexport type RecordedObservationKind =\n | 'command-result'\n | 'timeout'\n | 'format-error'\n | 'elided'\n | 'absent'\n | 'unreadable'\n\nexport function classifyObservation(observation: string | null): RecordedObservationKind {\n if (observation === null) return 'absent'\n if (isElidedField(observation)) return 'elided'\n if (parseRecordedReturncode(observation) !== null) return 'command-result'\n if (observation.startsWith(FORMAT_ERROR_OBSERVATION_PREFIX)) return 'format-error'\n if (observation.includes(TIMEOUT_OBSERVATION_MARKER)) return 'timeout'\n return 'unreadable'\n}\n\n/** True when the recording shows the environment killed this step at its\n * wall-clock bound. Such a step carries no returncode, so no replay can\n * confirm or contradict it. */\nexport function isRecordedTimeout(observation: string | null): boolean {\n return classifyObservation(observation) === 'timeout'\n}\n\n// ── Turns to steps ───────────────────────────────────────────────────\n\n/**\n * One turn as the published trajectory dump holds it.\n *\n * A turn is not a step: the system prompt, the task statement and every turn\n * the scaffold rejected are turns that executed nothing.\n */\nexport interface RecordedTrajectoryTurn {\n readonly src?: string | null\n readonly msg?: string | null\n readonly tools?: readonly { readonly cmd?: string | null }[] | null\n readonly obs?: string | null\n}\n\nexport interface DecodedTrajectory {\n /** Executed commands in recorded order, each holding its OWN observation. */\n readonly steps: readonly RecordedTrajectoryStep[]\n /** Turns the scaffold rejected before anything ran. */\n readonly formatErrorTurns: number\n /**\n * Executed commands whose text the dump dropped.\n *\n * The step stays in `steps` carrying the marker, because the run did execute\n * a command there and a shorter list would misreport the trajectory. The\n * marker is not a command, so any count above zero means this trajectory\n * cannot be replayed: gate on it, or call `assertReplayableTrajectory`.\n */\n readonly elidedCommands: number\n /** True when the run's last turn was the submit sentinel with no observation. */\n readonly endedOnSubmitSentinel: boolean\n /** Turns carrying an observation this grammar cannot read. */\n readonly unreadableTurns: number\n}\n\n/**\n * Pair every recorded command with the observation of its own turn.\n *\n * Collecting commands and observations into two lists and zipping them is the\n * decode that looks right and is not: a turn the scaffold rejected carries an\n * observation of its own, so from the first such turn onward every observation\n * belongs to a different command than the one it is read against.\n *\n * A rejected turn executed nothing, so it is never a step — including when the\n * dump recorded a command for it. The scaffold rejects a turn holding several\n * bash blocks and runs none of them, while the dump keeps one of the blocks in\n * the command field. Replaying that field would execute a command the recorded\n * run did not, which is a worse corpus than a smaller one.\n *\n * A rejected turn is recognised by its observation, so a rejected turn whose\n * observation the dump elided is indistinguishable from an executed command\n * whose observation it elided. Both read as a step. Nothing in the dump\n * separates them, and the row-level defence is the share of unreadable exits a\n * caller admits: a row with no elided observation cannot hold this case at all.\n *\n * A trailing step that echoes the sentinel and nothing else, with no\n * observation, is dropped from `steps` and reported as `endedOnSubmitSentinel`.\n * The scaffold records an observation only when it hands one to the model, and\n * the sentinel ends the run, so the missing observation is the end of the\n * transcript rather than a gap in it. Echoing the sentinel changes no state, so\n * the recorded end state is the state the step before it left.\n *\n * A step that DID get an observation stays a step: the run continued past it.\n * So does a step that echoes the sentinel after doing real work — its state\n * change is part of the recorded end state, and with no observation its exit is\n * unknown, which `finalRecordedOutcome` reports rather than hides.\n */\nexport function decodeRecordedTurns(turns: readonly RecordedTrajectoryTurn[]): DecodedTrajectory {\n const steps: RecordedTrajectoryStep[] = []\n let formatErrorTurns = 0\n let unreadableTurns = 0\n for (const turn of turns) {\n const command = turn.tools?.[0]?.cmd ?? null\n const observation = turn.obs ?? null\n const kind = classifyObservation(observation)\n if (kind === 'format-error') {\n formatErrorTurns += 1\n continue\n }\n if (command === null) {\n if (kind === 'unreadable' || kind === 'elided') unreadableTurns += 1\n continue\n }\n steps.push({ step_id: steps.length + 1, action: command, observation })\n }\n const last = steps[steps.length - 1]\n const endedOnSubmitSentinel =\n last !== undefined && last.observation === null && isSubmitOnlyAction(last.action)\n if (endedOnSubmitSentinel) steps.pop()\n return {\n steps,\n formatErrorTurns,\n elidedCommands: steps.filter((step) => isElidedField(step.action)).length,\n endedOnSubmitSentinel,\n unreadableTurns,\n }\n}\n\n// ── The state a trajectory ended in ──────────────────────────────────\n\n/**\n * How the recorded run's last executed command ended.\n *\n * `killed` is a measured outcome, not a missing one: the environment stopped\n * the command at its bound and wrote a notice instead of an exit status.\n * `unreadable` names the observation kind that blocked the read, so a funnel\n * can report which shape cost it the row.\n */\nexport type RecordedFinalOutcome =\n | { readonly kind: 'returncode'; readonly value: number }\n | { readonly kind: 'killed' }\n | { readonly kind: 'unreadable'; readonly reason: RecordedObservationKind }\n\n/**\n * The outcome of the last step, or `null` when the trajectory has no steps.\n *\n * Reads only the last step. `decodeRecordedTurns` has already removed the turns\n * that executed nothing, so the last step is the last command the run ran.\n */\nexport function finalRecordedOutcome(\n steps: readonly RecordedTrajectoryStep[],\n): RecordedFinalOutcome | null {\n const last = steps[steps.length - 1]\n if (last === undefined) return null\n const kind = classifyObservation(last.observation)\n if (kind === 'command-result') {\n return { kind: 'returncode', value: parseRecordedReturncode(last.observation)! }\n }\n if (kind === 'timeout') return { kind: 'killed' }\n return { kind: 'unreadable', reason: kind }\n}\n\n/**\n * Steps whose recorded exit a replay cannot check.\n *\n * A killed step counts: the recording holds no exit status to compare a replay\n * against, so agreement on it cannot be measured either way.\n */\nexport function unreadableExitCount(steps: readonly RecordedTrajectoryStep[]): number {\n return steps.filter((step) => classifyObservation(step.observation) !== 'command-result').length\n}\n\n/**\n * Throw unless every recorded command survived the dump.\n *\n * A replay executes `steps` verbatim, so one elision marker in an action means\n * the replay runs the literal two-to-four characters of the marker instead of\n * the command the run executed. The guard is here so a replayer needs one call\n * rather than a field check it can forget.\n */\nexport function assertReplayableTrajectory(decoded: DecodedTrajectory): void {\n if (decoded.elidedCommands === 0) return\n const markers = decoded.steps\n .filter((step) => isElidedField(step.action))\n .map((step) => step.action)\n throw new Error(\n `trajectory holds ${decoded.elidedCommands} command(s) the dump elided (${markers.slice(0, 5).join(', ')}); ` +\n 'no dictionary maps a marker back to its text, so this trajectory cannot be replayed',\n )\n}\n","/**\n * The execution boundary replay runs across.\n *\n * A replay needs one thing from its environment: a session that runs a shell\n * command inside the trajectory's own image and reports the exit code and\n * output. That is the whole contract. Concrete backends — a sandbox platform\n * client, a docker exec, an SSH shell — live with the consumer that owns the\n * infrastructure, so this package depends on no sandbox client.\n */\n\nexport interface ReplayExecResult {\n exitCode: number\n stdout: string\n stderr: string\n}\n\nexport interface ReplayExecSession {\n exec(command: string, timeoutMs: number): Promise<ReplayExecResult>\n close(): Promise<void>\n}\n\nexport interface ReplayExecBackend {\n /** One fresh execution environment per call; the caller closes it. */\n open(): Promise<ReplayExecSession>\n}\n\n/** Builds a backend pinned to one image. Callers that resolve images\n * internally (batch, corpus wire, finding verification) take this instead of\n * a backend, so every case runs against its own image. */\nexport type ReplayExecBackendFactory = (image: string) => ReplayExecBackend\n\n/**\n * mini-SWE runs every action as a fresh /bin/sh subshell from a fixed\n * workdir. Reproduce that exactly — and stay quote-proof for arbitrary\n * recorded actions — by piping the base64 of the action into `sh` after\n * cd-ing to the workdir. Exit code is sh's, i.e. the action's.\n */\nexport function wrapActionForExec(action: string, cwd: string): string {\n const b64 = Buffer.from(action, 'utf8').toString('base64')\n const quotedCwd = `'${cwd.replaceAll(\"'\", `'\\\\''`)}'`\n return `cd ${quotedCwd} && printf %s ${b64} | base64 -d | sh`\n}\n"],"mappings":"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"}
|
|
@@ -455,5 +455,159 @@ declare function clusteredPower(options: ClusteredPowerOptions): ClusteredPowerR
|
|
|
455
455
|
/** Throw the refusal for callers that want configuration-time failure. */
|
|
456
456
|
declare function assertDesignAdequate(result: ClusteredPowerResult): void;
|
|
457
457
|
//#endregion
|
|
458
|
-
|
|
458
|
+
//#region src/experiment/randomized-canary.d.ts
|
|
459
|
+
/** Fixed-horizon, assignment-level decision for an unpaired served canary. */
|
|
460
|
+
type CanaryArm = 'control' | 'candidate';
|
|
461
|
+
/** One assigned unit, including unsuccessful and unobserved executions. */
|
|
462
|
+
interface CanaryAssignmentReceipt {
|
|
463
|
+
assignmentId: string;
|
|
464
|
+
assignmentSourceId: string;
|
|
465
|
+
clusterId: string;
|
|
466
|
+
arm: CanaryArm;
|
|
467
|
+
}
|
|
468
|
+
interface CanaryObservation extends CanaryAssignmentReceipt {
|
|
469
|
+
/** Independently checked binary outcome. Null means measurement is unavailable. */
|
|
470
|
+
checkedOutcome: 0 | 1 | null;
|
|
471
|
+
outcomeSourceId: string | null;
|
|
472
|
+
/** Billed cost, including measured zero. Null means billing is unavailable. */
|
|
473
|
+
billedCostUsd: number | null;
|
|
474
|
+
billingSourceId: string | null;
|
|
475
|
+
/** Trace identity may be null for an assigned unit that never executed. */
|
|
476
|
+
traceSourceId: string | null;
|
|
477
|
+
/** Required when no trace exists; identifies the independent no-execution record. */
|
|
478
|
+
noExecutionSourceId: string | null;
|
|
479
|
+
/** Observed identities; null only when there was no execution or no checked outcome. */
|
|
480
|
+
servedProfileDigest: string | null;
|
|
481
|
+
servedCodeRevisionDigest: string | null;
|
|
482
|
+
outcomeCheckerDigest: string | null;
|
|
483
|
+
turns: number | null;
|
|
484
|
+
}
|
|
485
|
+
/** Seal this prospective protocol before the first eligible live assignment. */
|
|
486
|
+
interface RandomizedCanaryRule {
|
|
487
|
+
experimentId: string;
|
|
488
|
+
populationId: string;
|
|
489
|
+
eligibilityRuleDigest: string;
|
|
490
|
+
randomizationSourceId: string;
|
|
491
|
+
assignmentLedgerAuthorityId: string;
|
|
492
|
+
controlProfileDigest: string;
|
|
493
|
+
candidateProfileDigest: string;
|
|
494
|
+
servedCodeRevisionDigest: string;
|
|
495
|
+
outcomeCheckerDigest: string;
|
|
496
|
+
/** One externally reserved slot in a fixed family of confirmatory canaries. */
|
|
497
|
+
confirmatoryFamilyId: string;
|
|
498
|
+
confirmatoryFamilySize: number;
|
|
499
|
+
confirmatoryIndex: number;
|
|
500
|
+
familyReservationSourceId: string;
|
|
501
|
+
assignmentUnit: 'customer' | 'session';
|
|
502
|
+
/** Customer clustering also permits sessions of one customer in both arms. */
|
|
503
|
+
clusterUnit: 'customer' | 'session';
|
|
504
|
+
stoppingRule: 'fixed-time';
|
|
505
|
+
/** No assignment after this UTC time enters the frozen cohort. */
|
|
506
|
+
analysisCutoff: string;
|
|
507
|
+
/** Wait this long after cutoff for checked outcomes and billing to mature. */
|
|
508
|
+
outcomeMaturityMs: number;
|
|
509
|
+
/** Candidate minus control success probability must exceed this margin. */
|
|
510
|
+
minimumLift: number;
|
|
511
|
+
/** At least 40 total independent clusters and 20 represented in each arm. */
|
|
512
|
+
minimumClusters: number;
|
|
513
|
+
}
|
|
514
|
+
/** Post-cutoff evidence from the independently verified append-only assignment ledger. */
|
|
515
|
+
interface CanaryCohortReceipt {
|
|
516
|
+
protocolDigest: string;
|
|
517
|
+
assignmentRosterDigest: string;
|
|
518
|
+
assignmentCount: number;
|
|
519
|
+
assignmentLedgerSourceId: string;
|
|
520
|
+
assignmentLedgerTipDigest: string;
|
|
521
|
+
outcomeLedgerSourceId: string;
|
|
522
|
+
billingLedgerSourceId: string;
|
|
523
|
+
observationSnapshotDigest: string;
|
|
524
|
+
/** Exact preregistered cutoff + outcomeMaturityMs, not a selectable later look. */
|
|
525
|
+
observationFrozenAt: string;
|
|
526
|
+
authorityId: string;
|
|
527
|
+
attestationSourceId: string;
|
|
528
|
+
closedAt: string;
|
|
529
|
+
attestedAt: string;
|
|
530
|
+
}
|
|
531
|
+
/** Host verifies the independent witnesses and append-only ledger before inference. */
|
|
532
|
+
type CanaryCohortVerifier = (receipt: CanaryCohortReceipt, protocol: SealedRandomizedCanaryRule) => {
|
|
533
|
+
verified: true;
|
|
534
|
+
authorityId: string;
|
|
535
|
+
protocolWitnessedBeforeTraffic: boolean;
|
|
536
|
+
familySlotReservedBeforeTraffic: boolean;
|
|
537
|
+
eligibilityAndDispositionVerified: boolean;
|
|
538
|
+
randomizationVerified: boolean;
|
|
539
|
+
rosterCompleteAtCutoff: boolean;
|
|
540
|
+
outcomeAndBillingSnapshotFrozen: boolean;
|
|
541
|
+
sourceJoinsVerified: boolean;
|
|
542
|
+
armIsolationVerified: boolean;
|
|
543
|
+
} | {
|
|
544
|
+
verified: false;
|
|
545
|
+
reason: string;
|
|
546
|
+
};
|
|
547
|
+
interface SealedRandomizedCanaryRule {
|
|
548
|
+
rule: RandomizedCanaryRule;
|
|
549
|
+
digest: string;
|
|
550
|
+
algo: 'sha256-rfc8785';
|
|
551
|
+
}
|
|
552
|
+
type CanaryRefusal = 'missing-evidence' | 'missing-trace' | 'execution-drift' | 'insufficient-clusters' | 'dominant-cluster' | 'zero-variance' | null;
|
|
553
|
+
interface RandomizedCanaryDecision {
|
|
554
|
+
registrationDigest: string;
|
|
555
|
+
cohortReceiptSourceId: string;
|
|
556
|
+
assignmentRosterDigest: string;
|
|
557
|
+
estimand: 'assignment-weighted-itt-risk-difference';
|
|
558
|
+
method: 'cluster-robust-t';
|
|
559
|
+
/** The fixed headline remains a nominal 95% interval. */
|
|
560
|
+
confidence: 0.95;
|
|
561
|
+
/** Additional Bonferroni level: 1 - 0.05 / registered family size. */
|
|
562
|
+
familyAdjustedConfidence: number;
|
|
563
|
+
assignments: number;
|
|
564
|
+
controlAssignments: number;
|
|
565
|
+
candidateAssignments: number;
|
|
566
|
+
clusters: number;
|
|
567
|
+
controlClusters: number;
|
|
568
|
+
candidateClusters: number;
|
|
569
|
+
coverage: Record<CanaryArm, {
|
|
570
|
+
checkedOutcomes: number;
|
|
571
|
+
billedAssignments: number;
|
|
572
|
+
turnReceipts: number;
|
|
573
|
+
traceReceipts: number;
|
|
574
|
+
}>;
|
|
575
|
+
missingOutcomes: number;
|
|
576
|
+
missingBilling: number;
|
|
577
|
+
missingTurns: number;
|
|
578
|
+
missingTraces: number;
|
|
579
|
+
executionDrift: number;
|
|
580
|
+
controlSuccessRate: number | null;
|
|
581
|
+
candidateSuccessRate: number | null;
|
|
582
|
+
delta: number | null;
|
|
583
|
+
nominal95Interval: {
|
|
584
|
+
low: number;
|
|
585
|
+
high: number;
|
|
586
|
+
} | null;
|
|
587
|
+
headline95Pass: boolean;
|
|
588
|
+
familyAdjustedInterval: {
|
|
589
|
+
low: number;
|
|
590
|
+
high: number;
|
|
591
|
+
} | null;
|
|
592
|
+
familywisePass: boolean;
|
|
593
|
+
/** Two-sided zero-null p-value before the registered family correction. */
|
|
594
|
+
unadjustedPValue: number | null;
|
|
595
|
+
controlBilledCostUsd: number | null;
|
|
596
|
+
candidateBilledCostUsd: number | null;
|
|
597
|
+
controlTurns: number | null;
|
|
598
|
+
candidateTurns: number | null;
|
|
599
|
+
refusal: CanaryRefusal;
|
|
600
|
+
/** Primary lift criterion only; product cost and latency gates remain external. */
|
|
601
|
+
successCriterionMet: boolean;
|
|
602
|
+
}
|
|
603
|
+
/** Digest only assignment facts, so outcome and cost cannot change the roster. */
|
|
604
|
+
declare function randomizedCanaryRosterDigest(rows: readonly CanaryAssignmentReceipt[]): string;
|
|
605
|
+
/** Commit to the one complete checked-outcome, trace, turn, and billing snapshot. */
|
|
606
|
+
declare function randomizedCanaryObservationDigest(rows: readonly CanaryObservation[]): string;
|
|
607
|
+
/** Seal the fixed decision controls; retaining this digest alone cannot prove preregistration time. */
|
|
608
|
+
declare function sealRandomizedCanaryRule(rule: RandomizedCanaryRule): SealedRandomizedCanaryRule;
|
|
609
|
+
/** Apply one registered analysis to the entire frozen assignment roster. */
|
|
610
|
+
declare function decideRandomizedCanary(sealed: SealedRandomizedCanaryRule, cohort: CanaryCohortReceipt, rows: readonly CanaryObservation[], verifyCohort: CanaryCohortVerifier): RandomizedCanaryDecision;
|
|
611
|
+
//#endregion
|
|
612
|
+
export { type AdmissionExecution, type AdmissionPartition, type AdmissionRule, type AdmissionStage, type ArmRealizedBudget, type ArmSpec, BOOTSTRAP_GATE_MIN_N, type BudgetRule, type CampaignEvidenceContext, type CanaryArm, type CanaryAssignmentReceipt, type CanaryCohortReceipt, type CanaryCohortVerifier, type CanaryObservation, type CanaryRefusal, type ClusteredPowerOptions, type ClusteredPowerPoint, type ClusteredPowerRefusal, type ClusteredPowerResult, type ComputedInterval, type Condition, type CreateEvidenceReceiptInput, type DecisionBranch, type DecisionOutcome, type DecisionRule, type DerivedQuantities, DesignRefusalError, type EProcess, type EProcessOptions, type EProcessState, type EProcessStep, EVIDENCE_AUTHORITY_KINDS, EVIDENCE_RECEIPT_VERSION, EVIDENCE_STATES, type Estimand, type EstimandResult, type EvaluationClaim, type EvaluationUnitSummary, type EvidenceAuthority, type EvidenceAuthorityKind, type EvidenceBinding, type EvidenceDenominator, type EvidenceReceipt, type EvidenceReceiptVerification, type EvidenceRecord, type EvidenceRegistryRecord, type EvidenceState, type EvidenceVector, type Experiment, type ExperimentFunnel, type ExperimentRep, type ExperimentSpec, type ExperimentStats, ExperimentTracker, type ExperimentVerdict, FinalEvidenceConflictError, FinalEvidenceError, type FinalEvidenceLedger, type FinalEvidenceMeasurement, type FinalEvidenceOutcome, type FinalEvidenceRecord, type FinalEvidenceReservation, FunnelIntegrityError, type FunnelPartitionCount, type FunnelStageCount, type FunnelStageInput, type GateEvidence, type GateResult, type HaltOutcome, type HaltRule, type HeldoutSignificance, type HeldoutSignificanceOptions, type HypothesisManifest, type HypothesisResult, INDEPENDENT_EVIDENCE_AUTHORITY_KINDS, type IntervalSpec, type JsonValue, MatchedBudgetError, type MatchedBudgetRule, type MatchedBudgetVerdict, type MatchedPair, type NLadderProjection, type Obligation, type OutcomeSpec, type PairArmsOptions, type PairArmsResult, type PairedArmRow, type PairedBootstrapOptions, type PairedBootstrapResult, type PairedEvalueOptions, type PairedEvalueSequence, type PairedEvalueStep, type PairedHoldout, type PowerPreflight, type PowerPreflightOptions, type Predicate, type PromotionPolicy, type ProportionInterval, type RandomizedCanaryDecision, type RandomizedCanaryRule, type RegisteredExperiment, type ReissuePolicy, type ReissueVerdict, type SealAmendment, SealIntegrityError, type SealedExperiment, type SealedRandomizedCanaryRule, type SeedDerivation, type SelectionRule, type SequentialCrossingHorizon, type SequentialCrossingHorizonOptions, type SequentialDecision, type SequentialObservation, type SequentialPairedGate, type SequentialPairedGateOptions, type SetExpr, type SignFlipFloor, type SignedManifest, type SignedManifestAlgo, type UniformPassDecision, type UniformPassSchedule, type ValidityGate, amendExperiment, assertDesignAdequate, assertFunnelReconciles, assertMatchedBudgets, benjaminiHochberg, bonferroni, buildEvidenceVector, buildFunnel, classifyReissue, clusteredPower, comparePairedArms, composeFunnels, computeEstimand, computeInterval, createCampaignEvidenceReceipt, createEvidenceReceipt, decideRandomizedCanary, defineEvaluationClaim, defineExperiment, eProcess, evaluateCondition, evaluateHaltRule, evaluateHypothesis, evaluateIdentityGate, evaluateOracleDeterminismGate, evaluatePopulationReproducibilityGate, evaluatePowerFloorGate, evaluatePredicate, evaluateProvenanceGate, evidenceRegistryRecordSchema, executeAdmissionRule, executeDecisionRule, fileExperimentStore, hashJson, heldoutSignificance, holm, inMemoryExperimentStore, isIndependentEvidence, manifestContentDigest, mcnemar, mcnemarPower, mcnemarRequiredN, mulberry32, openFinalEvidenceLedger, openSealedExperiment, pairArms, pairHoldout, pairRunRecords, pairedBootstrap, pairedEvalueSequence, pairedMde, pairedRiskDifference, pairedRiskDifferenceExact, pairedRiskDifferenceScore, paretoPolicy, paretoSignificanceGate, parseEvidenceRegistryRecord, powerPreflight, projectNLadderBudget, randomizedCanaryObservationDigest, randomizedCanaryRosterDigest, readField, renderEvidenceIndex, renderFunnelTable, requiredPairedSampleSize, requiredSampleSize, runSelectionRule, runUniformPassBudget, sealExperiment, sealRandomizedCanaryRule, sequentialCrossingHorizon, sequentialDecide, sequentialPairedGate, signManifest, summarizeEvaluationUnits, validateEvidenceRegistry, verifyEvidenceReceipt, verifyManifest, verifyMatchedBudgets, verifySealedExperiment, wilson };
|
|
459
613
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/experiment/budget.ts","../../src/experiment/funnel.ts","../../src/experiment/define.ts","../../src/experiment/evidence-record.ts","../../src/experiment/power.ts"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/experiment/budget.ts","../../src/experiment/funnel.ts","../../src/experiment/define.ts","../../src/experiment/evidence-record.ts","../../src/experiment/power.ts","../../src/experiment/randomized-canary.ts"],"mappings":"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Cf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;EACA;EACA;EACA;EACA;;EAEA;EACA;;EAEA;;EAEA;;EAEA;;EAEA;;;UAIe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;EACA;EACA;EACA;EACA;;;KAIU,wBACV,SAAS,qBACT,UAAU;EAGN;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;EAAiB;;UAEN;EACf,MAAM;EACN;EACA;;KAGU;UASK;EACf;EACA;EACA;EACA;EACA;;EAEA;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,UAAU,OACR;IAEE;IACA;IACA;IACA;;EAGJ;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;IAAqB;IAAa;;EAClC;EACA;IAA0B;IAAa;;EACvC;;EAEA;EACA;EACA;EACA;EACA;EACA,SAAS;;EAET;;;iBAwBc,6BAA6B,eAAe;;iBA6B5C,kCAAkC,eAAe;;iBAiFjD,yBAAyB,MAAM,uBAAuB;;iBAOtD,uBACd,QAAQ,4BACR,QAAQ,qBACR,eAAe,qBACf,cAAc,uBACb"}
|
package/dist/experiment/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { n as CaptureIntegrityError, s as ValidationError } from "../errors-Dngq5h35.js";
|
|
2
|
-
import { a as hashCanonical } from "../canonical-BsZVgBhW.js";
|
|
2
|
+
import { a as hashCanonical, t as LEDGER_HASH_PATTERN } from "../canonical-BsZVgBhW.js";
|
|
3
3
|
import { _ as verifyManifest, g as signManifest, h as manifestContentDigest, m as hashJson, p as evaluateHypothesis } from "../agent-profile-cell-DcFW4vOc.js";
|
|
4
|
+
import { n as studentTQuantile, t as studentTCdf } from "../student-t-BA-Uy51p.js";
|
|
4
5
|
import { t as mulberry32 } from "../random-Dn5fPWkt.js";
|
|
5
6
|
import { a as requiredSampleSize, c as holm, i as requiredPairedSampleSize, n as mcnemarRequiredN, o as benjaminiHochberg, r as pairedMde, s as bonferroni, t as mcnemarPower } from "../power-and-mde-BJOpt6pq.js";
|
|
6
7
|
import { f as mcnemar, g as pairedRiskDifferenceScore, h as pairedRiskDifferenceExact, m as pairedRiskDifference, n as pairArms, r as pairRunRecords, t as comparePairedArms, v as wilson } from "../paired-arms-JSDcs2zx.js";
|
|
@@ -850,6 +851,271 @@ function mixSeed(seed, effect) {
|
|
|
850
851
|
return (seed ^ Math.round(effect * 1000003) * 2654435769) >>> 0 | 0;
|
|
851
852
|
}
|
|
852
853
|
//#endregion
|
|
853
|
-
|
|
854
|
+
//#region src/experiment/randomized-canary.ts
|
|
855
|
+
/** Fixed-horizon, assignment-level decision for an unpaired served canary. */
|
|
856
|
+
function nonEmpty(value, label) {
|
|
857
|
+
if (typeof value !== "string" || value.length === 0 || value.trim() !== value) throw new ValidationError(`randomized canary: ${label} must be a nonempty trimmed string`);
|
|
858
|
+
}
|
|
859
|
+
function validateAssignment(row, index) {
|
|
860
|
+
if (row === null || typeof row !== "object" || Array.isArray(row)) throw new ValidationError(`randomized canary: assignment ${index} must be an object`);
|
|
861
|
+
nonEmpty(row.assignmentId, `assignment ${index} id`);
|
|
862
|
+
nonEmpty(row.assignmentSourceId, `assignment ${index} source id`);
|
|
863
|
+
nonEmpty(row.clusterId, `assignment ${index} cluster id`);
|
|
864
|
+
if (row.arm !== "control" && row.arm !== "candidate") throw new ValidationError(`randomized canary: assignment ${index} arm must be control or candidate`);
|
|
865
|
+
}
|
|
866
|
+
/** Digest only assignment facts, so outcome and cost cannot change the roster. */
|
|
867
|
+
function randomizedCanaryRosterDigest(rows) {
|
|
868
|
+
const seen = /* @__PURE__ */ new Set();
|
|
869
|
+
const seenSources = /* @__PURE__ */ new Set();
|
|
870
|
+
const roster = rows.map((row, index) => {
|
|
871
|
+
validateAssignment(row, index);
|
|
872
|
+
if (seen.has(row.assignmentId)) throw new ValidationError(`randomized canary: duplicate assignment id '${row.assignmentId}'`);
|
|
873
|
+
seen.add(row.assignmentId);
|
|
874
|
+
if (seenSources.has(row.assignmentSourceId)) throw new ValidationError(`randomized canary: duplicate assignment source id '${row.assignmentSourceId}'`);
|
|
875
|
+
seenSources.add(row.assignmentSourceId);
|
|
876
|
+
return {
|
|
877
|
+
assignmentId: row.assignmentId,
|
|
878
|
+
assignmentSourceId: row.assignmentSourceId,
|
|
879
|
+
clusterId: row.clusterId,
|
|
880
|
+
arm: row.arm
|
|
881
|
+
};
|
|
882
|
+
});
|
|
883
|
+
roster.sort((a, b) => a.assignmentId < b.assignmentId ? -1 : a.assignmentId > b.assignmentId ? 1 : 0);
|
|
884
|
+
return hashCanonical(roster);
|
|
885
|
+
}
|
|
886
|
+
/** Commit to the one complete checked-outcome, trace, turn, and billing snapshot. */
|
|
887
|
+
function randomizedCanaryObservationDigest(rows) {
|
|
888
|
+
randomizedCanaryRosterDigest(rows);
|
|
889
|
+
const snapshot = rows.map((row) => ({
|
|
890
|
+
assignmentId: row.assignmentId,
|
|
891
|
+
checkedOutcome: row.checkedOutcome,
|
|
892
|
+
outcomeSourceId: row.outcomeSourceId,
|
|
893
|
+
billedCostUsd: row.billedCostUsd,
|
|
894
|
+
billingSourceId: row.billingSourceId,
|
|
895
|
+
traceSourceId: row.traceSourceId,
|
|
896
|
+
noExecutionSourceId: row.noExecutionSourceId,
|
|
897
|
+
servedProfileDigest: row.servedProfileDigest,
|
|
898
|
+
servedCodeRevisionDigest: row.servedCodeRevisionDigest,
|
|
899
|
+
outcomeCheckerDigest: row.outcomeCheckerDigest,
|
|
900
|
+
turns: row.turns
|
|
901
|
+
}));
|
|
902
|
+
snapshot.sort((a, b) => a.assignmentId < b.assignmentId ? -1 : a.assignmentId > b.assignmentId ? 1 : 0);
|
|
903
|
+
return hashCanonical(snapshot);
|
|
904
|
+
}
|
|
905
|
+
function validateRule(rule) {
|
|
906
|
+
for (const field of [
|
|
907
|
+
"experimentId",
|
|
908
|
+
"populationId",
|
|
909
|
+
"eligibilityRuleDigest",
|
|
910
|
+
"randomizationSourceId",
|
|
911
|
+
"assignmentLedgerAuthorityId",
|
|
912
|
+
"controlProfileDigest",
|
|
913
|
+
"candidateProfileDigest",
|
|
914
|
+
"servedCodeRevisionDigest",
|
|
915
|
+
"outcomeCheckerDigest",
|
|
916
|
+
"confirmatoryFamilyId",
|
|
917
|
+
"familyReservationSourceId",
|
|
918
|
+
"analysisCutoff"
|
|
919
|
+
]) nonEmpty(rule[field], field);
|
|
920
|
+
if (!Number.isSafeInteger(rule.confirmatoryFamilySize) || rule.confirmatoryFamilySize < 1 || rule.confirmatoryFamilySize > 1e3) throw new ValidationError("randomized canary: confirmatoryFamilySize must be in [1, 1000]");
|
|
921
|
+
if (!Number.isSafeInteger(rule.confirmatoryIndex) || rule.confirmatoryIndex < 1 || rule.confirmatoryIndex > rule.confirmatoryFamilySize) throw new ValidationError("randomized canary: confirmatoryIndex must name a reserved family slot");
|
|
922
|
+
if (rule.assignmentUnit !== "customer" && rule.assignmentUnit !== "session") throw new ValidationError("randomized canary: assignmentUnit must be customer or session");
|
|
923
|
+
if (rule.clusterUnit !== "customer" && rule.clusterUnit !== "session") throw new ValidationError("randomized canary: clusterUnit must be customer or session");
|
|
924
|
+
if (rule.assignmentUnit === "customer" && rule.clusterUnit !== "customer") throw new ValidationError("randomized canary: customer assignments must cluster by customer");
|
|
925
|
+
if (rule.stoppingRule !== "fixed-time") throw new ValidationError("randomized canary: only fixed-time stopping is supported");
|
|
926
|
+
if (!Number.isFinite(Date.parse(rule.analysisCutoff)) || !rule.analysisCutoff.endsWith("Z")) throw new ValidationError("randomized canary: analysisCutoff must be a UTC timestamp");
|
|
927
|
+
if (!Number.isSafeInteger(rule.outcomeMaturityMs) || rule.outcomeMaturityMs < 0) throw new ValidationError("randomized canary: outcomeMaturityMs must be a nonnegative integer");
|
|
928
|
+
if (!Number.isFinite(rule.minimumLift) || rule.minimumLift < 0 || rule.minimumLift >= 1) throw new ValidationError("randomized canary: minimumLift must be in [0, 1)");
|
|
929
|
+
if (!Number.isSafeInteger(rule.minimumClusters) || rule.minimumClusters < 40) throw new ValidationError("randomized canary: minimumClusters must be at least 40");
|
|
930
|
+
}
|
|
931
|
+
/** Seal the fixed decision controls; retaining this digest alone cannot prove preregistration time. */
|
|
932
|
+
function sealRandomizedCanaryRule(rule) {
|
|
933
|
+
validateRule(rule);
|
|
934
|
+
const captured = structuredClone(rule);
|
|
935
|
+
return {
|
|
936
|
+
rule: captured,
|
|
937
|
+
digest: hashCanonical(captured),
|
|
938
|
+
algo: "sha256-rfc8785"
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
/** Apply one registered analysis to the entire frozen assignment roster. */
|
|
942
|
+
function decideRandomizedCanary(sealed, cohort, rows, verifyCohort) {
|
|
943
|
+
if (sealed.algo !== "sha256-rfc8785") throw new ValidationError("randomized canary: unsupported registration digest scheme");
|
|
944
|
+
validateRule(sealed.rule);
|
|
945
|
+
if (hashCanonical(sealed.rule) !== sealed.digest) throw new ValidationError("randomized canary: registration digest mismatch");
|
|
946
|
+
if (cohort === null || typeof cohort !== "object" || Array.isArray(cohort)) throw new ValidationError("randomized canary: independent cohort receipt is required");
|
|
947
|
+
for (const field of [
|
|
948
|
+
"assignmentLedgerSourceId",
|
|
949
|
+
"outcomeLedgerSourceId",
|
|
950
|
+
"billingLedgerSourceId",
|
|
951
|
+
"authorityId",
|
|
952
|
+
"attestationSourceId",
|
|
953
|
+
"closedAt",
|
|
954
|
+
"observationFrozenAt",
|
|
955
|
+
"attestedAt"
|
|
956
|
+
]) nonEmpty(cohort[field], `cohort ${field}`);
|
|
957
|
+
if (cohort.protocolDigest !== sealed.digest) throw new ValidationError("randomized canary: cohort protocol digest mismatch");
|
|
958
|
+
if (cohort.authorityId !== sealed.rule.assignmentLedgerAuthorityId) throw new ValidationError("randomized canary: cohort authority differs from registered ledger");
|
|
959
|
+
for (const field of [
|
|
960
|
+
"assignmentRosterDigest",
|
|
961
|
+
"assignmentLedgerTipDigest",
|
|
962
|
+
"observationSnapshotDigest"
|
|
963
|
+
]) if (!LEDGER_HASH_PATTERN.test(cohort[field])) throw new ValidationError(`randomized canary: cohort ${field} must be a sha256 digest`);
|
|
964
|
+
if (!Number.isSafeInteger(cohort.assignmentCount) || cohort.assignmentCount < 1 || rows.length !== cohort.assignmentCount) throw new ValidationError("randomized canary: assignment count differs from complete ledger receipt");
|
|
965
|
+
if (randomizedCanaryRosterDigest(rows) !== cohort.assignmentRosterDigest) throw new ValidationError("randomized canary: assignment roster digest mismatch");
|
|
966
|
+
if (randomizedCanaryObservationDigest(rows) !== cohort.observationSnapshotDigest) throw new ValidationError("randomized canary: frozen observation snapshot digest mismatch");
|
|
967
|
+
const cutoff = Date.parse(sealed.rule.analysisCutoff);
|
|
968
|
+
const closedAt = Date.parse(cohort.closedAt);
|
|
969
|
+
const frozenAt = Date.parse(cohort.observationFrozenAt);
|
|
970
|
+
const attestedAt = Date.parse(cohort.attestedAt);
|
|
971
|
+
const expectedFreezeAt = cutoff + sealed.rule.outcomeMaturityMs;
|
|
972
|
+
if (!Number.isFinite(closedAt) || closedAt < cutoff || !cohort.closedAt.endsWith("Z") || !Number.isFinite(frozenAt) || frozenAt !== expectedFreezeAt || closedAt > frozenAt || !cohort.observationFrozenAt.endsWith("Z") || !Number.isFinite(attestedAt) || attestedAt < frozenAt || !cohort.attestedAt.endsWith("Z")) throw new ValidationError("randomized canary: ledger closure or frozen observation time differs from registered cutoffs");
|
|
973
|
+
if (typeof verifyCohort !== "function") throw new ValidationError("randomized canary: independent cohort verifier is required");
|
|
974
|
+
const verification = verifyCohort(cohort, sealed);
|
|
975
|
+
if (verification?.verified !== true || verification.authorityId !== cohort.authorityId || verification.protocolWitnessedBeforeTraffic !== true || verification.familySlotReservedBeforeTraffic !== true || verification.eligibilityAndDispositionVerified !== true || verification.randomizationVerified !== true || verification.rosterCompleteAtCutoff !== true || verification.outcomeAndBillingSnapshotFrozen !== true || verification.sourceJoinsVerified !== true || verification.armIsolationVerified !== true) throw new ValidationError(`randomized canary: cohort attestation or completeness unverified${verification?.verified === false ? `: ${verification.reason}` : ""}`);
|
|
976
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
977
|
+
const arms = {
|
|
978
|
+
control: [],
|
|
979
|
+
candidate: []
|
|
980
|
+
};
|
|
981
|
+
let missingOutcomes = 0;
|
|
982
|
+
let missingBilling = 0;
|
|
983
|
+
let missingTurns = 0;
|
|
984
|
+
let missingTraces = 0;
|
|
985
|
+
let executionDrift = 0;
|
|
986
|
+
for (const [index, row] of rows.entries()) {
|
|
987
|
+
if (row.checkedOutcome !== 0 && row.checkedOutcome !== 1 && row.checkedOutcome !== null) throw new ValidationError(`randomized canary: assignment ${index} outcome must be 0, 1, or null`);
|
|
988
|
+
if (row.checkedOutcome === null) missingOutcomes++;
|
|
989
|
+
if (row.outcomeSourceId === null) {
|
|
990
|
+
if (row.checkedOutcome !== null) throw new ValidationError(`randomized canary: assignment ${index} has an outcome without its source`);
|
|
991
|
+
} else nonEmpty(row.outcomeSourceId, `assignment ${index} outcome source id`);
|
|
992
|
+
if (row.billedCostUsd === null) missingBilling++;
|
|
993
|
+
else if (!Number.isFinite(row.billedCostUsd) || row.billedCostUsd < 0) throw new ValidationError(`randomized canary: assignment ${index} billed cost must be nonnegative`);
|
|
994
|
+
if (row.billingSourceId === null) {
|
|
995
|
+
if (row.billedCostUsd !== null) throw new ValidationError(`randomized canary: assignment ${index} has billed cost without its source`);
|
|
996
|
+
} else nonEmpty(row.billingSourceId, `assignment ${index} billing source id`);
|
|
997
|
+
if (row.turns === null) missingTurns++;
|
|
998
|
+
else if (!Number.isSafeInteger(row.turns) || row.turns < 0) throw new ValidationError(`randomized canary: assignment ${index} turns must be a nonnegative integer`);
|
|
999
|
+
if (row.traceSourceId !== null) nonEmpty(row.traceSourceId, `assignment ${index} trace source id`);
|
|
1000
|
+
if (row.noExecutionSourceId !== null) nonEmpty(row.noExecutionSourceId, `assignment ${index} no-execution source id`);
|
|
1001
|
+
if (row.traceSourceId !== null && row.noExecutionSourceId !== null) throw new ValidationError(`randomized canary: assignment ${index} has conflicting trace and no-execution receipts`);
|
|
1002
|
+
const noExecution = row.noExecutionSourceId !== null && row.turns === 0 && row.billedCostUsd === 0 && row.checkedOutcome === 0;
|
|
1003
|
+
if (row.traceSourceId === null && !noExecution) missingTraces++;
|
|
1004
|
+
for (const field of [
|
|
1005
|
+
"servedProfileDigest",
|
|
1006
|
+
"servedCodeRevisionDigest",
|
|
1007
|
+
"outcomeCheckerDigest"
|
|
1008
|
+
]) if (row[field] !== null) nonEmpty(row[field], `assignment ${index} ${field}`);
|
|
1009
|
+
const expectedProfile = row.arm === "control" ? sealed.rule.controlProfileDigest : sealed.rule.candidateProfileDigest;
|
|
1010
|
+
if (row.traceSourceId !== null && (row.servedProfileDigest !== expectedProfile || row.servedCodeRevisionDigest !== sealed.rule.servedCodeRevisionDigest) || row.checkedOutcome !== null && row.outcomeCheckerDigest !== sealed.rule.outcomeCheckerDigest || noExecution && (row.servedProfileDigest !== null || row.servedCodeRevisionDigest !== null)) executionDrift++;
|
|
1011
|
+
arms[row.arm].push(row);
|
|
1012
|
+
const bucket = clusters.get(row.clusterId) ?? [];
|
|
1013
|
+
bucket.push(row);
|
|
1014
|
+
clusters.set(row.clusterId, bucket);
|
|
1015
|
+
}
|
|
1016
|
+
const controlClusters = [...clusters.values()].filter((bucket) => bucket.some((r) => r.arm === "control")).length;
|
|
1017
|
+
const candidateClusters = [...clusters.values()].filter((bucket) => bucket.some((r) => r.arm === "candidate")).length;
|
|
1018
|
+
if (sealed.rule.assignmentUnit === sealed.rule.clusterUnit) {
|
|
1019
|
+
for (const [clusterId, bucket] of clusters) if (bucket.length !== 1) throw new ValidationError(`randomized canary: ${sealed.rule.assignmentUnit} cluster '${clusterId}' has multiple assignments`);
|
|
1020
|
+
}
|
|
1021
|
+
const coverage = (arm) => ({
|
|
1022
|
+
checkedOutcomes: arms[arm].filter((row) => row.checkedOutcome !== null).length,
|
|
1023
|
+
billedAssignments: arms[arm].filter((row) => row.billedCostUsd !== null).length,
|
|
1024
|
+
turnReceipts: arms[arm].filter((row) => row.turns !== null).length,
|
|
1025
|
+
traceReceipts: arms[arm].filter((row) => row.traceSourceId !== null).length
|
|
1026
|
+
});
|
|
1027
|
+
const armCoverage = {
|
|
1028
|
+
control: coverage("control"),
|
|
1029
|
+
candidate: coverage("candidate")
|
|
1030
|
+
};
|
|
1031
|
+
const sum = (arm, field) => arms[arm].reduce((total, row) => total + (row[field] ?? 0), 0);
|
|
1032
|
+
const n0 = arms.control.length;
|
|
1033
|
+
const n1 = arms.candidate.length;
|
|
1034
|
+
const outcomesComplete = missingOutcomes === 0 && n0 > 0 && n1 > 0;
|
|
1035
|
+
const p0 = outcomesComplete ? sum("control", "checkedOutcome") / n0 : null;
|
|
1036
|
+
const p1 = outcomesComplete ? sum("candidate", "checkedOutcome") / n1 : null;
|
|
1037
|
+
const delta = p0 === null || p1 === null ? null : p1 - p0;
|
|
1038
|
+
const result = {
|
|
1039
|
+
registrationDigest: sealed.digest,
|
|
1040
|
+
cohortReceiptSourceId: cohort.attestationSourceId,
|
|
1041
|
+
assignmentRosterDigest: cohort.assignmentRosterDigest,
|
|
1042
|
+
estimand: "assignment-weighted-itt-risk-difference",
|
|
1043
|
+
method: "cluster-robust-t",
|
|
1044
|
+
confidence: .95,
|
|
1045
|
+
familyAdjustedConfidence: 1 - .05 / sealed.rule.confirmatoryFamilySize,
|
|
1046
|
+
assignments: rows.length,
|
|
1047
|
+
controlAssignments: n0,
|
|
1048
|
+
candidateAssignments: n1,
|
|
1049
|
+
clusters: clusters.size,
|
|
1050
|
+
controlClusters,
|
|
1051
|
+
candidateClusters,
|
|
1052
|
+
coverage: armCoverage,
|
|
1053
|
+
missingOutcomes,
|
|
1054
|
+
missingBilling,
|
|
1055
|
+
missingTurns,
|
|
1056
|
+
missingTraces,
|
|
1057
|
+
executionDrift,
|
|
1058
|
+
controlSuccessRate: p0,
|
|
1059
|
+
candidateSuccessRate: p1,
|
|
1060
|
+
delta,
|
|
1061
|
+
nominal95Interval: null,
|
|
1062
|
+
headline95Pass: false,
|
|
1063
|
+
familyAdjustedInterval: null,
|
|
1064
|
+
familywisePass: false,
|
|
1065
|
+
unadjustedPValue: null,
|
|
1066
|
+
controlBilledCostUsd: armCoverage.control.billedAssignments === n0 ? sum("control", "billedCostUsd") : null,
|
|
1067
|
+
candidateBilledCostUsd: armCoverage.candidate.billedAssignments === n1 ? sum("candidate", "billedCostUsd") : null,
|
|
1068
|
+
controlTurns: armCoverage.control.turnReceipts === n0 ? sum("control", "turns") : null,
|
|
1069
|
+
candidateTurns: armCoverage.candidate.turnReceipts === n1 ? sum("candidate", "turns") : null,
|
|
1070
|
+
refusal: null,
|
|
1071
|
+
successCriterionMet: false
|
|
1072
|
+
};
|
|
1073
|
+
if (missingOutcomes > 0 || missingBilling > 0 || missingTurns > 0 || n0 === 0 || n1 === 0) {
|
|
1074
|
+
result.refusal = "missing-evidence";
|
|
1075
|
+
return result;
|
|
1076
|
+
}
|
|
1077
|
+
if (missingTraces > 0) {
|
|
1078
|
+
result.refusal = "missing-trace";
|
|
1079
|
+
return result;
|
|
1080
|
+
}
|
|
1081
|
+
if (executionDrift > 0) {
|
|
1082
|
+
result.refusal = "execution-drift";
|
|
1083
|
+
return result;
|
|
1084
|
+
}
|
|
1085
|
+
if (clusters.size < sealed.rule.minimumClusters || controlClusters < 20 || candidateClusters < 20) {
|
|
1086
|
+
result.refusal = "insufficient-clusters";
|
|
1087
|
+
return result;
|
|
1088
|
+
}
|
|
1089
|
+
for (const bucket of clusters.values()) if (bucket.filter((r) => r.arm === "control").length / n0 > .1 || bucket.filter((r) => r.arm === "candidate").length / n1 > .1) {
|
|
1090
|
+
result.refusal = "dominant-cluster";
|
|
1091
|
+
return result;
|
|
1092
|
+
}
|
|
1093
|
+
const scores = [...clusters.values()].map((bucket) => bucket.reduce((score, row) => score + (row.arm === "candidate" ? (row.checkedOutcome - p1) / n1 : -(row.checkedOutcome - p0) / n0), 0));
|
|
1094
|
+
const G = clusters.size;
|
|
1095
|
+
const variance = G / (G - 1) * (rows.length - 1) / (rows.length - 2) * scores.reduce((total, score) => total + score * score, 0);
|
|
1096
|
+
if (!Number.isFinite(variance) || variance <= 0) {
|
|
1097
|
+
result.refusal = "zero-variance";
|
|
1098
|
+
return result;
|
|
1099
|
+
}
|
|
1100
|
+
const se = Math.sqrt(variance);
|
|
1101
|
+
const t = delta / se;
|
|
1102
|
+
const critical95 = studentTQuantile(.975, G - 1);
|
|
1103
|
+
const familyCritical = studentTQuantile(1 - (1 - result.familyAdjustedConfidence) / 2, G - 1);
|
|
1104
|
+
result.nominal95Interval = {
|
|
1105
|
+
low: delta - critical95 * se,
|
|
1106
|
+
high: delta + critical95 * se
|
|
1107
|
+
};
|
|
1108
|
+
result.familyAdjustedInterval = {
|
|
1109
|
+
low: delta - familyCritical * se,
|
|
1110
|
+
high: delta + familyCritical * se
|
|
1111
|
+
};
|
|
1112
|
+
result.unadjustedPValue = Math.min(1, 2 * (1 - studentTCdf(Math.abs(t), G - 1)));
|
|
1113
|
+
result.headline95Pass = result.nominal95Interval.low > 0;
|
|
1114
|
+
result.familywisePass = result.familyAdjustedInterval.low > sealed.rule.minimumLift;
|
|
1115
|
+
result.successCriterionMet = result.headline95Pass && result.familywisePass;
|
|
1116
|
+
return result;
|
|
1117
|
+
}
|
|
1118
|
+
//#endregion
|
|
1119
|
+
export { BOOTSTRAP_GATE_MIN_N, DesignRefusalError, EVIDENCE_AUTHORITY_KINDS, EVIDENCE_RECEIPT_VERSION, EVIDENCE_STATES, ExperimentTracker, FinalEvidenceConflictError, FinalEvidenceError, FunnelIntegrityError, INDEPENDENT_EVIDENCE_AUTHORITY_KINDS, MatchedBudgetError, SealIntegrityError, amendExperiment, assertDesignAdequate, assertFunnelReconciles, assertMatchedBudgets, benjaminiHochberg, bonferroni, buildEvidenceVector, buildFunnel, classifyReissue, clusteredPower, comparePairedArms, composeFunnels, computeEstimand, computeInterval, createCampaignEvidenceReceipt, createEvidenceReceipt, decideRandomizedCanary, defineEvaluationClaim, defineExperiment, eProcess, evaluateCondition, evaluateHaltRule, evaluateHypothesis, evaluateIdentityGate, evaluateOracleDeterminismGate, evaluatePopulationReproducibilityGate, evaluatePowerFloorGate, evaluatePredicate, evaluateProvenanceGate, evidenceRegistryRecordSchema, executeAdmissionRule, executeDecisionRule, fileExperimentStore, hashJson, heldoutSignificance, holm, inMemoryExperimentStore, isIndependentEvidence, manifestContentDigest, mcnemar, mcnemarPower, mcnemarRequiredN, mulberry32, openFinalEvidenceLedger, openSealedExperiment, pairArms, pairHoldout, pairRunRecords, pairedBootstrap, pairedEvalueSequence, pairedMde, pairedRiskDifference, pairedRiskDifferenceExact, pairedRiskDifferenceScore, paretoPolicy, paretoSignificanceGate, parseEvidenceRegistryRecord, powerPreflight, projectNLadderBudget, randomizedCanaryObservationDigest, randomizedCanaryRosterDigest, readField, renderEvidenceIndex, renderFunnelTable, requiredPairedSampleSize, requiredSampleSize, runSelectionRule, runUniformPassBudget, sealExperiment, sealRandomizedCanaryRule, sequentialCrossingHorizon, sequentialDecide, sequentialPairedGate, signManifest, summarizeEvaluationUnits, validateEvidenceRegistry, verifyEvidenceReceipt, verifyManifest, verifyMatchedBudgets, verifySealedExperiment, wilson };
|
|
854
1120
|
|
|
855
1121
|
//# sourceMappingURL=index.js.map
|