modelrig 0.1.0 → 0.2.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 +94 -0
- package/README.md +41 -11
- package/data/PROVENANCE.md +19 -0
- package/data/pricing-manual.json +22 -0
- package/data/pricing-map.json +46362 -0
- package/data/pricing-overlay.json +419 -0
- package/dist/adapters/anthropic.d.ts +50 -6
- package/dist/adapters/anthropic.d.ts.map +1 -1
- package/dist/adapters/anthropic.js +123 -29
- package/dist/adapters/build-adapters.d.ts +11 -0
- package/dist/adapters/build-adapters.d.ts.map +1 -0
- package/dist/adapters/build-adapters.js +45 -0
- package/dist/adapters/deepseek.d.ts +6 -0
- package/dist/adapters/deepseek.d.ts.map +1 -1
- package/dist/adapters/deepseek.js +54 -1
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +46 -6
- package/dist/adapters/grok.d.ts +9 -0
- package/dist/adapters/grok.d.ts.map +1 -1
- package/dist/adapters/grok.js +67 -3
- package/dist/adapters/openai-compat.d.ts +6 -0
- package/dist/adapters/openai-compat.d.ts.map +1 -1
- package/dist/adapters/openai-compat.js +58 -1
- package/dist/adapters/openai-stream.d.ts +58 -0
- package/dist/adapters/openai-stream.d.ts.map +1 -0
- package/dist/adapters/openai-stream.js +120 -0
- package/dist/adapters/openai.d.ts +6 -0
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +60 -1
- package/dist/adapters/sampling.d.ts +28 -0
- package/dist/adapters/sampling.d.ts.map +1 -0
- package/dist/adapters/sampling.js +34 -0
- package/dist/adapters/tool-mapping.d.ts +92 -0
- package/dist/adapters/tool-mapping.d.ts.map +1 -0
- package/dist/adapters/tool-mapping.js +143 -0
- package/dist/artifacts/artifact.d.ts +137 -0
- package/dist/artifacts/artifact.d.ts.map +1 -0
- package/dist/artifacts/artifact.js +377 -0
- package/dist/artifacts/content-uploader.d.ts +87 -0
- package/dist/artifacts/content-uploader.d.ts.map +1 -0
- package/dist/artifacts/content-uploader.js +124 -0
- package/dist/artifacts/index.d.ts +63 -0
- package/dist/artifacts/index.d.ts.map +1 -0
- package/dist/artifacts/index.js +80 -0
- package/dist/artifacts/run-context.d.ts +129 -0
- package/dist/artifacts/run-context.d.ts.map +1 -0
- package/dist/artifacts/run-context.js +208 -0
- package/dist/artifacts/scrub.d.ts +60 -0
- package/dist/artifacts/scrub.d.ts.map +1 -0
- package/dist/artifacts/scrub.js +139 -0
- package/dist/artifacts/step.d.ts +26 -0
- package/dist/artifacts/step.d.ts.map +1 -0
- package/dist/artifacts/step.js +37 -0
- package/dist/artifacts/taint.d.ts +63 -0
- package/dist/artifacts/taint.d.ts.map +1 -0
- package/dist/artifacts/taint.js +76 -0
- package/dist/bakeoff/cache-sim.d.ts +99 -0
- package/dist/bakeoff/cache-sim.d.ts.map +1 -0
- package/dist/bakeoff/cache-sim.js +126 -0
- package/dist/bakeoff/eval-cases.d.ts +96 -0
- package/dist/bakeoff/eval-cases.d.ts.map +1 -0
- package/dist/bakeoff/eval-cases.js +105 -0
- package/dist/bakeoff/report.d.ts +41 -0
- package/dist/bakeoff/report.d.ts.map +1 -0
- package/dist/bakeoff/report.js +142 -0
- package/dist/bakeoff/samples.d.ts +15 -0
- package/dist/bakeoff/samples.d.ts.map +1 -1
- package/dist/bakeoff/samples.js +20 -0
- package/dist/cli.d.ts +1 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +134 -116
- package/dist/config.d.ts +100 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +104 -0
- package/dist/deprecations.d.ts +32 -0
- package/dist/deprecations.d.ts.map +1 -0
- package/dist/deprecations.js +59 -0
- package/dist/gateway/http-protocol.d.ts +175 -0
- package/dist/gateway/http-protocol.d.ts.map +1 -0
- package/dist/gateway/http-protocol.js +105 -0
- package/dist/index.d.ts +105 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +266 -138
- package/dist/init/finder.d.ts +15 -0
- package/dist/init/finder.d.ts.map +1 -1
- package/dist/init/finder.js +46 -0
- package/dist/init/init-cli.d.ts +10 -0
- package/dist/init/init-cli.d.ts.map +1 -0
- package/dist/init/init-cli.js +86 -0
- package/dist/init/observe-cli.d.ts +19 -0
- package/dist/init/observe-cli.d.ts.map +1 -0
- package/dist/init/observe-cli.js +73 -0
- package/dist/init/observe.d.ts +81 -0
- package/dist/init/observe.d.ts.map +1 -0
- package/dist/init/observe.js +245 -0
- package/dist/registry/build.d.ts +94 -1
- package/dist/registry/build.d.ts.map +1 -1
- package/dist/registry/build.js +125 -4
- package/dist/registry/cache-policy.d.ts +94 -0
- package/dist/registry/cache-policy.d.ts.map +1 -0
- package/dist/registry/cache-policy.js +238 -0
- package/dist/registry/call-notes.d.ts.map +1 -1
- package/dist/registry/call-notes.js +40 -2
- package/dist/registry/effective-rate.d.ts +151 -0
- package/dist/registry/effective-rate.d.ts.map +1 -0
- package/dist/registry/effective-rate.js +156 -0
- package/dist/registry/facts.d.ts.map +1 -1
- package/dist/registry/facts.js +16 -0
- package/dist/registry/parity.d.ts +26 -0
- package/dist/registry/parity.d.ts.map +1 -1
- package/dist/registry/parity.js +43 -2
- package/dist/registry/pricing-shape.d.ts +113 -0
- package/dist/registry/pricing-shape.d.ts.map +1 -0
- package/dist/registry/pricing-shape.js +123 -0
- package/dist/registry/pricing.d.ts +25 -1
- package/dist/registry/pricing.d.ts.map +1 -1
- package/dist/registry/pricing.js +64 -6
- package/dist/rigindex/bootstrap.d.ts +36 -0
- package/dist/rigindex/bootstrap.d.ts.map +1 -0
- package/dist/rigindex/bootstrap.js +97 -0
- package/dist/rigindex/fit.d.ts +24 -0
- package/dist/rigindex/fit.d.ts.map +1 -0
- package/dist/rigindex/fit.js +76 -0
- package/dist/rigindex/gates.d.ts +52 -0
- package/dist/rigindex/gates.d.ts.map +1 -0
- package/dist/rigindex/gates.js +109 -0
- package/dist/rigindex/graph.d.ts +31 -0
- package/dist/rigindex/graph.d.ts.map +1 -0
- package/dist/rigindex/graph.js +120 -0
- package/dist/rigindex/index.d.ts +23 -0
- package/dist/rigindex/index.d.ts.map +1 -0
- package/dist/rigindex/index.js +112 -0
- package/dist/rigindex/types.d.ts +76 -0
- package/dist/rigindex/types.d.ts.map +1 -0
- package/dist/rigindex/types.js +8 -0
- package/dist/routes/cache-lint.d.ts +41 -0
- package/dist/routes/cache-lint.d.ts.map +1 -0
- package/dist/routes/cache-lint.js +81 -0
- package/dist/routes/loader.d.ts +37 -0
- package/dist/routes/loader.d.ts.map +1 -1
- package/dist/routes/loader.js +138 -0
- package/dist/routes/resolved-bundle.d.ts +79 -0
- package/dist/routes/resolved-bundle.d.ts.map +1 -0
- package/dist/routes/resolved-bundle.js +148 -0
- package/dist/routes/sampling-config.d.ts +32 -0
- package/dist/routes/sampling-config.d.ts.map +1 -0
- package/dist/routes/sampling-config.js +91 -0
- package/dist/run/ajv.d.ts +22 -0
- package/dist/run/ajv.d.ts.map +1 -0
- package/dist/run/ajv.js +32 -0
- package/dist/run/cache-handle.d.ts +58 -0
- package/dist/run/cache-handle.d.ts.map +1 -0
- package/dist/run/cache-handle.js +54 -0
- package/dist/run/cache-stamp.d.ts +65 -0
- package/dist/run/cache-stamp.d.ts.map +1 -0
- package/dist/run/cache-stamp.js +0 -0
- package/dist/run/capture-write.d.ts +33 -0
- package/dist/run/capture-write.d.ts.map +1 -0
- package/dist/run/capture-write.js +55 -0
- package/dist/run/cost.d.ts.map +1 -1
- package/dist/run/cost.js +1 -1
- package/dist/run/dispatch.d.ts +99 -0
- package/dist/run/dispatch.d.ts.map +1 -0
- package/dist/run/dispatch.js +191 -0
- package/dist/run/raw.d.ts +88 -0
- package/dist/run/raw.d.ts.map +1 -0
- package/dist/run/raw.js +286 -0
- package/dist/run/record-attempt.d.ts +40 -0
- package/dist/run/record-attempt.d.ts.map +1 -0
- package/dist/run/record-attempt.js +99 -0
- package/dist/run/resolve.d.ts +75 -2
- package/dist/run/resolve.d.ts.map +1 -1
- package/dist/run/resolve.js +126 -4
- package/dist/run/run.d.ts +14 -0
- package/dist/run/run.d.ts.map +1 -1
- package/dist/run/run.js +0 -0
- package/dist/run/stream.d.ts +49 -0
- package/dist/run/stream.d.ts.map +1 -0
- package/dist/run/stream.js +87 -0
- package/dist/run/validate.d.ts.map +1 -1
- package/dist/run/validate.js +6 -7
- package/dist/savings/cache-ledger.d.ts +108 -0
- package/dist/savings/cache-ledger.d.ts.map +1 -0
- package/dist/savings/cache-ledger.js +174 -0
- package/dist/savings/cache-reconcile.d.ts +64 -0
- package/dist/savings/cache-reconcile.d.ts.map +1 -0
- package/dist/savings/cache-reconcile.js +59 -0
- package/dist/savings/cache-stats.d.ts +63 -0
- package/dist/savings/cache-stats.d.ts.map +1 -0
- package/dist/savings/cache-stats.js +108 -0
- package/dist/savings/emit.d.ts +13 -0
- package/dist/savings/emit.d.ts.map +1 -1
- package/dist/savings/emit.js +25 -0
- package/dist/savings/swap-execute.d.ts +14 -0
- package/dist/savings/swap-execute.d.ts.map +1 -1
- package/dist/savings/swap-execute.js +44 -1
- package/dist/savings/swaps-client.d.ts +14 -0
- package/dist/savings/swaps-client.d.ts.map +1 -1
- package/dist/savings/swaps-client.js +16 -2
- package/dist/settings/api-fetcher.d.ts +39 -0
- package/dist/settings/api-fetcher.d.ts.map +1 -0
- package/dist/settings/api-fetcher.js +92 -0
- package/dist/settings/index.d.ts +10 -0
- package/dist/settings/index.d.ts.map +1 -0
- package/dist/settings/index.js +15 -0
- package/dist/settings/task-settings.d.ts +91 -0
- package/dist/settings/task-settings.d.ts.map +1 -0
- package/dist/settings/task-settings.js +116 -0
- package/dist/status.d.ts +77 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +113 -0
- package/dist/telemetry/artifact-store.d.ts +48 -0
- package/dist/telemetry/artifact-store.d.ts.map +1 -0
- package/dist/telemetry/artifact-store.js +189 -0
- package/dist/telemetry/cache-miss-streak.d.ts +79 -0
- package/dist/telemetry/cache-miss-streak.d.ts.map +1 -0
- package/dist/telemetry/cache-miss-streak.js +99 -0
- package/dist/telemetry/capture.d.ts +3 -0
- package/dist/telemetry/capture.d.ts.map +1 -1
- package/dist/telemetry/capture.js +15 -8
- package/dist/telemetry/exporter-setup.d.ts +28 -0
- package/dist/telemetry/exporter-setup.d.ts.map +1 -0
- package/dist/telemetry/exporter-setup.js +34 -0
- package/dist/telemetry/exporter.d.ts +75 -0
- package/dist/telemetry/exporter.d.ts.map +1 -1
- package/dist/telemetry/exporter.js +324 -47
- package/dist/telemetry/health.d.ts +76 -0
- package/dist/telemetry/health.d.ts.map +1 -0
- package/dist/telemetry/health.js +92 -0
- package/dist/telemetry/ingest-protocol.d.ts +152 -3
- package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
- package/dist/telemetry/ingest-protocol.js +204 -4
- package/dist/telemetry/load-error-report.d.ts +24 -0
- package/dist/telemetry/load-error-report.d.ts.map +1 -0
- package/dist/telemetry/load-error-report.js +50 -0
- package/dist/telemetry/otel-map.d.ts +121 -0
- package/dist/telemetry/otel-map.d.ts.map +1 -0
- package/dist/telemetry/otel-map.js +224 -0
- package/dist/telemetry/otel-normalize.d.ts +104 -0
- package/dist/telemetry/otel-normalize.d.ts.map +1 -0
- package/dist/telemetry/otel-normalize.js +530 -0
- package/dist/telemetry/otlp-emitter.d.ts +52 -0
- package/dist/telemetry/otlp-emitter.d.ts.map +1 -0
- package/dist/telemetry/otlp-emitter.js +144 -0
- package/dist/telemetry/otlp-proto.d.ts +29 -0
- package/dist/telemetry/otlp-proto.d.ts.map +1 -0
- package/dist/telemetry/otlp-proto.js +266 -0
- package/dist/telemetry/sinks.d.ts +37 -1
- package/dist/telemetry/sinks.d.ts.map +1 -1
- package/dist/telemetry/sinks.js +121 -30
- package/dist/telemetry/store.d.ts +28 -1
- package/dist/telemetry/store.d.ts.map +1 -1
- package/dist/telemetry/store.js +218 -3
- package/dist/tenancy/api-key-format.d.ts +15 -5
- package/dist/tenancy/api-key-format.d.ts.map +1 -1
- package/dist/tenancy/api-key-format.js +24 -5
- package/dist/types/config-teaching.d.ts +62 -0
- package/dist/types/config-teaching.d.ts.map +1 -0
- package/dist/types/config-teaching.js +183 -0
- package/dist/types/provider.d.ts +122 -1
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/provider.js +21 -1
- package/dist/types/route.d.ts +79 -1
- package/dist/types/route.d.ts.map +1 -1
- package/dist/types/telemetry.d.ts +117 -0
- package/dist/types/telemetry.d.ts.map +1 -1
- package/dist/validate-cli.d.ts +12 -0
- package/dist/validate-cli.d.ts.map +1 -0
- package/dist/validate-cli.js +57 -0
- package/dist/verify/capture.d.ts +66 -0
- package/dist/verify/capture.d.ts.map +1 -0
- package/dist/verify/capture.js +180 -0
- package/dist/verify/cli-run.d.ts +23 -0
- package/dist/verify/cli-run.d.ts.map +1 -0
- package/dist/verify/cli-run.js +57 -0
- package/dist/verify/diff.d.ts +48 -0
- package/dist/verify/diff.d.ts.map +1 -0
- package/dist/verify/diff.js +157 -0
- package/dist/verify/index.d.ts +17 -0
- package/dist/verify/index.d.ts.map +1 -0
- package/dist/verify/index.js +24 -0
- package/dist/verify/report.d.ts +14 -0
- package/dist/verify/report.d.ts.map +1 -0
- package/dist/verify/report.js +88 -0
- package/dist/verify/types.d.ts +101 -0
- package/dist/verify/types.d.ts.map +1 -0
- package/dist/verify/types.js +17 -0
- package/dist/verify/verify.d.ts +29 -0
- package/dist/verify/verify.d.ts.map +1 -0
- package/dist/verify/verify.js +148 -0
- package/dist/watch/evaluate.d.ts +162 -3
- package/dist/watch/evaluate.d.ts.map +1 -1
- package/dist/watch/evaluate.js +61 -0
- package/dist/watch/monitor.d.ts +116 -0
- package/dist/watch/monitor.d.ts.map +1 -0
- package/dist/watch/monitor.js +199 -0
- package/dist/watch/proposals.d.ts +20 -4
- package/dist/watch/proposals.d.ts.map +1 -1
- package/dist/watch/proposals.js +31 -3
- package/dist/watch/run.d.ts +33 -0
- package/dist/watch/run.d.ts.map +1 -1
- package/dist/watch/run.js +39 -0
- package/package.json +7 -1
- package/registry/LICENSE +20 -0
- package/registry/leaderboard.json +3639 -0
- package/registry/registry.json +7263 -0
- package/dist/telemetry/supabase-exporter.d.ts +0 -64
- package/dist/telemetry/supabase-exporter.d.ts.map +0 -1
- package/dist/telemetry/supabase-exporter.js +0 -211
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The diff engine — the honest verdict vocabulary that makes the verifier's
|
|
4
|
+
* report trustworthy. Pure (no adapters, no run loop): it compares an
|
|
5
|
+
* original output against a routed one and returns one of three verdicts.
|
|
6
|
+
*
|
|
7
|
+
* Design decision (flagged for architect review): the routed output arrives
|
|
8
|
+
* as PARSED JSON — `run()` only returns after the output validated against the
|
|
9
|
+
* route schema (or it throws), so a routed sample is guaranteed schema-
|
|
10
|
+
* conformant and JSON. We therefore compare STRUCTURE, not raw bytes. For JSON,
|
|
11
|
+
* key order and whitespace are not behavior, so "byte-identical where
|
|
12
|
+
* deterministic" is operationalized as CANONICAL identity (sorted-key
|
|
13
|
+
* serialization). Structured-field equality — with declared nondeterministic
|
|
14
|
+
* fields excluded — is the fallback, exactly as the spec's §6 evaluation
|
|
15
|
+
* contract prescribes.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.canonicalJson = canonicalJson;
|
|
19
|
+
exports.firstDivergence = firstDivergence;
|
|
20
|
+
exports.diffOutputs = diffOutputs;
|
|
21
|
+
/** Deterministic JSON serialization: object keys sorted recursively, arrays
|
|
22
|
+
* left in order (order is semantic). The operative notion of "byte-identical"
|
|
23
|
+
* for JSON outputs. */
|
|
24
|
+
function canonicalJson(value) {
|
|
25
|
+
return JSON.stringify(sortKeys(value));
|
|
26
|
+
}
|
|
27
|
+
function sortKeys(value) {
|
|
28
|
+
if (Array.isArray(value))
|
|
29
|
+
return value.map(sortKeys);
|
|
30
|
+
if (value !== null && typeof value === "object") {
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const key of Object.keys(value).sort()) {
|
|
33
|
+
out[key] = sortKeys(value[key]);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
/** True when `path` is exactly, or nested under, any declared nondeterministic
|
|
40
|
+
* path. `/meta` masks `/meta` and `/meta/gen`; it never masks `/metadata`. */
|
|
41
|
+
function isMasked(path, nondeterministicPaths) {
|
|
42
|
+
return nondeterministicPaths.some((nd) => path === nd || path.startsWith(`${nd}/`));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* First point at which `original` and `routed` structurally differ, in a
|
|
46
|
+
* stable depth-first order, or null when they are deep-equal after masking the
|
|
47
|
+
* declared nondeterministic paths. Object keys are visited in sorted order so
|
|
48
|
+
* the "first" differing field is deterministic across runs.
|
|
49
|
+
*/
|
|
50
|
+
function firstDivergence(original, routed, nondeterministicPaths, path = "") {
|
|
51
|
+
const here = path === "" ? "" : path;
|
|
52
|
+
if (isMasked(here || "/", nondeterministicPaths))
|
|
53
|
+
return null;
|
|
54
|
+
if (Array.isArray(original) || Array.isArray(routed)) {
|
|
55
|
+
if (!Array.isArray(original) || !Array.isArray(routed) || original.length !== routed.length) {
|
|
56
|
+
return { path: here || "/", originalValue: original, routedValue: routed };
|
|
57
|
+
}
|
|
58
|
+
for (let i = 0; i < original.length; i += 1) {
|
|
59
|
+
const d = firstDivergence(original[i], routed[i], nondeterministicPaths, `${here}/${i}`);
|
|
60
|
+
if (d !== null)
|
|
61
|
+
return d;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (isObject(original) || isObject(routed)) {
|
|
66
|
+
if (!isObject(original) || !isObject(routed)) {
|
|
67
|
+
return { path: here || "/", originalValue: original, routedValue: routed };
|
|
68
|
+
}
|
|
69
|
+
const keys = [...new Set([...Object.keys(original), ...Object.keys(routed)])].sort();
|
|
70
|
+
for (const key of keys) {
|
|
71
|
+
const childPath = `${here}/${escapePointer(key)}`;
|
|
72
|
+
if (isMasked(childPath, nondeterministicPaths))
|
|
73
|
+
continue;
|
|
74
|
+
const inO = Object.prototype.hasOwnProperty.call(original, key);
|
|
75
|
+
const inR = Object.prototype.hasOwnProperty.call(routed, key);
|
|
76
|
+
if (!inO || !inR) {
|
|
77
|
+
return { path: childPath, originalValue: original[key], routedValue: routed[key] };
|
|
78
|
+
}
|
|
79
|
+
const d = firstDivergence(original[key], routed[key], nondeterministicPaths, childPath);
|
|
80
|
+
if (d !== null)
|
|
81
|
+
return d;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
// Scalars (incl. null): a strict, type-aware compare.
|
|
86
|
+
if (original !== routed) {
|
|
87
|
+
return { path: here || "/", originalValue: original, routedValue: routed };
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
function isObject(v) {
|
|
92
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
93
|
+
}
|
|
94
|
+
/** RFC-6901 escaping for a path segment (`~`→`~0`, `/`→`~1`). */
|
|
95
|
+
function escapePointer(key) {
|
|
96
|
+
return key.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
97
|
+
}
|
|
98
|
+
function shortValue(v) {
|
|
99
|
+
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
100
|
+
return s !== undefined && s.length > 60 ? `${s.slice(0, 57)}…` : String(s);
|
|
101
|
+
}
|
|
102
|
+
function tryParse(text) {
|
|
103
|
+
try {
|
|
104
|
+
return { ok: true, value: JSON.parse(text) };
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return { ok: false };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Diff an original output (raw text) against a routed output (already parsed by
|
|
112
|
+
* `run()`), returning the verdict. `routedParsed` is guaranteed schema-
|
|
113
|
+
* conformant JSON by the run loop; `originalText` is whatever the original call
|
|
114
|
+
* site emitted.
|
|
115
|
+
*
|
|
116
|
+
* - byte / canonical identity ⇒ `equivalent`
|
|
117
|
+
* - structurally equal after masking declared nd-paths ⇒
|
|
118
|
+
* `equivalent-modulo-nondeterminism`
|
|
119
|
+
* - a real structured field differs ⇒ `divergent` (first field named)
|
|
120
|
+
* - the original is not JSON ⇒ `divergent` (a free-text call site — this
|
|
121
|
+
* isn't route-shaped work; T0/T1 territory, honestly flagged not forced)
|
|
122
|
+
*/
|
|
123
|
+
function diffOutputs(originalText, routedParsed, routedText, nondeterministicPaths) {
|
|
124
|
+
// True byte-identity first — the strongest, cheapest signal.
|
|
125
|
+
if (originalText === routedText) {
|
|
126
|
+
return { verdict: "equivalent", firstDivergentPath: null, detail: "byte-identical output" };
|
|
127
|
+
}
|
|
128
|
+
const parsedOriginal = tryParse(originalText);
|
|
129
|
+
if (!parsedOriginal.ok) {
|
|
130
|
+
return {
|
|
131
|
+
verdict: "divergent",
|
|
132
|
+
firstDivergentPath: null,
|
|
133
|
+
detail: "original output is not JSON — this is a free-text call site; equivalence is not " +
|
|
134
|
+
"provable through a task route (T0/T1 territory, not a Tier-2 replacement)",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// Canonical identity: for JSON, key order and whitespace are not behavior.
|
|
138
|
+
if (canonicalJson(parsedOriginal.value) === canonicalJson(routedParsed)) {
|
|
139
|
+
return { verdict: "equivalent", firstDivergentPath: null, detail: "canonically identical JSON" };
|
|
140
|
+
}
|
|
141
|
+
const d = firstDivergence(parsedOriginal.value, routedParsed, nondeterministicPaths);
|
|
142
|
+
if (d === null) {
|
|
143
|
+
return {
|
|
144
|
+
verdict: "equivalent-modulo-nondeterminism",
|
|
145
|
+
firstDivergentPath: null,
|
|
146
|
+
detail: nondeterministicPaths.length > 0
|
|
147
|
+
? `structurally equal after excluding nondeterministic field(s): ${nondeterministicPaths.join(", ")}`
|
|
148
|
+
: "structurally equal (serialization differs only)",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
verdict: "divergent",
|
|
153
|
+
firstDivergentPath: d.path,
|
|
154
|
+
detail: `structured field ${d.path} differs: original=${shortValue(d.originalValue)} ` +
|
|
155
|
+
`routed=${shortValue(d.routedValue)}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow-equivalence verifier (Expansion §9, autonomy-ladder Tier 2) — the
|
|
3
|
+
* public barrel. Proves a same-model call replacement is behavior-preserving
|
|
4
|
+
* before a human merges the Tier-2 PR: capture (local) → replay through the
|
|
5
|
+
* routed replacement (reusing the run loop) → diff (honest verdict vocabulary)
|
|
6
|
+
* → report (machine + human). Refusing an uncapturable call site is a correct
|
|
7
|
+
* output, never a failure.
|
|
8
|
+
*/
|
|
9
|
+
export { verifySwap } from "./verify";
|
|
10
|
+
export type { VerifySwapRequest } from "./verify";
|
|
11
|
+
export { diffOutputs, firstDivergence, canonicalJson } from "./diff";
|
|
12
|
+
export type { Divergence } from "./diff";
|
|
13
|
+
export { captureOriginal, openShadowCaptureStore, parseFixture, serializeFixture, } from "./capture";
|
|
14
|
+
export type { OriginalCallSite, OriginalCallFn, CaptureOptions, ShadowCaptureStore, ShadowCaptureRecord, } from "./capture";
|
|
15
|
+
export { renderReportMarkdown } from "./report";
|
|
16
|
+
export type { ShadowCapture, ShadowCaptureFixture, CaptureRefusal, SampleVerdict, EquivalenceVerdict, SampleDiff, SampleReport, EquivalenceReport, } from "./types";
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/verify/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,YAAY,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACrE,YAAY,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,YAAY,EACZ,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,YAAY,EACV,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAChD,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,iBAAiB,GAClB,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shadow-equivalence verifier (Expansion §9, autonomy-ladder Tier 2) — the
|
|
4
|
+
* public barrel. Proves a same-model call replacement is behavior-preserving
|
|
5
|
+
* before a human merges the Tier-2 PR: capture (local) → replay through the
|
|
6
|
+
* routed replacement (reusing the run loop) → diff (honest verdict vocabulary)
|
|
7
|
+
* → report (machine + human). Refusing an uncapturable call site is a correct
|
|
8
|
+
* output, never a failure.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.renderReportMarkdown = exports.serializeFixture = exports.parseFixture = exports.openShadowCaptureStore = exports.captureOriginal = exports.canonicalJson = exports.firstDivergence = exports.diffOutputs = exports.verifySwap = void 0;
|
|
12
|
+
var verify_1 = require("./verify");
|
|
13
|
+
Object.defineProperty(exports, "verifySwap", { enumerable: true, get: function () { return verify_1.verifySwap; } });
|
|
14
|
+
var diff_1 = require("./diff");
|
|
15
|
+
Object.defineProperty(exports, "diffOutputs", { enumerable: true, get: function () { return diff_1.diffOutputs; } });
|
|
16
|
+
Object.defineProperty(exports, "firstDivergence", { enumerable: true, get: function () { return diff_1.firstDivergence; } });
|
|
17
|
+
Object.defineProperty(exports, "canonicalJson", { enumerable: true, get: function () { return diff_1.canonicalJson; } });
|
|
18
|
+
var capture_1 = require("./capture");
|
|
19
|
+
Object.defineProperty(exports, "captureOriginal", { enumerable: true, get: function () { return capture_1.captureOriginal; } });
|
|
20
|
+
Object.defineProperty(exports, "openShadowCaptureStore", { enumerable: true, get: function () { return capture_1.openShadowCaptureStore; } });
|
|
21
|
+
Object.defineProperty(exports, "parseFixture", { enumerable: true, get: function () { return capture_1.parseFixture; } });
|
|
22
|
+
Object.defineProperty(exports, "serializeFixture", { enumerable: true, get: function () { return capture_1.serializeFixture; } });
|
|
23
|
+
var report_1 = require("./report");
|
|
24
|
+
Object.defineProperty(exports, "renderReportMarkdown", { enumerable: true, get: function () { return report_1.renderReportMarkdown; } });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report rendering — the shadow-equivalence report the skill's Tier-2 PR
|
|
3
|
+
* embeds. Two forms, one source: the machine-readable form is the
|
|
4
|
+
* `EquivalenceReport` object serialized as JSON (stable, `schemaVersion: 1`);
|
|
5
|
+
* the human-readable form is the Markdown below, written for a PR reviewer who
|
|
6
|
+
* must decide whether the same-model replacement is safe to merge.
|
|
7
|
+
*
|
|
8
|
+
* The rendering never editorializes the verdict — it prints exactly what the
|
|
9
|
+
* diff decided, always alongside the plumbing-not-model-choice guarantee so a
|
|
10
|
+
* reader cannot mistake equivalence for a model-quality claim.
|
|
11
|
+
*/
|
|
12
|
+
import type { EquivalenceReport } from "./types";
|
|
13
|
+
export declare function renderReportMarkdown(report: EquivalenceReport): string;
|
|
14
|
+
//# sourceMappingURL=report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/verify/report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAoC,MAAM,SAAS,CAAC;AAwCnF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CA+CtE"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Report rendering — the shadow-equivalence report the skill's Tier-2 PR
|
|
4
|
+
* embeds. Two forms, one source: the machine-readable form is the
|
|
5
|
+
* `EquivalenceReport` object serialized as JSON (stable, `schemaVersion: 1`);
|
|
6
|
+
* the human-readable form is the Markdown below, written for a PR reviewer who
|
|
7
|
+
* must decide whether the same-model replacement is safe to merge.
|
|
8
|
+
*
|
|
9
|
+
* The rendering never editorializes the verdict — it prints exactly what the
|
|
10
|
+
* diff decided, always alongside the plumbing-not-model-choice guarantee so a
|
|
11
|
+
* reader cannot mistake equivalence for a model-quality claim.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.renderReportMarkdown = renderReportMarkdown;
|
|
15
|
+
const VERDICT_LABEL = {
|
|
16
|
+
equivalent: "EQUIVALENT",
|
|
17
|
+
"equivalent-modulo-nondeterminism": "EQUIVALENT (modulo nondeterminism)",
|
|
18
|
+
divergent: "DIVERGENT",
|
|
19
|
+
refused: "REFUSED",
|
|
20
|
+
};
|
|
21
|
+
/** The one-line headline a reviewer reads first. */
|
|
22
|
+
function headline(report) {
|
|
23
|
+
const label = VERDICT_LABEL[report.overall];
|
|
24
|
+
switch (report.overall) {
|
|
25
|
+
case "equivalent":
|
|
26
|
+
return `✅ **${label}** — all ${report.n} replayed input(s) reproduced the original output.`;
|
|
27
|
+
case "equivalent-modulo-nondeterminism":
|
|
28
|
+
return `🟡 **${label}** — all ${report.n} input(s) matched once declared nondeterministic fields were excluded.`;
|
|
29
|
+
case "divergent":
|
|
30
|
+
return `❌ **${label}** — ${report.counts.divergent} of ${report.n} input(s) did not reproduce; do not merge.`;
|
|
31
|
+
case "refused":
|
|
32
|
+
return `⛔ **${label}** — not a Tier-2 replacement. ${report.refusalReason ?? ""}`.trim();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Escape `|` so a field value never breaks the markdown table (the report is
|
|
36
|
+
* embedded in a PR body — a stray pipe from an odd JSON key must not corrupt it). */
|
|
37
|
+
function cell(text) {
|
|
38
|
+
return text.replace(/\|/g, "\\|");
|
|
39
|
+
}
|
|
40
|
+
function sampleRow(s) {
|
|
41
|
+
const detail = s.failureClass !== null
|
|
42
|
+
? `replay failed (${s.failureClass})`
|
|
43
|
+
: s.firstDivergentPath !== null
|
|
44
|
+
? `first differs at \`${cell(s.firstDivergentPath)}\``
|
|
45
|
+
: cell(s.detail);
|
|
46
|
+
return `| ${s.index} | ${s.verdict} | ${cell(s.servedModel ?? "—")} | $${s.costUsd.toFixed(4)} | ${detail} |`;
|
|
47
|
+
}
|
|
48
|
+
function renderReportMarkdown(report) {
|
|
49
|
+
const lines = [];
|
|
50
|
+
lines.push("## Shadow-equivalence verification");
|
|
51
|
+
lines.push("");
|
|
52
|
+
lines.push(headline(report));
|
|
53
|
+
lines.push("");
|
|
54
|
+
lines.push(`- **Call site:** \`${report.callSiteId}\``);
|
|
55
|
+
lines.push(`- **Route:** \`${report.route}@${report.routeVersion}\``);
|
|
56
|
+
lines.push(`- **Pinned model:** \`${report.pinnedModel}\` (original == replacement)`);
|
|
57
|
+
lines.push(`- **Ran at:** ${report.ranAt}`);
|
|
58
|
+
lines.push(`- **Tally:** ${report.counts.equivalent} equivalent · ` +
|
|
59
|
+
`${report.counts.equivalentModulo} modulo-nondeterminism · ${report.counts.divergent} divergent`);
|
|
60
|
+
lines.push("");
|
|
61
|
+
lines.push(`> ${report.guarantee}`);
|
|
62
|
+
lines.push("");
|
|
63
|
+
if (report.samples.length > 0) {
|
|
64
|
+
lines.push("| # | verdict | served model | cost | detail |");
|
|
65
|
+
lines.push("|---|---------|--------------|------|--------|");
|
|
66
|
+
for (const s of report.samples)
|
|
67
|
+
lines.push(sampleRow(s));
|
|
68
|
+
lines.push("");
|
|
69
|
+
}
|
|
70
|
+
if (report.refusals.length > 0) {
|
|
71
|
+
lines.push("### Refusals (honest, not failures)");
|
|
72
|
+
lines.push("");
|
|
73
|
+
for (const r of report.refusals)
|
|
74
|
+
lines.push(`- **${r.reason}** — ${r.detail}`);
|
|
75
|
+
lines.push("");
|
|
76
|
+
lines.push("Refused call sites are T0/T1 territory (gateway-pointing or instrumentation), " +
|
|
77
|
+
"not same-model call replacement — this is a correct outcome, not a gap.");
|
|
78
|
+
lines.push("");
|
|
79
|
+
}
|
|
80
|
+
lines.push(report.overall === "divergent"
|
|
81
|
+
? "**Rollback is git-native:** revert this PR to restore the original call byte-for-byte."
|
|
82
|
+
: report.overall === "refused"
|
|
83
|
+
? "No change is proposed for this call site."
|
|
84
|
+
: "Safe to merge on this evidence: the routed replacement preserves behavior. " +
|
|
85
|
+
"Rollback stays git-native (revert this PR) — same-model means it is also a route-YAML flip.");
|
|
86
|
+
lines.push("");
|
|
87
|
+
return lines.join("\n");
|
|
88
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow-equivalence verifier contracts (Expansion §9, autonomy-ladder Tier 2).
|
|
3
|
+
*
|
|
4
|
+
* The verifier proves a SAME-MODEL call replacement is behavior-preserving
|
|
5
|
+
* BEFORE a human merges the Tier-2 PR: capture N invocations of the original
|
|
6
|
+
* call site's inputs/outputs locally, replay the SAME inputs through the
|
|
7
|
+
* ModelRig-routed replacement (same provider/model PINNED — this verifies the
|
|
8
|
+
* PLUMBING, never the model choice), diff the outputs, and emit a report the
|
|
9
|
+
* skill embeds in the PR.
|
|
10
|
+
*
|
|
11
|
+
* Honest refusal is a first-class output: a call site that can't be captured
|
|
12
|
+
* or replayed (streaming, dynamically-assembled prompts, or a route that does
|
|
13
|
+
* not pin the original model) is REFUSED, not forced — those sites are T0/T1
|
|
14
|
+
* territory per the spec's stated limits.
|
|
15
|
+
*/
|
|
16
|
+
import type { CandidateSpec } from "../types/route";
|
|
17
|
+
/** One recorded original invocation: the extracted template variables that
|
|
18
|
+
* reconstruct the original prompt through the drafted route, and the original
|
|
19
|
+
* call's raw output text. */
|
|
20
|
+
export interface ShadowCapture {
|
|
21
|
+
/** The extracted template variables (Tier-2 turns a prompt into
|
|
22
|
+
* `systemTemplate` + variables); replayed through the route unchanged. */
|
|
23
|
+
readonly input: Record<string, unknown>;
|
|
24
|
+
/** The original call site's raw output text, verbatim. */
|
|
25
|
+
readonly output: string;
|
|
26
|
+
}
|
|
27
|
+
/** A call site (or a single input) that could NOT be captured/replayed.
|
|
28
|
+
* Refusing IS a correct output — these are T0/T1 territory, never a failure. */
|
|
29
|
+
export interface CaptureRefusal {
|
|
30
|
+
readonly reason: "streaming" | "dynamic-prompt" | "uncapturable";
|
|
31
|
+
readonly detail: string;
|
|
32
|
+
}
|
|
33
|
+
/** The portable capture fixture the skill writes and the verifier reads. Stays
|
|
34
|
+
* local (SQLite/JSON in the SDK process) — nothing here uploads. */
|
|
35
|
+
export interface ShadowCaptureFixture {
|
|
36
|
+
/** Where the original call lives, e.g. "src/pipeline/summarize.ts:42". */
|
|
37
|
+
readonly callSiteId: string;
|
|
38
|
+
/** The provider+model of the ORIGINAL call — the route MUST pin exactly this
|
|
39
|
+
* (same-model replacement; the verifier refuses otherwise). */
|
|
40
|
+
readonly original: CandidateSpec;
|
|
41
|
+
readonly capturedAt: string;
|
|
42
|
+
readonly samples: readonly ShadowCapture[];
|
|
43
|
+
/** Inputs/sites the capture step honestly declined (streaming, dynamic). */
|
|
44
|
+
readonly refusals?: readonly CaptureRefusal[];
|
|
45
|
+
/** JSON-pointer paths (exact, or subtree prefixes) whose value is allowed to
|
|
46
|
+
* differ without counting as a divergence — the migration's declared
|
|
47
|
+
* free-text / nondeterministic fields (e.g. "/summary"). Absent = every
|
|
48
|
+
* field must match (the conservative default). */
|
|
49
|
+
readonly nondeterministicPaths?: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
/** Per-sample verdict — the honest three-way vocabulary. */
|
|
52
|
+
export type SampleVerdict = "equivalent" | "equivalent-modulo-nondeterminism" | "divergent";
|
|
53
|
+
/** Whole-run verdict: the per-sample vocabulary plus `refused` (the run never
|
|
54
|
+
* ran — pin violation, or nothing capturable). */
|
|
55
|
+
export type EquivalenceVerdict = SampleVerdict | "refused";
|
|
56
|
+
/** The result of diffing one original/routed output pair. */
|
|
57
|
+
export interface SampleDiff {
|
|
58
|
+
readonly verdict: SampleVerdict;
|
|
59
|
+
/** First differing structured field (JSON-pointer) when divergent on
|
|
60
|
+
* structured output; null when equivalent, modulo, or divergent for a
|
|
61
|
+
* non-field reason (bad JSON, plumbing failure, model mismatch). */
|
|
62
|
+
readonly firstDivergentPath: string | null;
|
|
63
|
+
readonly detail: string;
|
|
64
|
+
}
|
|
65
|
+
export interface SampleReport extends SampleDiff {
|
|
66
|
+
readonly index: number;
|
|
67
|
+
/** "provider/model" that actually served the replay — null when the replay
|
|
68
|
+
* itself failed before any model served. */
|
|
69
|
+
readonly servedModel: string | null;
|
|
70
|
+
readonly costUsd: number;
|
|
71
|
+
/** Set when the routed replay threw a typed failure (the plumbing could not
|
|
72
|
+
* produce a conformant output for this input) — itself a divergence. */
|
|
73
|
+
readonly failureClass: string | null;
|
|
74
|
+
}
|
|
75
|
+
export interface EquivalenceReport {
|
|
76
|
+
readonly schemaVersion: 1;
|
|
77
|
+
readonly kind: "shadow-equivalence";
|
|
78
|
+
readonly callSiteId: string;
|
|
79
|
+
readonly route: string;
|
|
80
|
+
readonly routeVersion: number;
|
|
81
|
+
/** "provider/model" the route pins — equal to the original by construction
|
|
82
|
+
* (the verifier refuses when it isn't). */
|
|
83
|
+
readonly pinnedModel: string;
|
|
84
|
+
readonly ranAt: string;
|
|
85
|
+
readonly n: number;
|
|
86
|
+
readonly overall: EquivalenceVerdict;
|
|
87
|
+
readonly counts: {
|
|
88
|
+
readonly equivalent: number;
|
|
89
|
+
readonly equivalentModulo: number;
|
|
90
|
+
readonly divergent: number;
|
|
91
|
+
};
|
|
92
|
+
readonly samples: readonly SampleReport[];
|
|
93
|
+
/** Capture-time refusals, surfaced verbatim (never a silent subset). */
|
|
94
|
+
readonly refusals: readonly CaptureRefusal[];
|
|
95
|
+
/** Set when `overall === "refused"` — why the run never ran. */
|
|
96
|
+
readonly refusalReason: string | null;
|
|
97
|
+
/** The plumbing-not-model-choice statement, embedded in the report so a PR
|
|
98
|
+
* reader cannot mistake equivalence for a model-quality claim. */
|
|
99
|
+
readonly guarantee: string;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/verify/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD;;6BAE6B;AAC7B,MAAM,WAAW,aAAa;IAC5B;8EAC0E;IAC1E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;gFACgF;AAChF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,gBAAgB,GAAG,cAAc,CAAC;IACjE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;oEACoE;AACpE,MAAM,WAAW,oBAAoB;IACnC,0EAA0E;IAC1E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;mEAC+D;IAC/D,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,CAAC;IAC3C,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAC9C;;;sDAGkD;IAClD,QAAQ,CAAC,qBAAqB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpD;AAED,4DAA4D;AAC5D,MAAM,MAAM,aAAa,GACrB,YAAY,GACZ,kCAAkC,GAClC,WAAW,CAAC;AAEhB;kDACkD;AAClD,MAAM,MAAM,kBAAkB,GAAG,aAAa,GAAG,SAAS,CAAC;AAE3D,6DAA6D;AAC7D,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC;;wEAEoE;IACpE,QAAQ,CAAC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;gDAC4C;IAC5C,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;4EACwE;IACxE,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;+CAC2C;IAC3C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE;QACf,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;QAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;KAC5B,CAAC;IACF,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IAC1C,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAC;IAC7C,gEAAgE;IAChE,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC;sEACkE;IAClE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shadow-equivalence verifier contracts (Expansion §9, autonomy-ladder Tier 2).
|
|
4
|
+
*
|
|
5
|
+
* The verifier proves a SAME-MODEL call replacement is behavior-preserving
|
|
6
|
+
* BEFORE a human merges the Tier-2 PR: capture N invocations of the original
|
|
7
|
+
* call site's inputs/outputs locally, replay the SAME inputs through the
|
|
8
|
+
* ModelRig-routed replacement (same provider/model PINNED — this verifies the
|
|
9
|
+
* PLUMBING, never the model choice), diff the outputs, and emit a report the
|
|
10
|
+
* skill embeds in the PR.
|
|
11
|
+
*
|
|
12
|
+
* Honest refusal is a first-class output: a call site that can't be captured
|
|
13
|
+
* or replayed (streaming, dynamically-assembled prompts, or a route that does
|
|
14
|
+
* not pin the original model) is REFUSED, not forced — those sites are T0/T1
|
|
15
|
+
* territory per the spec's stated limits.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow-equivalence orchestrator (Expansion §9, Tier 2) — the replay-and-diff
|
|
3
|
+
* pass that proves a same-model call replacement is behavior-preserving before
|
|
4
|
+
* a human merges the Tier-2 PR.
|
|
5
|
+
*
|
|
6
|
+
* Reuse, not twin (dev-rule S1): the EXECUTOR is the run loop — the same
|
|
7
|
+
* verified pipeline `rig.run()` drives — and the replay POSTURE is the
|
|
8
|
+
* bake-off's (`bakeoff/replay.ts`): captures severed (no capture-of-verify),
|
|
9
|
+
* one hard envelope across the pass, per-sample cost reconstructed from the
|
|
10
|
+
* tagged telemetry rows. What differs, and why this is a sibling of
|
|
11
|
+
* `replayVariant` rather than a call into it: shadow-equivalence pins a
|
|
12
|
+
* SPECIFIC captured input (not "last N by route"), needs the routed OUTPUT for
|
|
13
|
+
* a pairwise diff (not an aggregate metric), and asserts the served model
|
|
14
|
+
* equals the pinned original (a plumbing check variant comparison never makes).
|
|
15
|
+
*/
|
|
16
|
+
import type { RunDeps } from "../run/run";
|
|
17
|
+
import type { EquivalenceReport, ShadowCaptureFixture } from "./types";
|
|
18
|
+
export interface VerifySwapRequest {
|
|
19
|
+
/** The drafted replacement route (must pin the original model — see below). */
|
|
20
|
+
readonly route: string;
|
|
21
|
+
/** The local capture fixture (original inputs/outputs). Never uploaded. */
|
|
22
|
+
readonly fixture: ShadowCaptureFixture;
|
|
23
|
+
/** Hard cap for the whole replay pass (default $10). */
|
|
24
|
+
readonly envelopeUsd?: number;
|
|
25
|
+
/** Injectable clock for a deterministic `ranAt` (tests). */
|
|
26
|
+
readonly now?: () => string;
|
|
27
|
+
}
|
|
28
|
+
export declare function verifySwap(deps: RunDeps, req: VerifySwapRequest): Promise<EquivalenceReport>;
|
|
29
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/verify/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAK1C,OAAO,KAAK,EACV,iBAAiB,EAIjB,oBAAoB,EACrB,MAAM,SAAS,CAAC;AAWjB,MAAM,WAAW,iBAAiB;IAChC,+EAA+E;IAC/E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAC;IACvC,wDAAwD;IACxD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,4DAA4D;IAC5D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAiCD,wBAAsB,UAAU,CAC9B,IAAI,EAAE,OAAO,EACb,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,iBAAiB,CAAC,CAsH5B"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shadow-equivalence orchestrator (Expansion §9, Tier 2) — the replay-and-diff
|
|
4
|
+
* pass that proves a same-model call replacement is behavior-preserving before
|
|
5
|
+
* a human merges the Tier-2 PR.
|
|
6
|
+
*
|
|
7
|
+
* Reuse, not twin (dev-rule S1): the EXECUTOR is the run loop — the same
|
|
8
|
+
* verified pipeline `rig.run()` drives — and the replay POSTURE is the
|
|
9
|
+
* bake-off's (`bakeoff/replay.ts`): captures severed (no capture-of-verify),
|
|
10
|
+
* one hard envelope across the pass, per-sample cost reconstructed from the
|
|
11
|
+
* tagged telemetry rows. What differs, and why this is a sibling of
|
|
12
|
+
* `replayVariant` rather than a call into it: shadow-equivalence pins a
|
|
13
|
+
* SPECIFIC captured input (not "last N by route"), needs the routed OUTPUT for
|
|
14
|
+
* a pairwise diff (not an aggregate metric), and asserts the served model
|
|
15
|
+
* equals the pinned original (a plumbing check variant comparison never makes).
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.verifySwap = verifySwap;
|
|
19
|
+
const run_1 = require("../run/run");
|
|
20
|
+
const failures_1 = require("../types/failures");
|
|
21
|
+
const diff_1 = require("./diff");
|
|
22
|
+
/** One hard envelope for the whole verification pass when the caller sets none
|
|
23
|
+
* — replay is real, billed dispatch and must stay bounded (spec §9). */
|
|
24
|
+
const DEFAULT_VERIFY_ENVELOPE_USD = 10;
|
|
25
|
+
const GUARANTEE = "Same provider/model pinned on both sides: this verifies the PLUMBING of the " +
|
|
26
|
+
"ModelRig-routed replacement, never the model choice. Model optimization is a " +
|
|
27
|
+
"separate, human-gated bake-off decision.";
|
|
28
|
+
function candidateEquals(a, b) {
|
|
29
|
+
return a.provider === b.provider && a.model === b.model;
|
|
30
|
+
}
|
|
31
|
+
/** Build a refused report — the run never ran (pin violation, or nothing to
|
|
32
|
+
* replay). Refusing IS a correct output; it costs nothing. */
|
|
33
|
+
function refused(req, routeVersion, pinnedModel, reason, nowIso) {
|
|
34
|
+
return {
|
|
35
|
+
schemaVersion: 1,
|
|
36
|
+
kind: "shadow-equivalence",
|
|
37
|
+
callSiteId: req.fixture.callSiteId,
|
|
38
|
+
route: req.route,
|
|
39
|
+
routeVersion,
|
|
40
|
+
pinnedModel,
|
|
41
|
+
ranAt: nowIso,
|
|
42
|
+
n: 0,
|
|
43
|
+
overall: "refused",
|
|
44
|
+
counts: { equivalent: 0, equivalentModulo: 0, divergent: 0 },
|
|
45
|
+
samples: [],
|
|
46
|
+
refusals: req.fixture.refusals ?? [],
|
|
47
|
+
refusalReason: reason,
|
|
48
|
+
guarantee: GUARANTEE,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function verifySwap(deps, req) {
|
|
52
|
+
const nowIso = (req.now ?? (() => new Date().toISOString()))();
|
|
53
|
+
const bundle = deps.routes.get(req.route);
|
|
54
|
+
const original = req.fixture.original;
|
|
55
|
+
const pinnedModel = `${original.provider}/${original.model}`;
|
|
56
|
+
// Same-model pin (the C2-exit gateway-substitution ruling): the route must
|
|
57
|
+
// pin EXACTLY the original model and nothing else. A route that offers other
|
|
58
|
+
// candidates would introduce model choice — which is not what this verifies.
|
|
59
|
+
const pins = bundle.candidates.length > 0 &&
|
|
60
|
+
bundle.candidates.every((c) => candidateEquals(c, original));
|
|
61
|
+
if (!pins) {
|
|
62
|
+
const declared = bundle.candidates.map((c) => `${c.provider}/${c.model}`).join(", ") || "none";
|
|
63
|
+
return refused(req, bundle.version, pinnedModel, `route "${req.route}" declares candidates [${declared}]; shadow-equivalence requires it to ` +
|
|
64
|
+
`pin exactly the original model ${pinnedModel} — this verifies plumbing, not model choice. ` +
|
|
65
|
+
"A multi-model route is a bake-off, not a Tier-2 replacement.", nowIso);
|
|
66
|
+
}
|
|
67
|
+
if (req.fixture.samples.length === 0) {
|
|
68
|
+
const hadRefusals = (req.fixture.refusals ?? []).length > 0;
|
|
69
|
+
return refused(req, bundle.version, pinnedModel, hadRefusals
|
|
70
|
+
? "no replayable captures — every invocation was honestly refused (streaming / dynamic prompt); " +
|
|
71
|
+
"this call site is T0/T1 territory, not a Tier-2 replacement"
|
|
72
|
+
: "no captures to replay — capture N original invocations first", nowIso);
|
|
73
|
+
}
|
|
74
|
+
// Replay posture: sever captures (no capture-of-verify) and share one hard
|
|
75
|
+
// envelope, exactly as bakeoff/replay.ts does.
|
|
76
|
+
const envelopeUsd = req.envelopeUsd ?? DEFAULT_VERIFY_ENVELOPE_USD;
|
|
77
|
+
const replayDeps = { ...deps, captures: null, envelopeBudgetUsd: envelopeUsd };
|
|
78
|
+
const envelopeKey = `verify:${req.route}:${nowIso}`;
|
|
79
|
+
const nd = req.fixture.nondeterministicPaths ?? [];
|
|
80
|
+
const samples = [];
|
|
81
|
+
for (let index = 0; index < req.fixture.samples.length; index += 1) {
|
|
82
|
+
const capture = req.fixture.samples[index];
|
|
83
|
+
const sampleTag = `${envelopeKey}#${index}`;
|
|
84
|
+
let servedModel = null;
|
|
85
|
+
let failureClass = null;
|
|
86
|
+
let diff = { verdict: "divergent", firstDivergentPath: null, detail: "" };
|
|
87
|
+
try {
|
|
88
|
+
const result = await (0, run_1.run)(replayDeps, req.route, {
|
|
89
|
+
input: capture.input,
|
|
90
|
+
tags: { shadow_verify: envelopeKey, shadow_verify_sample: sampleTag },
|
|
91
|
+
budget: { envelope: envelopeKey, budgetUsd: envelopeUsd },
|
|
92
|
+
});
|
|
93
|
+
servedModel = `${result.meta.provider}/${result.meta.model}`;
|
|
94
|
+
if (servedModel !== pinnedModel) {
|
|
95
|
+
// A declared candidate served something other than the original — the
|
|
96
|
+
// plumbing does not preserve the model. Divergent, not equivalent.
|
|
97
|
+
diff = {
|
|
98
|
+
verdict: "divergent",
|
|
99
|
+
firstDivergentPath: null,
|
|
100
|
+
detail: `routed run served ${servedModel}, not the pinned original ${pinnedModel} — a fallback fired`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const routedText = JSON.stringify(result.output);
|
|
105
|
+
diff = (0, diff_1.diffOutputs)(capture.output, result.output, routedText, nd);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
if (!(err instanceof failures_1.RigFailureError))
|
|
110
|
+
throw err;
|
|
111
|
+
failureClass = err.failure.class;
|
|
112
|
+
diff = {
|
|
113
|
+
verdict: "divergent",
|
|
114
|
+
firstDivergentPath: null,
|
|
115
|
+
detail: `routed replacement did not return a conformant output (${err.failure.class}): ` +
|
|
116
|
+
`${err.failure.message} — the plumbing regressed for this input`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// Per-sample cost from the tagged rows — base + retries + repair, exactly
|
|
120
|
+
// what telemetry billed (the bake-off's F1-correct accounting).
|
|
121
|
+
const costUsd = deps.telemetry
|
|
122
|
+
.queryByTag("shadow_verify_sample", sampleTag)
|
|
123
|
+
.reduce((a, r) => a + r.costEstimateUsd, 0);
|
|
124
|
+
samples.push({ index, ...diff, servedModel, costUsd, failureClass });
|
|
125
|
+
}
|
|
126
|
+
const counts = {
|
|
127
|
+
equivalent: samples.filter((s) => s.verdict === "equivalent").length,
|
|
128
|
+
equivalentModulo: samples.filter((s) => s.verdict === "equivalent-modulo-nondeterminism").length,
|
|
129
|
+
divergent: samples.filter((s) => s.verdict === "divergent").length,
|
|
130
|
+
};
|
|
131
|
+
const overall = counts.divergent > 0 ? "divergent" : counts.equivalentModulo > 0 ? "equivalent-modulo-nondeterminism" : "equivalent";
|
|
132
|
+
return {
|
|
133
|
+
schemaVersion: 1,
|
|
134
|
+
kind: "shadow-equivalence",
|
|
135
|
+
callSiteId: req.fixture.callSiteId,
|
|
136
|
+
route: req.route,
|
|
137
|
+
routeVersion: bundle.version,
|
|
138
|
+
pinnedModel,
|
|
139
|
+
ranAt: nowIso,
|
|
140
|
+
n: samples.length,
|
|
141
|
+
overall,
|
|
142
|
+
counts,
|
|
143
|
+
samples,
|
|
144
|
+
refusals: req.fixture.refusals ?? [],
|
|
145
|
+
refusalReason: null,
|
|
146
|
+
guarantee: GUARANTEE,
|
|
147
|
+
};
|
|
148
|
+
}
|