modelrig 0.7.0 → 0.8.1
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 +118 -0
- package/README.md +86 -0
- package/dist/bakeoff/disagreement.d.ts +93 -0
- package/dist/bakeoff/disagreement.d.ts.map +1 -0
- package/dist/bakeoff/disagreement.js +140 -0
- package/dist/bakeoff/engine.d.ts.map +1 -1
- package/dist/bakeoff/engine.js +58 -2
- package/dist/bakeoff/pairwise.d.ts +102 -0
- package/dist/bakeoff/pairwise.d.ts.map +1 -0
- package/dist/bakeoff/pairwise.js +214 -0
- package/dist/bakeoff/replay.d.ts +6 -0
- package/dist/bakeoff/replay.d.ts.map +1 -1
- package/dist/bakeoff/replay.js +6 -2
- package/dist/bakeoff/report.d.ts +7 -0
- package/dist/bakeoff/report.d.ts.map +1 -1
- package/dist/bakeoff/report.js +46 -0
- package/dist/bakeoff/types.d.ts +24 -0
- package/dist/bakeoff/types.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -9
- package/dist/mask/apply.d.ts +80 -0
- package/dist/mask/apply.d.ts.map +1 -0
- package/dist/mask/apply.js +228 -0
- package/dist/mask/detect.d.ts +38 -0
- package/dist/mask/detect.d.ts.map +1 -0
- package/dist/mask/detect.js +167 -0
- package/dist/mask/index.d.ts +21 -0
- package/dist/mask/index.d.ts.map +1 -0
- package/dist/mask/index.js +54 -0
- package/dist/mask/mask-policy-cli.d.ts +11 -0
- package/dist/mask/mask-policy-cli.d.ts.map +1 -0
- package/dist/mask/mask-policy-cli.js +33 -0
- package/dist/mask/policy.d.ts +38 -0
- package/dist/mask/policy.d.ts.map +1 -0
- package/dist/mask/policy.js +187 -0
- package/dist/mask/proof.d.ts +54 -0
- package/dist/mask/proof.d.ts.map +1 -0
- package/dist/mask/proof.js +120 -0
- package/dist/mask/receipt.d.ts +30 -0
- package/dist/mask/receipt.d.ts.map +1 -0
- package/dist/mask/receipt.js +49 -0
- package/dist/mask/seam.d.ts +53 -0
- package/dist/mask/seam.d.ts.map +1 -0
- package/dist/mask/seam.js +120 -0
- package/dist/mask/types.d.ts +81 -0
- package/dist/mask/types.d.ts.map +1 -0
- package/dist/mask/types.js +10 -0
- package/dist/mask/vault.d.ts +50 -0
- package/dist/mask/vault.d.ts.map +1 -0
- package/dist/mask/vault.js +81 -0
- package/dist/receipts/types.d.ts +65 -0
- package/dist/receipts/types.d.ts.map +1 -0
- package/dist/receipts/types.js +134 -0
- package/dist/routes/loader-warnings.d.ts +8 -0
- package/dist/routes/loader-warnings.d.ts.map +1 -1
- package/dist/routes/loader-warnings.js +23 -0
- package/dist/routes/loader.d.ts +1 -1
- package/dist/routes/loader.d.ts.map +1 -1
- package/dist/routes/loader.js +24 -25
- package/dist/routes/mask-loader.d.ts +18 -0
- package/dist/routes/mask-loader.d.ts.map +1 -0
- package/dist/routes/mask-loader.js +39 -0
- package/dist/routes/objective-loader.d.ts.map +1 -1
- package/dist/routes/objective-loader.js +51 -17
- package/dist/routes/screen-loader.d.ts +23 -0
- package/dist/routes/screen-loader.d.ts.map +1 -0
- package/dist/routes/screen-loader.js +79 -0
- package/dist/routes/template.d.ts +10 -0
- package/dist/routes/template.d.ts.map +1 -1
- package/dist/routes/template.js +10 -3
- package/dist/run/accept.d.ts +4 -0
- package/dist/run/accept.d.ts.map +1 -1
- package/dist/run/accept.js +2 -0
- package/dist/run/dispatch.d.ts +35 -0
- package/dist/run/dispatch.d.ts.map +1 -1
- package/dist/run/dispatch.js +33 -0
- package/dist/run/grounding.d.ts +47 -2
- package/dist/run/grounding.d.ts.map +1 -1
- package/dist/run/grounding.js +113 -6
- package/dist/run/raw.d.ts.map +1 -1
- package/dist/run/raw.js +6 -1
- package/dist/run/record-attempt.d.ts.map +1 -1
- package/dist/run/record-attempt.js +12 -1
- package/dist/run/run.d.ts.map +1 -1
- package/dist/run/run.js +19 -16
- package/dist/screen/provenance.d.ts +59 -0
- package/dist/screen/provenance.d.ts.map +1 -0
- package/dist/screen/provenance.js +104 -0
- package/dist/screen/types.d.ts +71 -0
- package/dist/screen/types.d.ts.map +1 -0
- package/dist/screen/types.js +9 -0
- package/dist/status-cli.d.ts +11 -0
- package/dist/status-cli.d.ts.map +1 -0
- package/dist/status-cli.js +21 -0
- package/dist/status-runs.d.ts +38 -0
- package/dist/status-runs.d.ts.map +1 -0
- package/dist/status-runs.js +80 -0
- package/dist/status.d.ts +7 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +11 -2
- package/dist/telemetry/ingest-protocol.d.ts +8 -0
- package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
- package/dist/telemetry/store-migrations.d.ts.map +1 -1
- package/dist/telemetry/store-migrations.js +11 -0
- package/dist/telemetry/store.d.ts.map +1 -1
- package/dist/telemetry/store.js +12 -3
- package/dist/types/provider.d.ts +11 -0
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/provider.js +9 -0
- package/dist/types/route.d.ts +55 -0
- package/dist/types/route.d.ts.map +1 -1
- package/dist/types/telemetry.d.ts +7 -0
- package/dist/types/telemetry.d.ts.map +1 -1
- package/dist/validate-cli.d.ts.map +1 -1
- package/dist/validate-cli.js +13 -0
- package/dist/verify/types.d.ts +6 -0
- package/dist/verify/types.d.ts.map +1 -1
- package/docs/agent-skill/SKILL.md +131 -0
- package/docs/how-it-fits.md +34 -0
- package/docs/instrumentation.md +221 -0
- package/docs/llms.txt +276 -0
- package/docs/migration-playbook.md +512 -0
- package/docs/quickstart.md +339 -0
- package/package.json +18 -2
- package/receipts.d.ts +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,124 @@ published package diverge.
|
|
|
10
10
|
This project adheres to [Semantic Versioning](https://semver.org/). Dates are
|
|
11
11
|
UTC.
|
|
12
12
|
|
|
13
|
+
## [0.8.1] — 2026-08-24
|
|
14
|
+
|
|
15
|
+
Docs — deploy guidance for the native `better-sqlite3` dependency. The SDK loads
|
|
16
|
+
a native SQLite buffer on import (the local telemetry row written before each run
|
|
17
|
+
returns), so the README Install section and `quickstart.md` now tell customers to
|
|
18
|
+
run on a Node runtime (not Edge/Workers), mark `modelrig` external when bundling
|
|
19
|
+
server code (Next.js `serverExternalPackages`, esbuild `--external:better-sqlite3`,
|
|
20
|
+
webpack `externals`), and set `MODELRIG_API_KEY` on every service/worker/cron;
|
|
21
|
+
plus a short monorepo workspace-source caveat. Docs/packaging only — no SDK
|
|
22
|
+
behaviour change.
|
|
23
|
+
|
|
24
|
+
## [0.8.0] — 2026-08-24
|
|
25
|
+
|
|
26
|
+
Packaging (GS-3) — the tarball now ships the customer-facing runs docs
|
|
27
|
+
(`docs/quickstart.md`, `docs/instrumentation.md`, `docs/how-it-fits.md`,
|
|
28
|
+
`docs/migration-playbook.md`, `docs/agent-skill/SKILL.md`, and the generated
|
|
29
|
+
`docs/llms.txt`) plus a self-contained **Runs — the standard path** section in
|
|
30
|
+
`README.md`, so `npm i modelrig` + read-the-package teaches run → step → artifact
|
|
31
|
+
offline; and every doc now states the `MODELRIG_ARTIFACTS` default consistently
|
|
32
|
+
(on by default once a control plane is configured; a local-only rig stays off;
|
|
33
|
+
`=0` opts out, `=1` forces on). Docs/packaging only — no SDK behaviour change.
|
|
34
|
+
|
|
35
|
+
Get-started v5 — runs are the standard path (GS-1). The front-door instructions
|
|
36
|
+
(www `/get-started`, the console setup prompt, `quickstart.md`, `how-it-fits.md`,
|
|
37
|
+
the agent `SKILL.md`) now teach **run → step → artifact** as the standard way to
|
|
38
|
+
instrument a pipeline, not an appendix; `instrumentation.md` stays the reference
|
|
39
|
+
they link to. SDK: inside a run context the SDK now also stamps **`tags.step`**
|
|
40
|
+
(the current step key: your explicit `stepKey`, else `tags.step`, else the auto
|
|
41
|
+
`route#n` / `provider/model#n` key) on every attempt row of both lanes when the
|
|
42
|
+
caller set none — the mirror of RC-1's `run_id` stamp; a caller value always wins,
|
|
43
|
+
and outside a run context rows are byte-identical. `modelrig status` prints
|
|
44
|
+
**`runs recorded: N (last: <ts>)`** from a best-effort readonly read of the local
|
|
45
|
+
buffer, and `modelrig validate` **warns when a rig has recorded calls but never
|
|
46
|
+
started a run** (a teaching hint — never a failure). ZERO DDL, no new deps.
|
|
47
|
+
|
|
48
|
+
Provenance-first assembly (screening B3) — additive, opt-in SDK seams for keeping
|
|
49
|
+
customer- and web-derived text out of the trusted system slot. **Default
|
|
50
|
+
behaviour is byte-identical**: a route with no `grounding.assembly` and no
|
|
51
|
+
`x-trust` takes the legacy path unchanged. A route may set
|
|
52
|
+
`grounding.assembly: spotlight` to route search results through the assembler as
|
|
53
|
+
an untrusted user block (search results are delivered as data, not instructions);
|
|
54
|
+
a template variable may be annotated `x-trust: untrusted` to move it out of the
|
|
55
|
+
system slot into that spotlighted block. Model-output citations are allowlisted to
|
|
56
|
+
the injected sources, with a `RunMeta.grounding.citations_dropped` count. `modelrig
|
|
57
|
+
validate` warns on legacy grounding and on an untrusted-shaped variable left
|
|
58
|
+
unannotated in the system slot. `EquivalenceReport.subject` gains
|
|
59
|
+
`"screen-assembly"`. `screen/provenance.ts` + `screen/types.ts` now ship in
|
|
60
|
+
`dist/`. ZERO DDL, no new deps.
|
|
61
|
+
|
|
62
|
+
Near-optimal pairwise + disagreement (N5/N6) — bake-offs gain a second read on
|
|
63
|
+
regret and a fixture-mining output, both additive and **byte-identical for
|
|
64
|
+
routes that don't opt in**. N5 scores each served arm's **pairwise standing vs
|
|
65
|
+
the reference arm** on the paired replay set — a `pairwise_win_rate`
|
|
66
|
+
(win=1/tie=0.5/loss=0) with a seeded-bootstrap CI and n — surfaced as
|
|
67
|
+
`VariantMetrics.pairwise` and a `pairwise-win` column in the report. It is
|
|
68
|
+
DETERMINISTIC-FIRST, exactly like the RB-1 regret tiers: when both an arm and the
|
|
69
|
+
reference carry a per-sample value-accuracy score the verdict is free (the sign
|
|
70
|
+
of their difference); only the samples nothing scored fall to an OPT-IN, CAPPED,
|
|
71
|
+
METERED pairwise judge, declared `bakeoff.pairwise.judge` (judge model ∈ declared
|
|
72
|
+
candidates, #223 F3; every call a metered inference tagged `bakeoff_pairwise`,
|
|
73
|
+
capped per arm). A new `objective.pairwiseAware` (default off) adds one
|
|
74
|
+
eligibility clause to the `quality-first`/`balanced` gate — a proposed arm must
|
|
75
|
+
not be pairwise-worse than the reference within CI — and never touches the
|
|
76
|
+
cost-first gate. N6 mines the same replay for **eval-case candidates**: ranked
|
|
77
|
+
POINTERS (by `captureId`) to the samples where the bake-off's own signals
|
|
78
|
+
disagree (conformance vs value score, the Q1 gate vs conformance, served arm vs
|
|
79
|
+
reference, a pairwise upset), surfaced on `BakeoffArtifact.disagreement` and a
|
|
80
|
+
report summary line. Candidates are **metadata only** (captureId + numbers /
|
|
81
|
+
booleans, asserted meta-safe — never input or output text) and are **never
|
|
82
|
+
auto-promoted**; the full pre-cap `found` count rides the truncated list so a
|
|
83
|
+
capped surface is never mistaken for all of them. No new root exports — additive
|
|
84
|
+
route YAML plus report/artifact fields. ZERO DDL, no new deps. See
|
|
85
|
+
`docs/bakeoffs.md`.
|
|
86
|
+
|
|
87
|
+
Published receipts (RP-1 / RP-1c) — a signed, login-free **Receipt@v1** an org
|
|
88
|
+
can publish from its own measured rows. **This is two halves; only the shape is
|
|
89
|
+
in the npm package.** The `modelrig` package exposes the receipt CONTRACT via the
|
|
90
|
+
new `modelrig/receipts` sub-path export ONLY (never re-exported from `index.ts`):
|
|
91
|
+
the `ReceiptV1` shape, `canonicalizeReceipt` / `hashReceipt` (JCS-style
|
|
92
|
+
canonical body, self-describing `sha256:` hash), and the pure `isReceiptSafe`
|
|
93
|
+
classification gate — `measured` carries numbers / booleans / hashes /
|
|
94
|
+
identifiers only, no content, no tenant ids (fail-closed). The additive
|
|
95
|
+
`exports` map (`.`, `./receipts`, `./data/*`, `./registry/*`) plus a
|
|
96
|
+
`receipts.d.ts` shim keep the historical bare surface and the runtime data /
|
|
97
|
+
registry files reachable now that a sub-path map turns resolution into an
|
|
98
|
+
allowlist; `dist/receipts/**` now ships. **Everything else lives in
|
|
99
|
+
`modelrig-server` (NOT the npm tag):** Ed25519 signing with a `key_id` (node
|
|
100
|
+
crypto, no new dep), authed publish / revoke, the store-only public family —
|
|
101
|
+
`GET /v1/r/:id`, `…/badge.svg`, `GET /v1/verify/:id` (re-canonicalize → re-hash →
|
|
102
|
+
verify signature), `GET /.well-known/modelrig-receipts.json` (key rotation) — a
|
|
103
|
+
handler constructed with NO database client, an org alias that never leaks
|
|
104
|
+
`org_id`, `recipient` visibility (a login-free capability URL never listed
|
|
105
|
+
publicly), and a revoke tombstone that answers 410. RP-1c adds the `run` and
|
|
106
|
+
`fact-sheet` producers + source readers and the `source_ref` lookup (X9 — the
|
|
107
|
+
PR-body "verify this" link). **Honest limit:** publish / revoke / signing degrade
|
|
108
|
+
to **501** until `MODELRIG_RECEIPT_SIGNING_KEY` + `MODELRIG_RECEIPT_KEY_ID` and
|
|
109
|
+
the content store are provisioned (a half-configured signer is refused, not
|
|
110
|
+
half-enabled); the public read / verify surface answers 404 until an object is
|
|
111
|
+
published. ZERO DDL, no new deps. See `docs/receipts.md`.
|
|
112
|
+
|
|
113
|
+
Tenant ledger (TB-1) — lazy per-customer identity and derived statements for a
|
|
114
|
+
platform that resells routed inference. **Server / console / docs only — the
|
|
115
|
+
`modelrig` npm surface is unchanged by TB-1.** TB-1a adds tenant identity as a
|
|
116
|
+
single column: migration `00059` puts a `kind` discriminator
|
|
117
|
+
(`'project'` | `'tenant'`) on `projects`, with a reserved per-org `__tenants__`
|
|
118
|
+
parent and tenant leaves slugged `t_<sha256(org_id:subject)[:24]>`, materialized
|
|
119
|
+
service-role at ingest from the principal's org (never the payload) — **zero new
|
|
120
|
+
tables**. TB-1b derives a per-tenant statement (`TenantStatementV1`) on read,
|
|
121
|
+
**never stored**: counts / cents / hashes only, where `verified` is DETERMINISTIC
|
|
122
|
+
conformance only (run-outcome@v1 / schema-conformant artifact; model judge and
|
|
123
|
+
human feedback are `quality_context` and never gate billable), unpriced models
|
|
124
|
+
fall to a separate "priced at list" line rather than into the total, and a new
|
|
125
|
+
Tenants console renders it. Rate cards and real price / margin are **TB-3, NOT in
|
|
126
|
+
this tag** — every TB-1 statement is "priced at list" with `billable = verified`
|
|
127
|
+
and null price / margin. The migration ships in the repo but is applied by a
|
|
128
|
+
Supabase-enabled session, not this build. ZERO DDL added by this release; no new
|
|
129
|
+
deps. See `docs/tenants.md`.
|
|
130
|
+
|
|
13
131
|
## [0.7.0] — 2026-08-23
|
|
14
132
|
|
|
15
133
|
Released with the docs/registry mirror in one R-A ceremony. Headline: wrap a
|
package/README.md
CHANGED
|
@@ -22,6 +22,29 @@ npm i modelrig
|
|
|
22
22
|
> macOS and Linux on x64/arm64 (nearly everyone); an unusual platform will
|
|
23
23
|
> compile it at install time and needs a C++ toolchain. Node ≥ 20 is assumed.
|
|
24
24
|
|
|
25
|
+
`modelrig` is a normal dependency: your existing `npm install` (or `pnpm` /
|
|
26
|
+
`yarn`) step installs it **prebuilt** — there is no separate "build modelrig"
|
|
27
|
+
step. Three things to get right when you deploy:
|
|
28
|
+
|
|
29
|
+
- **Run it on a Node runtime, not an Edge/Workers runtime.** `better-sqlite3`
|
|
30
|
+
is a native module and loads on import, so an Edge runtime can't run it. A
|
|
31
|
+
standard Node host — server, container, or a Node serverless function — works
|
|
32
|
+
after `npm install`.
|
|
33
|
+
- **If your deploy _bundles_ server code, mark modelrig external** so the native
|
|
34
|
+
binary is loaded at runtime instead of bundled:
|
|
35
|
+
- **Next.js** — `serverExternalPackages: ['modelrig']` in `next.config.js`
|
|
36
|
+
(Next 13/14: `experimental.serverComponentsExternalPackages`)
|
|
37
|
+
- **esbuild** — `--external:better-sqlite3` (and `--external:modelrig`)
|
|
38
|
+
- **webpack** — add `better-sqlite3` to `externals`
|
|
39
|
+
- **Set `MODELRIG_API_KEY` on every service, worker, and cron** that runs a rig
|
|
40
|
+
— each process reports its own telemetry, so the key belongs on all of them,
|
|
41
|
+
not just your web service.
|
|
42
|
+
|
|
43
|
+
(In a monorepo that vendors ModelRig as a workspace package — the SDK source
|
|
44
|
+
rather than the published tarball — build that package before the app that
|
|
45
|
+
imports it, so its `dist/` types resolve. Consuming the published package needs
|
|
46
|
+
none of this.)
|
|
47
|
+
|
|
25
48
|
## Quickstart
|
|
26
49
|
|
|
27
50
|
This runs with **only a provider key** — no account, no signup.
|
|
@@ -229,6 +252,69 @@ lines up with the console's built-ins:
|
|
|
229
252
|
`customer`, `tenant`, `user_id`, `team`, … is nudged toward the reserved key it
|
|
230
253
|
resembles. They stay ordinary tags; nothing is required.
|
|
231
254
|
|
|
255
|
+
## Runs — the standard path
|
|
256
|
+
|
|
257
|
+
Every pipeline execution is a **run** — the standard way ModelRig records what
|
|
258
|
+
your workstream did, not an appendix. Wrap the execution in a run context, name
|
|
259
|
+
one **step** per model-call family, and **save the work products** as artifacts;
|
|
260
|
+
the console's **Runs** tab then shows the run → step → artifact chain you click
|
|
261
|
+
through, and `/projects` groups it by your tags.
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import { createRig, loadConfigFromEnv } from "modelrig";
|
|
265
|
+
|
|
266
|
+
const rig = createRig(loadConfigFromEnv());
|
|
267
|
+
|
|
268
|
+
// One run per pipeline execution. run.scope isolates concurrent runs; use
|
|
269
|
+
// run.start()/run.end() for one run at a time on a request path.
|
|
270
|
+
await rig.artifacts.run.scope(
|
|
271
|
+
{ pipeline: "support", episodeKey: ticketId },
|
|
272
|
+
async () => {
|
|
273
|
+
const result = await rig.run("example.support_summarize", {
|
|
274
|
+
input: { ticket, product, priorContext },
|
|
275
|
+
tags: { subject: customer, feature: "summarize" }, // run_id auto-stamped
|
|
276
|
+
stepKey: "summarize", // names this step on /runs
|
|
277
|
+
});
|
|
278
|
+
// Save the work product — prompt / raw / parsed all save the same way.
|
|
279
|
+
rig.artifacts.artifact.save(result.output, { name: "summary", type: "step_output" });
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
rig.close();
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
- **Tags:** carry `subject` (what the run is *about* — an opaque id) and
|
|
286
|
+
`feature`. Inside a run context the SDK stamps `tags.run_id` (the run's
|
|
287
|
+
`episodeKey`, else its id) and `tags.step` (the current step key) on every
|
|
288
|
+
attempt row of both lanes — a value you pass always wins, and outside a run
|
|
289
|
+
context nothing is stamped, so the row is byte-identical.
|
|
290
|
+
- **Raw lane too:** a `rig.runRaw` call inside a run context records a
|
|
291
|
+
ground-truth step (since 0.5.0), so grounded/cached BYOK pipelines fill the
|
|
292
|
+
same graph.
|
|
293
|
+
- **Acceptance:** the run appears on `/runs` with its steps in order and the
|
|
294
|
+
artifacts you saved attached. `modelrig status` prints `runs recorded: N` as
|
|
295
|
+
the local mirror; `modelrig validate` warns if a rig records calls but never
|
|
296
|
+
starts a run.
|
|
297
|
+
|
|
298
|
+
**On by default under a control plane.** The run/artifact namespace is **on by
|
|
299
|
+
default once a control plane is configured** — set `MODELRIG_API_KEY=rig_sk_…`
|
|
300
|
+
(or the self-host Supabase pair) and runs record automatically, metadata and
|
|
301
|
+
hashes only. A **pure local-only rig** (no sink to ship to) stays off; force it
|
|
302
|
+
on there with `MODELRIG_ARTIFACTS=1`, or opt out anywhere with
|
|
303
|
+
`MODELRIG_ARTIFACTS=0`. With the namespace off it is inert — `run.start` is a
|
|
304
|
+
no-op, `artifact.save` returns null, zero rows are written, and your pipeline
|
|
305
|
+
behaves byte-for-byte as before. `modelrig status` prints the current posture.
|
|
306
|
+
|
|
307
|
+
> **Metadata and hashes only, this release.** Content custody (holding the
|
|
308
|
+
> serialized bytes) is early access; today the value you save is hashed (sha256)
|
|
309
|
+
> and the bytes are discarded — the metadata row and its integrity hash are what
|
|
310
|
+
> persist. A `zeroRetention` run refuses every artifact fail-closed at the SDK
|
|
311
|
+
> gate.
|
|
312
|
+
|
|
313
|
+
The full artifact API — lineage (`link`), evaluations (`evaluate`), the reusable
|
|
314
|
+
per-step seam, and `run.scope` vs `run.start` — is the
|
|
315
|
+
[instrumentation guide](./docs/instrumentation.md), shipped in this package so
|
|
316
|
+
you can read it offline.
|
|
317
|
+
|
|
232
318
|
## CLI
|
|
233
319
|
|
|
234
320
|
The package ships a `modelrig` binary:
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* N6 — disagreement mining into eval-case candidates (near-optimal brief §3.2 N6).
|
|
3
|
+
*
|
|
4
|
+
* The discriminating-fixtures gap (brief §2): a route's fixtures separate *valid*
|
|
5
|
+
* from *invalid* but rarely *good* from *great*. The richest place to look for the
|
|
6
|
+
* fixtures that WOULD discriminate is where the bake-off's own signals DISAGREE on
|
|
7
|
+
* a replayed sample:
|
|
8
|
+
* - deterministic conformance says one thing, the value score another
|
|
9
|
+
* (schema-valid-but-wrong, or invalid-but-scored-right);
|
|
10
|
+
* - the Q1 quality gate's verdict contradicts conformance;
|
|
11
|
+
* - the SERVED arm and the REFERENCE arm diverge on the same capture (the
|
|
12
|
+
* top-of-distribution signal the brief names);
|
|
13
|
+
* - the pairwise judge upsets the expected order (arm beats / loses to the ref).
|
|
14
|
+
*
|
|
15
|
+
* Each such sample becomes an eval-case CANDIDATE — a ranked POINTER (captureId)
|
|
16
|
+
* an operator may promote through the existing E4 path. NEVER auto-promoted.
|
|
17
|
+
*
|
|
18
|
+
* CONTENT CUSTODY (P6 / spec): a candidate carries ONLY already-captured
|
|
19
|
+
* metadata — the captureId (an identifier), booleans, and numeric scores. No
|
|
20
|
+
* output text, no input text. Every string field is asserted meta-safe
|
|
21
|
+
* (`isMetaSafeString`) before it is surfaced, the same predicate the WI-1
|
|
22
|
+
* envelope uses. This module is PURE and fixture-tested.
|
|
23
|
+
*/
|
|
24
|
+
import type { ReplaySampleResult } from "./replay";
|
|
25
|
+
import type { PairwiseOutcome } from "./pairwise";
|
|
26
|
+
export type DisagreementReason = "conform_vs_value" | "conform_vs_gate" | "arm_vs_reference" | "pairwise_upset";
|
|
27
|
+
/** A per-sample verdict divergence, surfaced as a ranked eval-case candidate. All
|
|
28
|
+
* fields are already-captured metadata — a POINTER to promote, never content. */
|
|
29
|
+
export interface DisagreementCandidate {
|
|
30
|
+
/** The captured input's inference id — the promote path rehydrates from it. */
|
|
31
|
+
readonly captureId: string;
|
|
32
|
+
/** Which served arm this divergence is on (never the reference arm). */
|
|
33
|
+
readonly arm: string;
|
|
34
|
+
/** 0..1 magnitude of the disagreement — the ranking key (desc). */
|
|
35
|
+
readonly score: number;
|
|
36
|
+
/** Every signal pair that fired (ranked candidates usually have one; the
|
|
37
|
+
* top ones several). */
|
|
38
|
+
readonly reasons: readonly DisagreementReason[];
|
|
39
|
+
/** The already-captured numbers/booleans behind the score (no content). */
|
|
40
|
+
readonly signals: {
|
|
41
|
+
readonly armConform: boolean;
|
|
42
|
+
readonly armValueAccuracy: number | null;
|
|
43
|
+
readonly armQualityPassed: boolean | null;
|
|
44
|
+
readonly refConform?: boolean;
|
|
45
|
+
readonly refValueAccuracy?: number | null;
|
|
46
|
+
readonly pairwise?: PairwiseOutcome;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export interface DisagreementReport {
|
|
50
|
+
/** Paired/served samples examined across all arms. */
|
|
51
|
+
readonly examined: number;
|
|
52
|
+
/** Ranked candidates (score desc, then captureId asc), truncated to the cap. */
|
|
53
|
+
readonly candidates: readonly DisagreementCandidate[];
|
|
54
|
+
/** How many candidates existed BEFORE the cap — surfaced so a truncated list is
|
|
55
|
+
* never mistaken for "all of them" (no silent caps). */
|
|
56
|
+
readonly found: number;
|
|
57
|
+
/** The cap applied when `found > candidates.length`, else null. */
|
|
58
|
+
readonly cappedAt: number | null;
|
|
59
|
+
/** Count of candidates each reason contributed to (a candidate with two
|
|
60
|
+
* reasons counts once under each). */
|
|
61
|
+
readonly counts: Readonly<Record<DisagreementReason, number>>;
|
|
62
|
+
}
|
|
63
|
+
/** Default cap on the surfaced candidate list — enough to act on, bounded so a
|
|
64
|
+
* noisy replay set does not flood the surface. The full `found` count rides
|
|
65
|
+
* alongside so the truncation is legible. */
|
|
66
|
+
export declare const DEFAULT_DISAGREEMENT_CAP = 50;
|
|
67
|
+
export interface MineInput {
|
|
68
|
+
/** One entry per SERVED arm (never the reference): its per-sample results. */
|
|
69
|
+
readonly arms: readonly {
|
|
70
|
+
readonly arm: string;
|
|
71
|
+
readonly samples: readonly ReplaySampleResult[];
|
|
72
|
+
}[];
|
|
73
|
+
/** The reference arm's per-sample results, keyed for the arm-vs-reference and
|
|
74
|
+
* pairwise signals. Absent ⇒ those two reasons never fire (only the
|
|
75
|
+
* within-arm signals do). */
|
|
76
|
+
readonly reference?: readonly ReplaySampleResult[];
|
|
77
|
+
/** Pairwise verdicts keyed `${arm}\0${captureId}` → outcome, when a pairwise
|
|
78
|
+
* signal was computed. */
|
|
79
|
+
readonly pairwise?: ReadonlyMap<string, PairwiseOutcome>;
|
|
80
|
+
/** Surfaced-list cap (default {@link DEFAULT_DISAGREEMENT_CAP}). */
|
|
81
|
+
readonly maxCandidates?: number;
|
|
82
|
+
}
|
|
83
|
+
/** The pairwise map key — arm + captureId, NUL-joined so neither can spoof the
|
|
84
|
+
* other's boundary. */
|
|
85
|
+
export declare function pairwiseKey(arm: string, captureId: string): string;
|
|
86
|
+
/**
|
|
87
|
+
* Mine a replayed bake-off's per-sample results for verdict disagreements and
|
|
88
|
+
* rank them as eval-case candidates (N6). Pure over its inputs; deterministic
|
|
89
|
+
* ordering (score desc, then captureId asc). A produced sample with NO
|
|
90
|
+
* divergence past {@link SIGNAL_EPS} yields no candidate.
|
|
91
|
+
*/
|
|
92
|
+
export declare function mineDisagreements(input: MineInput): DisagreementReport;
|
|
93
|
+
//# sourceMappingURL=disagreement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disagreement.d.ts","sourceRoot":"","sources":["../../src/bakeoff/disagreement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,MAAM,kBAAkB,GAC1B,kBAAkB,GAClB,iBAAiB,GACjB,kBAAkB,GAClB,gBAAgB,CAAC;AAErB;iFACiF;AACjF,MAAM,WAAW,qBAAqB;IACpC,+EAA+E;IAC/E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,wEAAwE;IACxE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;4BACwB;IACxB,QAAQ,CAAC,OAAO,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAChD,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE;QAChB,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;QAC7B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;QACzC,QAAQ,CAAC,gBAAgB,EAAE,OAAO,GAAG,IAAI,CAAC;QAC1C,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;QAC9B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1C,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;KACrC,CAAC;CACH;AAED,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,gFAAgF;IAChF,QAAQ,CAAC,UAAU,EAAE,SAAS,qBAAqB,EAAE,CAAC;IACtD;4DACwD;IACxD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC;0CACsC;IACtC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC,CAAC;CAC/D;AAED;;6CAE6C;AAC7C,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAY3C,MAAM,WAAW,SAAS;IACxB,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,SAAS;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,SAAS,kBAAkB,EAAE,CAAA;KAAE,EAAE,CAAC;IACpG;;iCAE6B;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACnD;8BAC0B;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACzD,oEAAoE;IACpE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;uBACuB;AACvB,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAG,kBAAkB,CA0FtE"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* N6 — disagreement mining into eval-case candidates (near-optimal brief §3.2 N6).
|
|
4
|
+
*
|
|
5
|
+
* The discriminating-fixtures gap (brief §2): a route's fixtures separate *valid*
|
|
6
|
+
* from *invalid* but rarely *good* from *great*. The richest place to look for the
|
|
7
|
+
* fixtures that WOULD discriminate is where the bake-off's own signals DISAGREE on
|
|
8
|
+
* a replayed sample:
|
|
9
|
+
* - deterministic conformance says one thing, the value score another
|
|
10
|
+
* (schema-valid-but-wrong, or invalid-but-scored-right);
|
|
11
|
+
* - the Q1 quality gate's verdict contradicts conformance;
|
|
12
|
+
* - the SERVED arm and the REFERENCE arm diverge on the same capture (the
|
|
13
|
+
* top-of-distribution signal the brief names);
|
|
14
|
+
* - the pairwise judge upsets the expected order (arm beats / loses to the ref).
|
|
15
|
+
*
|
|
16
|
+
* Each such sample becomes an eval-case CANDIDATE — a ranked POINTER (captureId)
|
|
17
|
+
* an operator may promote through the existing E4 path. NEVER auto-promoted.
|
|
18
|
+
*
|
|
19
|
+
* CONTENT CUSTODY (P6 / spec): a candidate carries ONLY already-captured
|
|
20
|
+
* metadata — the captureId (an identifier), booleans, and numeric scores. No
|
|
21
|
+
* output text, no input text. Every string field is asserted meta-safe
|
|
22
|
+
* (`isMetaSafeString`) before it is surfaced, the same predicate the WI-1
|
|
23
|
+
* envelope uses. This module is PURE and fixture-tested.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.DEFAULT_DISAGREEMENT_CAP = void 0;
|
|
27
|
+
exports.pairwiseKey = pairwiseKey;
|
|
28
|
+
exports.mineDisagreements = mineDisagreements;
|
|
29
|
+
const attempt_meta_1 = require("../telemetry/attempt-meta");
|
|
30
|
+
/** Default cap on the surfaced candidate list — enough to act on, bounded so a
|
|
31
|
+
* noisy replay set does not flood the surface. The full `found` count rides
|
|
32
|
+
* alongside so the truncation is legible. */
|
|
33
|
+
exports.DEFAULT_DISAGREEMENT_CAP = 50;
|
|
34
|
+
/** Per-sample quality on the SAME 0..1 scale regret uses: the value-accuracy
|
|
35
|
+
* score when present, else 1/0 conformance. */
|
|
36
|
+
function quality01(s) {
|
|
37
|
+
return s.valueAccuracy ?? (s.conform ? 1 : 0);
|
|
38
|
+
}
|
|
39
|
+
/** A signal is "meaningfully divergent" past this magnitude (below it is noise —
|
|
40
|
+
* e.g. a 0.98-vs-1.0 field-accuracy hair-split is not a fixture worth promoting). */
|
|
41
|
+
const SIGNAL_EPS = 0.05;
|
|
42
|
+
/** The pairwise map key — arm + captureId, NUL-joined so neither can spoof the
|
|
43
|
+
* other's boundary. */
|
|
44
|
+
function pairwiseKey(arm, captureId) {
|
|
45
|
+
return `${arm}\0${captureId}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Mine a replayed bake-off's per-sample results for verdict disagreements and
|
|
49
|
+
* rank them as eval-case candidates (N6). Pure over its inputs; deterministic
|
|
50
|
+
* ordering (score desc, then captureId asc). A produced sample with NO
|
|
51
|
+
* divergence past {@link SIGNAL_EPS} yields no candidate.
|
|
52
|
+
*/
|
|
53
|
+
function mineDisagreements(input) {
|
|
54
|
+
const cap = input.maxCandidates ?? exports.DEFAULT_DISAGREEMENT_CAP;
|
|
55
|
+
const refByCapture = new Map((input.reference ?? []).map((s) => [s.captureId, s]));
|
|
56
|
+
const counts = {
|
|
57
|
+
conform_vs_value: 0,
|
|
58
|
+
conform_vs_gate: 0,
|
|
59
|
+
arm_vs_reference: 0,
|
|
60
|
+
pairwise_upset: 0,
|
|
61
|
+
};
|
|
62
|
+
const candidates = [];
|
|
63
|
+
let examined = 0;
|
|
64
|
+
for (const { arm, samples } of input.arms) {
|
|
65
|
+
for (const s of samples) {
|
|
66
|
+
// Only PRODUCED samples carry verdicts to disagree about; a failed sample
|
|
67
|
+
// (no output) has nothing to mine. Guard the captureId is meta-safe before
|
|
68
|
+
// it is surfaced (captureIds are identifiers, always safe — asserted, never
|
|
69
|
+
// assumed, per the content-custody rule).
|
|
70
|
+
if (s.failureClass !== null)
|
|
71
|
+
continue;
|
|
72
|
+
if (!(0, attempt_meta_1.isMetaSafeString)(s.captureId))
|
|
73
|
+
continue;
|
|
74
|
+
examined += 1;
|
|
75
|
+
const reasons = [];
|
|
76
|
+
let score = 0;
|
|
77
|
+
const conform01 = s.conform ? 1 : 0;
|
|
78
|
+
// conform_vs_value: schema conformance disagrees with the value score.
|
|
79
|
+
if (s.valueAccuracy !== null) {
|
|
80
|
+
const gap = Math.abs(conform01 - s.valueAccuracy);
|
|
81
|
+
if (gap > SIGNAL_EPS) {
|
|
82
|
+
reasons.push("conform_vs_value");
|
|
83
|
+
score = Math.max(score, gap);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// conform_vs_gate: the Q1 boolean verdict contradicts conformance.
|
|
87
|
+
if (s.qualityPassed !== null && s.qualityPassed !== s.conform) {
|
|
88
|
+
reasons.push("conform_vs_gate");
|
|
89
|
+
score = Math.max(score, 1);
|
|
90
|
+
}
|
|
91
|
+
const ref = refByCapture.get(s.captureId);
|
|
92
|
+
const signals = {
|
|
93
|
+
armConform: s.conform,
|
|
94
|
+
armValueAccuracy: s.valueAccuracy,
|
|
95
|
+
armQualityPassed: s.qualityPassed,
|
|
96
|
+
...(ref !== undefined ? { refConform: ref.conform, refValueAccuracy: ref.valueAccuracy } : {}),
|
|
97
|
+
};
|
|
98
|
+
// arm_vs_reference: the served arm and the reference diverge on this capture
|
|
99
|
+
// (the top-of-distribution signal — where the fixture corpus learns to tell
|
|
100
|
+
// good from great).
|
|
101
|
+
if (ref !== undefined) {
|
|
102
|
+
const gap = Math.abs(quality01(s) - quality01(ref));
|
|
103
|
+
if (gap > SIGNAL_EPS) {
|
|
104
|
+
reasons.push("arm_vs_reference");
|
|
105
|
+
score = Math.max(score, gap);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// pairwise_upset: a non-tie pairwise verdict is itself a divergence worth a
|
|
109
|
+
// fixture (an arm beating OR losing to the reference by preference).
|
|
110
|
+
const pw = input.pairwise?.get(pairwiseKey(arm, s.captureId));
|
|
111
|
+
if (pw !== undefined && pw !== "tie") {
|
|
112
|
+
reasons.push("pairwise_upset");
|
|
113
|
+
score = Math.max(score, 1);
|
|
114
|
+
}
|
|
115
|
+
if (reasons.length === 0)
|
|
116
|
+
continue;
|
|
117
|
+
for (const r of reasons)
|
|
118
|
+
counts[r] += 1;
|
|
119
|
+
candidates.push({
|
|
120
|
+
captureId: s.captureId,
|
|
121
|
+
arm,
|
|
122
|
+
score,
|
|
123
|
+
reasons,
|
|
124
|
+
signals: pw !== undefined ? { ...signals, pairwise: pw } : signals,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Deterministic ranking: strongest disagreement first, captureId as a stable
|
|
129
|
+
// tiebreak so the same replay set always ranks identically.
|
|
130
|
+
candidates.sort((a, b) => b.score - a.score || a.captureId.localeCompare(b.captureId));
|
|
131
|
+
const found = candidates.length;
|
|
132
|
+
const surfaced = candidates.slice(0, cap);
|
|
133
|
+
return {
|
|
134
|
+
examined,
|
|
135
|
+
candidates: surfaced,
|
|
136
|
+
found,
|
|
137
|
+
cappedAt: found > surfaced.length ? cap : null,
|
|
138
|
+
counts,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/bakeoff/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAgB,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/bakeoff/engine.ts"],"names":[],"mappings":"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"}
|
package/dist/bakeoff/engine.js
CHANGED
|
@@ -22,6 +22,8 @@ const loader_1 = require("../routes/loader");
|
|
|
22
22
|
const replay_1 = require("./replay");
|
|
23
23
|
const stats_1 = require("./stats");
|
|
24
24
|
const judge_1 = require("./judge");
|
|
25
|
+
const pairwise_1 = require("./pairwise");
|
|
26
|
+
const disagreement_1 = require("./disagreement");
|
|
25
27
|
const reference_1 = require("./reference");
|
|
26
28
|
const samples_1 = require("./samples");
|
|
27
29
|
/** The arm name carrying the reference (ceiling) metrics. Reserved — a user
|
|
@@ -75,6 +77,10 @@ async function runBakeoff(deps, captures, req, samplesSink) {
|
|
|
75
77
|
const keepOutputs = req.keepOutputs !== undefined && samplesSink !== undefined;
|
|
76
78
|
const bakeoffId = (0, node_crypto_1.randomUUID)();
|
|
77
79
|
const judgeCfg = route.bakeoff?.judge;
|
|
80
|
+
// N5: a declared pairwise judge needs each sample's output retained (arm-vs-
|
|
81
|
+
// reference comparison in the engine) — WITHOUT the keepOutputs sink persist.
|
|
82
|
+
const pairwiseJudgeCfg = route.bakeoff?.pairwise?.judge;
|
|
83
|
+
const retainForPairwise = pairwiseJudgeCfg !== undefined;
|
|
78
84
|
// Replay every requested arm. Raw samples are kept ALWAYS (paired regret is
|
|
79
85
|
// computed from per-sample quality); the keepOutputs sink is separate. A fresh
|
|
80
86
|
// judge scorer per arm keeps `max_samples` per-arm (spec §4.3).
|
|
@@ -88,6 +94,7 @@ async function runBakeoff(deps, captures, req, samplesSink) {
|
|
|
88
94
|
lastN: req.replayLastN,
|
|
89
95
|
envelopeUsd: req.envelopeUsdPerVariant,
|
|
90
96
|
...(keepOutputs ? { keepOutputs: true } : {}),
|
|
97
|
+
...(retainForPairwise ? { retainOutputForPairwise: true } : {}),
|
|
91
98
|
...(req.expected !== undefined ? { expected: req.expected } : {}),
|
|
92
99
|
...(judgeCfg !== undefined ? { judge: (0, judge_1.makeJudgeScorer)(deps, route, judgeCfg) } : {}),
|
|
93
100
|
...(req.qualityGate !== undefined ? { qualityGate: req.qualityGate } : {}),
|
|
@@ -101,6 +108,10 @@ async function runBakeoff(deps, captures, req, samplesSink) {
|
|
|
101
108
|
// arm's paired regret vs it. Partial-result honesty is preserved.
|
|
102
109
|
const referenceInfo = resolveReference(route, req);
|
|
103
110
|
let reference = null;
|
|
111
|
+
// N6: pairwise verdicts keyed `${arm} ${captureId}` (a mining input) + the
|
|
112
|
+
// reference samples, hoisted so disagreement mining sees them after the block.
|
|
113
|
+
const pairwiseOutcomes = new Map();
|
|
114
|
+
let refSamplesForMining = [];
|
|
104
115
|
if (referenceInfo !== null) {
|
|
105
116
|
const { deps: refDeps } = withReferenceArm(deps, route, referenceInfo.candidate);
|
|
106
117
|
const refEnvelope = route.bakeoff?.referenceEnvelopeUsd ?? req.envelopeUsdPerVariant;
|
|
@@ -109,16 +120,26 @@ async function runBakeoff(deps, captures, req, samplesSink) {
|
|
|
109
120
|
variant: exports.REFERENCE_ARM,
|
|
110
121
|
lastN: req.replayLastN,
|
|
111
122
|
envelopeUsd: refEnvelope,
|
|
123
|
+
...(retainForPairwise ? { retainOutputForPairwise: true } : {}),
|
|
112
124
|
...(req.expected !== undefined ? { expected: req.expected } : {}),
|
|
113
125
|
...(judgeCfg !== undefined ? { judge: (0, judge_1.makeJudgeScorer)(deps, route, judgeCfg) } : {}),
|
|
114
126
|
...(req.qualityGate !== undefined ? { qualityGate: req.qualityGate } : {}),
|
|
115
127
|
});
|
|
116
128
|
const partial = refSamples.length < replayN || refSamples.some((s) => s.failureClass === "budget_exhausted");
|
|
129
|
+
refSamplesForMining = refSamples;
|
|
117
130
|
for (let i = 0; i < metrics.length; i += 1) {
|
|
118
131
|
const raw = rawByArm.get(metrics[i].variant) ?? [];
|
|
132
|
+
// N5: pairwise standing vs the reference — deterministic where both sides
|
|
133
|
+
// carry value accuracy, else the opt-in capped judge (fresh per arm, so
|
|
134
|
+
// maxSamples is per-arm). A REPORTED signal beside regret.
|
|
135
|
+
const pwJudge = pairwiseJudgeCfg !== undefined ? (0, pairwise_1.makePairwiseJudge)(deps, route, pairwiseJudgeCfg) : null;
|
|
136
|
+
const { stats: pairwise, outcomes } = await (0, pairwise_1.scoreArmPairwise)(raw, refSamples, pwJudge);
|
|
137
|
+
for (const [captureId, outcome] of outcomes)
|
|
138
|
+
pairwiseOutcomes.set((0, disagreement_1.pairwiseKey)(metrics[i].variant, captureId), outcome);
|
|
119
139
|
metrics[i] = {
|
|
120
140
|
...metrics[i],
|
|
121
141
|
regret: (0, stats_1.pairedRegret)(raw, refSamples),
|
|
142
|
+
...(pairwise !== null ? { pairwise } : {}),
|
|
122
143
|
...(partial ? { referencePartial: true } : {}),
|
|
123
144
|
};
|
|
124
145
|
}
|
|
@@ -181,6 +202,15 @@ async function runBakeoff(deps, captures, req, samplesSink) {
|
|
|
181
202
|
"re-run with --keep-outputs to retry the samples.");
|
|
182
203
|
}
|
|
183
204
|
}
|
|
205
|
+
// N6: mine the replay/verdict set for disagreements → ranked eval-case
|
|
206
|
+
// candidates (never auto-promoted). Pure over the per-arm raw samples + the
|
|
207
|
+
// reference + pairwise verdicts. Rides the LOCAL artifact only — captureIds are
|
|
208
|
+
// pointers to already-captured inputs and are never mirrored.
|
|
209
|
+
const disagreement = (0, disagreement_1.mineDisagreements)({
|
|
210
|
+
arms: req.variants.map((variant) => ({ arm: variant, samples: rawByArm.get(variant) ?? [] })),
|
|
211
|
+
...(refSamplesForMining.length > 0 ? { reference: refSamplesForMining } : {}),
|
|
212
|
+
...(pairwiseOutcomes.size > 0 ? { pairwise: pairwiseOutcomes } : {}),
|
|
213
|
+
});
|
|
184
214
|
return {
|
|
185
215
|
id: bakeoffId,
|
|
186
216
|
route: route.route,
|
|
@@ -192,6 +222,7 @@ async function runBakeoff(deps, captures, req, samplesSink) {
|
|
|
192
222
|
objective,
|
|
193
223
|
reference,
|
|
194
224
|
qualityMetric,
|
|
225
|
+
disagreement,
|
|
195
226
|
};
|
|
196
227
|
}
|
|
197
228
|
function isDeclared(route, candidate) {
|
|
@@ -224,6 +255,7 @@ function resolveObjective(route, req) {
|
|
|
224
255
|
maxRegret: route.objective?.maxRegret ?? null,
|
|
225
256
|
priceCeiling: route.objective?.priceCeiling ?? null,
|
|
226
257
|
lambda: route.objective?.lambda ?? null,
|
|
258
|
+
...(route.objective?.pairwiseAware === true ? { pairwiseAware: true } : {}),
|
|
227
259
|
};
|
|
228
260
|
}
|
|
229
261
|
const EPS = 1e-9;
|
|
@@ -233,6 +265,16 @@ function savingsPct(incumbent, winner) {
|
|
|
233
265
|
? 0
|
|
234
266
|
: ((inc - winner.effectiveUsdPer1kConformant) / inc) * 100;
|
|
235
267
|
}
|
|
268
|
+
/** N5 pairwise-aware gate clause (default off). When `objective.pairwiseAware` is
|
|
269
|
+
* set, an eligible arm must NOT be pairwise-worse than the reference within CI —
|
|
270
|
+
* its pairwise win-rate lower bound ≥ 0.5. Fail-closed: an arm with NO pairwise
|
|
271
|
+
* stats cannot be confirmed not-worse, so it is ineligible under the knob. When
|
|
272
|
+
* the knob is off this returns true for every arm ⇒ RB-1 behaviour byte-identical. */
|
|
273
|
+
function pairwiseEligible(m, objective) {
|
|
274
|
+
if (objective.pairwiseAware !== true)
|
|
275
|
+
return true;
|
|
276
|
+
return m.pairwise != null && m.pairwise.winRateCi95[0] >= 0.5 - EPS;
|
|
277
|
+
}
|
|
236
278
|
/** A price ceiling test: usd_per_1k on the effective-$/1k axis, usd_per_run on
|
|
237
279
|
* mean cost. Null ceiling ⇒ no cost constraint (regret alone gates). */
|
|
238
280
|
function withinCeiling(m, ceiling) {
|
|
@@ -298,7 +340,8 @@ function computeProposal(requested, metrics, ctx) {
|
|
|
298
340
|
const eligible = pool.filter((m) => m.regret != null &&
|
|
299
341
|
m.regret.nPaired >= 10 &&
|
|
300
342
|
(objective.maxRegret === null || m.regret.regretCi95[1] <= objective.maxRegret + EPS) &&
|
|
301
|
-
withinCeiling(m, objective.priceCeiling)
|
|
343
|
+
withinCeiling(m, objective.priceCeiling) &&
|
|
344
|
+
pairwiseEligible(m, objective));
|
|
302
345
|
if (eligible.length === 0)
|
|
303
346
|
return null;
|
|
304
347
|
// Lowest regret, tie → lowest cost.
|
|
@@ -333,7 +376,8 @@ function computeProposal(requested, metrics, ctx) {
|
|
|
333
376
|
const eligible = pool.filter((m) => m.regret != null &&
|
|
334
377
|
m.regret.nPaired >= 10 &&
|
|
335
378
|
(objective.maxRegret === null || m.regret.regretCi95[1] <= objective.maxRegret + EPS) &&
|
|
336
|
-
withinCeiling(m, objective.priceCeiling)
|
|
379
|
+
withinCeiling(m, objective.priceCeiling) &&
|
|
380
|
+
pairwiseEligible(m, objective));
|
|
337
381
|
if (eligible.length === 0)
|
|
338
382
|
return null;
|
|
339
383
|
const winner = [...eligible].sort((a, b) => blended(a) - blended(b))[0];
|
|
@@ -395,6 +439,17 @@ function bakeoffEvidence(artifact, spentUsd) {
|
|
|
395
439
|
reference: artifact.reference ?? null,
|
|
396
440
|
regret: artifact.proposal?.regret ?? null,
|
|
397
441
|
quality_metric: artifact.qualityMetric ?? null,
|
|
442
|
+
// N6: disagreement COUNTS only (numbers — meta-safe) ride the evidence; the
|
|
443
|
+
// captureId candidate list stays on the LOCAL artifact and is never sent to
|
|
444
|
+
// the control plane (content custody).
|
|
445
|
+
disagreement: artifact.disagreement === undefined
|
|
446
|
+
? null
|
|
447
|
+
: {
|
|
448
|
+
examined: artifact.disagreement.examined,
|
|
449
|
+
found: artifact.disagreement.found,
|
|
450
|
+
capped_at: artifact.disagreement.cappedAt,
|
|
451
|
+
counts: artifact.disagreement.counts,
|
|
452
|
+
},
|
|
398
453
|
matrix: artifact.variants.map((m) => ({
|
|
399
454
|
variant: m.variant,
|
|
400
455
|
samples: m.samples,
|
|
@@ -408,6 +463,7 @@ function bakeoffEvidence(artifact, spentUsd) {
|
|
|
408
463
|
quality_mean: m.qualityMean,
|
|
409
464
|
quality_metric: m.qualityMetric,
|
|
410
465
|
regret: m.regret ?? null,
|
|
466
|
+
pairwise: m.pairwise ?? null,
|
|
411
467
|
})),
|
|
412
468
|
};
|
|
413
469
|
}
|