@crewhaus/grader-provability 0.0.0-stage → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +98 -2
- package/dist/cost.d.ts +66 -0
- package/dist/cost.js +91 -0
- package/dist/graders.d.ts +149 -0
- package/dist/graders.js +435 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +65 -0
- package/dist/metadata.d.ts +47 -0
- package/dist/metadata.js +71 -0
- package/dist/summary.d.ts +77 -0
- package/dist/summary.js +114 -0
- package/package.json +45 -4
package/dist/graders.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { claimReasons, levelRank, maxRung, meetsFloor, } from "@crewhaus/provability";
|
|
2
|
+
import { gateSpendOf } from "./cost";
|
|
3
|
+
import { expectedSourceKey, sampleProvabilityOf } from "./metadata";
|
|
4
|
+
/** The release thresholds (design §9.1's table). */
|
|
5
|
+
export const PROVABILITY_METRIC_THRESHOLDS = Object.freeze({
|
|
6
|
+
/** met / (claims − assumptions) at the floor. */
|
|
7
|
+
claimCoverage: 0.95,
|
|
8
|
+
/** defined markers / markers. */
|
|
9
|
+
citationValidity: 0.99,
|
|
10
|
+
/** declared assumptions / claims — an UPPER bound. */
|
|
11
|
+
assumptionRate: 0.1,
|
|
12
|
+
/** matched replays / replays — every replay must match. */
|
|
13
|
+
replayIntegrity: 1,
|
|
14
|
+
});
|
|
15
|
+
/** The reason a provability grader returns when the run carried no bundle. */
|
|
16
|
+
export const NO_PROOF_BUNDLE = "no proof bundle (is provability: declared?)";
|
|
17
|
+
/** The pinned prefix of the result for a bundle whose verifier threw (§5.1 fail-open). */
|
|
18
|
+
export const UNVERIFIED_BUNDLE = "the gate could not verify this answer";
|
|
19
|
+
/** The pinned prefix of the result for a malformed `metadata.provability`. */
|
|
20
|
+
export const INVALID_SAMPLE_METADATA = "invalid metadata.provability";
|
|
21
|
+
/** The pinned prefix of a floor-dependent result whose floor is above the bundle's. */
|
|
22
|
+
export const FLOOR_ABOVE_BUNDLE = "not gradable at this floor";
|
|
23
|
+
/**
|
|
24
|
+
* Every prefix a result carries when it REPLACES a measurement (no score was
|
|
25
|
+
* measured; `score` is a placeholder 0). A roll-up keeps these out of a
|
|
26
|
+
* lower-is-better mean, where a placeholder 0 would read as the best value.
|
|
27
|
+
*/
|
|
28
|
+
export const UNMEASURED_PREFIXES = Object.freeze([
|
|
29
|
+
NO_PROOF_BUNDLE,
|
|
30
|
+
UNVERIFIED_BUNDLE,
|
|
31
|
+
INVALID_SAMPLE_METADATA,
|
|
32
|
+
FLOOR_ABOVE_BUNDLE,
|
|
33
|
+
]);
|
|
34
|
+
/** Whether a provability grade replaced its measurement with a failure (see {@link UNMEASURED_PREFIXES}). */
|
|
35
|
+
export function isUnmeasured(result) {
|
|
36
|
+
return UNMEASURED_PREFIXES.some((prefix) => result.rationale.startsWith(prefix));
|
|
37
|
+
}
|
|
38
|
+
/** The pinned prefix of an infinite `costPerProvenClaim` rationale (see the grader). */
|
|
39
|
+
export const INFINITE_COST_PREFIX = "∞ per proven claim";
|
|
40
|
+
/** The pinned prefix of a `costPerProvenClaim` whose spend is a lower bound (see the grader). */
|
|
41
|
+
export const UNPRICED_COST_PREFIX = "≥ (partly unpriced) per proven claim";
|
|
42
|
+
const fail = (rationale) => ({
|
|
43
|
+
result: { passed: false, score: 0, rationale },
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* The bundle and floor a grader measures, or the failing result that replaces
|
|
47
|
+
* the measurement. `floorDependent` graders also refuse a floor above the
|
|
48
|
+
* bundle's (see the module doc).
|
|
49
|
+
*/
|
|
50
|
+
function standing(sample, run, floorDependent) {
|
|
51
|
+
const meta = sampleProvabilityOf(sample);
|
|
52
|
+
// `meta.error` names the field: "metadata.provability… must be …".
|
|
53
|
+
if (!meta.ok)
|
|
54
|
+
return fail(`invalid ${meta.error}`);
|
|
55
|
+
const bundle = run.artifacts?.proof;
|
|
56
|
+
if (bundle === undefined) {
|
|
57
|
+
return fail(`${NO_PROOF_BUNDLE} — this sample carries no provability verdict: the run was not gated (declare provability: on the spec, or run \`crewhaus eval --provability <level>\`), or the sample ended before the gate graded an answer`);
|
|
58
|
+
}
|
|
59
|
+
if (bundle.verdict === "unverified") {
|
|
60
|
+
return fail(`${UNVERIFIED_BUNDLE} (${bundle.error ?? "the verifier failed"}) — it stood unverified, so nothing was graded`);
|
|
61
|
+
}
|
|
62
|
+
const floor = meta.value.level ?? bundle.level;
|
|
63
|
+
if (floorDependent && levelRank(floor) > levelRank(bundle.level)) {
|
|
64
|
+
return fail(`${FLOOR_ABOVE_BUNDLE}: the sample's ${floor} floor is above the ${bundle.level} floor the gate graded at, and rungs above a gate's floor are recorded without the checks only those floors run — gate the sample at ${floor} (the eval runner does, per sample) for a grade`);
|
|
65
|
+
}
|
|
66
|
+
return { bundle, floor, meta: meta.value };
|
|
67
|
+
}
|
|
68
|
+
/** The claims that count toward the score: everything but accepted assumptions. */
|
|
69
|
+
function counted(bundle) {
|
|
70
|
+
return bundle.claims.filter((c) => c.status !== "assumption");
|
|
71
|
+
}
|
|
72
|
+
/** The highest floor one recorded claim would meet (see the module doc). */
|
|
73
|
+
function claimCeiling(claim) {
|
|
74
|
+
if (claim.bindings.some((b) => b.reason === "no_definition"))
|
|
75
|
+
return "none";
|
|
76
|
+
return maxRung(...claim.bindings.filter((b) => b.kind !== "assumption").map((b) => b.rung));
|
|
77
|
+
}
|
|
78
|
+
/** Does `claim` meet `floor`? Exact at the bundle's own floor, re-read from rungs elsewhere. */
|
|
79
|
+
function claimMeets(bundle, claim, floor) {
|
|
80
|
+
if (floor === bundle.level)
|
|
81
|
+
return claim.status === "met";
|
|
82
|
+
if (claim.status === "assumption")
|
|
83
|
+
return false;
|
|
84
|
+
return meetsFloor(claimCeiling(claim), floor);
|
|
85
|
+
}
|
|
86
|
+
/** met / counted at `floor`; 1 for the vacuous zero-counted answer. */
|
|
87
|
+
function coverageAt(bundle, floor) {
|
|
88
|
+
const claims = counted(bundle);
|
|
89
|
+
if (floor === bundle.level) {
|
|
90
|
+
return { met: bundle.met, counted: claims.length, score: bundle.score };
|
|
91
|
+
}
|
|
92
|
+
const met = claims.filter((c) => claimMeets(bundle, c, floor)).length;
|
|
93
|
+
return { met, counted: claims.length, score: claims.length === 0 ? 1 : met / claims.length };
|
|
94
|
+
}
|
|
95
|
+
/** `reason ×n` tally over the claims that miss `floor`, most frequent first. */
|
|
96
|
+
function reasonTally(bundle, floor) {
|
|
97
|
+
const tally = new Map();
|
|
98
|
+
for (const claim of counted(bundle)) {
|
|
99
|
+
if (claimMeets(bundle, claim, floor))
|
|
100
|
+
continue;
|
|
101
|
+
const reasons = claimReasons(claim.status === "unmet" ? claim : { ...claim, status: "unmet" });
|
|
102
|
+
for (const reason of reasons.length > 0 ? reasons : ["below_floor"]) {
|
|
103
|
+
tally.set(reason, (tally.get(reason) ?? 0) + 1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return [...tally]
|
|
107
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
108
|
+
.map(([reason, n]) => (n > 1 ? `${reason} ×${n}` : reason))
|
|
109
|
+
.join(", ");
|
|
110
|
+
}
|
|
111
|
+
/** The highest rung a source binding can reach (design §3: a source grounds; it never checks). */
|
|
112
|
+
const SOURCE_CEILING = "grounded";
|
|
113
|
+
/**
|
|
114
|
+
* Whether a source binding grounded the answer at `floor`. At `cited` /
|
|
115
|
+
* `grounded` that is the floor itself — the gate's own `ok` at its floor
|
|
116
|
+
* (which also applies `grounded.independent`), the recorded rung elsewhere.
|
|
117
|
+
* Above `grounded` no source is ever `ok` (`below_floor`: a figure at
|
|
118
|
+
* `checked` needs a tool), yet a source the claim was grounded in still
|
|
119
|
+
* grounded the answer, so the bar there is the source ceiling, `grounded`.
|
|
120
|
+
*/
|
|
121
|
+
function sourceGrounds(bundle, b, floor) {
|
|
122
|
+
if (levelRank(floor) > levelRank(SOURCE_CEILING))
|
|
123
|
+
return meetsFloor(b.rung, SOURCE_CEILING);
|
|
124
|
+
return floor === bundle.level ? b.ok : meetsFloor(b.rung, floor);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Every source origin that grounded the answer at `floor` (see
|
|
128
|
+
* {@link sourceGrounds}) in text the run did not write there itself: an
|
|
129
|
+
* expected source is one the answer was meant to READ, and a binding whose
|
|
130
|
+
* grounding text an earlier call of the run wrote to that origin
|
|
131
|
+
* (`writtenBy`, design §10) shows the run authored it instead.
|
|
132
|
+
*/
|
|
133
|
+
function okSourceOrigins(bundle, floor) {
|
|
134
|
+
const out = new Set();
|
|
135
|
+
for (const claim of bundle.claims) {
|
|
136
|
+
for (const b of claim.bindings) {
|
|
137
|
+
if (b.kind !== "source" || b.origin === undefined || b.writtenBy !== undefined)
|
|
138
|
+
continue;
|
|
139
|
+
if (sourceGrounds(bundle, b, floor))
|
|
140
|
+
out.add(b.origin);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
/** Every source origin a binding grounded in text the run wrote there itself (`writtenBy`). */
|
|
146
|
+
function selfWrittenOrigins(bundle) {
|
|
147
|
+
const out = new Set();
|
|
148
|
+
for (const claim of bundle.claims) {
|
|
149
|
+
for (const b of claim.bindings) {
|
|
150
|
+
if (b.kind === "source" && b.origin !== undefined && b.writtenBy !== undefined) {
|
|
151
|
+
out.add(b.origin);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
/** The `expected_sources` no ok binding grounded, as written. */
|
|
158
|
+
function missingSources(bundle, floor, expected) {
|
|
159
|
+
const origins = okSourceOrigins(bundle, floor);
|
|
160
|
+
return expected.filter((source) => !origins.has(expectedSourceKey(source)));
|
|
161
|
+
}
|
|
162
|
+
const plural = (n, one, many = `${one}s`) => (n === 1 ? one : many);
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// 1. provability.claimCoverage
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
/**
|
|
167
|
+
* met / (claims − assumptions) at the floor, the gate's own score when the
|
|
168
|
+
* floor is the run's. Passes at ≥ 0.95 AND when every
|
|
169
|
+
* `metadata.provability.expected_sources` origin grounded the answer — an ok
|
|
170
|
+
* source binding, or at `checked` / `proven` (where a source is never `ok`)
|
|
171
|
+
* one that reached `grounded` (see `sourceGrounds`). A missing one fails the
|
|
172
|
+
* sample; the score stays the coverage, the rationale names the source.
|
|
173
|
+
*/
|
|
174
|
+
export const claimCoverage = async (sample, run) => {
|
|
175
|
+
const s = standing(sample, run, true);
|
|
176
|
+
if ("result" in s)
|
|
177
|
+
return s.result;
|
|
178
|
+
const { bundle, floor } = s;
|
|
179
|
+
const { met, counted: n, score } = coverageAt(bundle, floor);
|
|
180
|
+
const threshold = PROVABILITY_METRIC_THRESHOLDS.claimCoverage;
|
|
181
|
+
const missing = missingSources(bundle, floor, s.meta.expectedSources ?? []);
|
|
182
|
+
const parts = [
|
|
183
|
+
n === 0
|
|
184
|
+
? `no claims to prove at the ${floor} floor (vacuous: ${bundle.vacuous ?? "no counted claims"})`
|
|
185
|
+
: `${met}/${n} ${plural(n, "claim")} meet the ${floor} floor (threshold ≥${threshold})`,
|
|
186
|
+
];
|
|
187
|
+
const tally = reasonTally(bundle, floor);
|
|
188
|
+
if (tally !== "")
|
|
189
|
+
parts.push(`unmet: ${tally}`);
|
|
190
|
+
if (bundle.assumptions > 0) {
|
|
191
|
+
parts.push(`${bundle.assumptions} ${plural(bundle.assumptions, "assumption")} excluded`);
|
|
192
|
+
}
|
|
193
|
+
// Design §10: a source the run wrote itself before reading it back still
|
|
194
|
+
// grounds at the gate; the grader names it, so a sample graded on such a
|
|
195
|
+
// run says so.
|
|
196
|
+
const own = selfWrittenOrigins(bundle);
|
|
197
|
+
if (own.size > 0) {
|
|
198
|
+
parts.push(`${own.size} ${plural(own.size, "source")} the run wrote itself before reading ${own.size === 1 ? "it" : "them"} back: ${[...own].map((o) => JSON.stringify(o)).join(", ")}`);
|
|
199
|
+
}
|
|
200
|
+
if (missing.length > 0) {
|
|
201
|
+
parts.push(`expected source(s) not grounding the answer: ${missing
|
|
202
|
+
.map((m) => `${JSON.stringify(m)}${own.has(expectedSourceKey(m)) ? " (the run wrote it itself)" : ""}`)
|
|
203
|
+
.join(", ")}`);
|
|
204
|
+
}
|
|
205
|
+
return { passed: score >= threshold && missing.length === 0, score, rationale: parts.join("; ") };
|
|
206
|
+
};
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// 2. provability.rungAchieved
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
/**
|
|
211
|
+
* The highest floor the answer would have passed (`bundle.achieved`, the
|
|
212
|
+
* gate ran every rung for it), normalized 0..1 as rank/4 — a per-sample
|
|
213
|
+
* ladder score that shows HOW FAR an answer falls short. Passes when the
|
|
214
|
+
* achieved rung is at or above the floor.
|
|
215
|
+
*/
|
|
216
|
+
export const rungAchieved = async (sample, run) => {
|
|
217
|
+
const s = standing(sample, run, true);
|
|
218
|
+
if ("result" in s)
|
|
219
|
+
return s.result;
|
|
220
|
+
const { bundle, floor } = s;
|
|
221
|
+
const rank = levelRank(bundle.achieved);
|
|
222
|
+
const passed = rank >= levelRank(floor);
|
|
223
|
+
return {
|
|
224
|
+
passed,
|
|
225
|
+
score: rank / 4,
|
|
226
|
+
rationale: `achieved ${bundle.achieved} (rung ${rank}/4) against the ${floor} floor — ${passed
|
|
227
|
+
? "at or above it"
|
|
228
|
+
: `${levelRank(floor) - rank} ${plural(levelRank(floor) - rank, "rung")} short`}`,
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// 3. provability.citationValidity
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
/**
|
|
235
|
+
* defined markers / markers — the `cited` sub-check alone: every `[n]` in a
|
|
236
|
+
* claim needs its `[n]: …` definition. One binding is one marker occurrence;
|
|
237
|
+
* a marker with no definition is a binding whose reason is `no_definition`.
|
|
238
|
+
* No markers at all is a vacuous pass (claimCoverage is what fails an
|
|
239
|
+
* uncited answer). Passes at ≥ 0.99.
|
|
240
|
+
*
|
|
241
|
+
* Design §9.1 counts the markers the ANSWER wrote. A claim carrying
|
|
242
|
+
* `definition` is text a definition line shows after what it names, read
|
|
243
|
+
* as citing that definition: its one binding is the verifier's synthetic
|
|
244
|
+
* self-binding, defined by construction and never typed as a marker. So it
|
|
245
|
+
* is skipped — counted, a gloss sentence (or a long bibliography) would add
|
|
246
|
+
* a defined marker to both sides and lift a dangling-marker answer over the
|
|
247
|
+
* bar.
|
|
248
|
+
*/
|
|
249
|
+
export const citationValidity = async (sample, run) => {
|
|
250
|
+
const s = standing(sample, run, false);
|
|
251
|
+
if ("result" in s)
|
|
252
|
+
return s.result;
|
|
253
|
+
const { bundle } = s;
|
|
254
|
+
let markers = 0;
|
|
255
|
+
let dangling = 0;
|
|
256
|
+
const undefinedRefs = [];
|
|
257
|
+
for (const claim of bundle.claims) {
|
|
258
|
+
if (claim.definition !== undefined)
|
|
259
|
+
continue;
|
|
260
|
+
for (const b of claim.bindings) {
|
|
261
|
+
markers += 1;
|
|
262
|
+
if (b.reason === "no_definition") {
|
|
263
|
+
dangling += 1;
|
|
264
|
+
if (!undefinedRefs.includes(b.ref))
|
|
265
|
+
undefinedRefs.push(b.ref);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const threshold = PROVABILITY_METRIC_THRESHOLDS.citationValidity;
|
|
270
|
+
if (markers === 0) {
|
|
271
|
+
const uncited = bundle.claims.filter((c) => c.bindings.length === 0).length;
|
|
272
|
+
return {
|
|
273
|
+
passed: true,
|
|
274
|
+
score: 1,
|
|
275
|
+
rationale: `no citation markers to check (vacuous)${uncited > 0 ? ` — ${uncited} uncited ${plural(uncited, "claim")} (see claimCoverage)` : ""}`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const score = (markers - dangling) / markers;
|
|
279
|
+
return {
|
|
280
|
+
passed: score >= threshold,
|
|
281
|
+
score,
|
|
282
|
+
rationale: `${markers - dangling}/${markers} ${plural(markers, "marker")} defined (threshold ≥${threshold})${undefinedRefs.length > 0 ? ` — no definition for ${undefinedRefs.join(" ")}` : ""}`,
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// 4. provability.assumptionRate
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
/**
|
|
289
|
+
* declared assumptions / claims — the assumption dodge made visible. LOWER is
|
|
290
|
+
* better (the continuity.reAskRate precedent): passes at ≤ 0.1. Declared
|
|
291
|
+
* counts the accepted assumptions, the ones the config refused
|
|
292
|
+
* (`assumptions: forbid`, past `max_assumptions`) — a refused assumption is
|
|
293
|
+
* still the answer leaning on one — and the hypotheses accepted proofs rest
|
|
294
|
+
* on (`hypotheses`, design §6.4): a proof that assumes its own conclusion is
|
|
295
|
+
* not "0 assumptions". Zero claims is a vacuous 0.
|
|
296
|
+
*/
|
|
297
|
+
export const assumptionRate = async (sample, run) => {
|
|
298
|
+
const s = standing(sample, run, false);
|
|
299
|
+
if ("result" in s)
|
|
300
|
+
return s.result;
|
|
301
|
+
const { bundle } = s;
|
|
302
|
+
const refused = bundle.assumptionsRefused ?? 0;
|
|
303
|
+
const hypotheses = bundle.hypotheses ?? 0;
|
|
304
|
+
const declared = bundle.assumptions + refused + hypotheses;
|
|
305
|
+
const threshold = PROVABILITY_METRIC_THRESHOLDS.assumptionRate;
|
|
306
|
+
if (bundle.claimCount === 0) {
|
|
307
|
+
return { passed: true, score: 0, rationale: "no claims (assumption rate 0, vacuous)" };
|
|
308
|
+
}
|
|
309
|
+
const rate = declared / bundle.claimCount;
|
|
310
|
+
return {
|
|
311
|
+
passed: rate <= threshold,
|
|
312
|
+
score: rate,
|
|
313
|
+
rationale: `${declared}/${bundle.claimCount} ${plural(bundle.claimCount, "claim")} rest on a declared assumption (upper-bound ≤${threshold})${refused > 0 ? ` — ${refused} refused by the config` : ""}${hypotheses > 0
|
|
314
|
+
? ` — ${hypotheses} ${plural(hypotheses, "proof hypothesis", "proof hypotheses")} counted`
|
|
315
|
+
: ""}`,
|
|
316
|
+
};
|
|
317
|
+
};
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// 5. provability.replayIntegrity
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
/**
|
|
322
|
+
* replayed executions whose output digest matched / replayed executions.
|
|
323
|
+
* An execution is one `tool:<toolUseId>` (bindings citing the same call share
|
|
324
|
+
* its one replay). Must be exactly 1: a mismatch means a cited result is not
|
|
325
|
+
* what the tool returns for that input. None replayed is a vacuous pass —
|
|
326
|
+
* replay runs only at a `checked` floor, for pure tools, with
|
|
327
|
+
* `execution.replay` on.
|
|
328
|
+
*/
|
|
329
|
+
export const replayIntegrity = async (sample, run) => {
|
|
330
|
+
const s = standing(sample, run, true);
|
|
331
|
+
if ("result" in s)
|
|
332
|
+
return s.result;
|
|
333
|
+
const replays = new Map();
|
|
334
|
+
s.bundle.claims.forEach((claim, i) => {
|
|
335
|
+
claim.bindings.forEach((b, j) => {
|
|
336
|
+
if (b.kind !== "execution" || b.replayed !== true)
|
|
337
|
+
return;
|
|
338
|
+
const key = b.origin ?? `${i}:${j}`;
|
|
339
|
+
const matched = b.reason !== "replay_mismatch";
|
|
340
|
+
// One mismatching citation of a call is the call's verdict.
|
|
341
|
+
replays.set(key, (replays.get(key) ?? true) && matched);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
if (replays.size === 0) {
|
|
345
|
+
return {
|
|
346
|
+
passed: true,
|
|
347
|
+
score: 1,
|
|
348
|
+
rationale: "no replayed executions (vacuous) — replay runs at the checked floor, for pure tools, with execution.replay on",
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const matched = [...replays.values()].filter(Boolean).length;
|
|
352
|
+
const mismatched = [...replays].filter(([, ok]) => !ok).map(([id]) => id);
|
|
353
|
+
const score = matched / replays.size;
|
|
354
|
+
return {
|
|
355
|
+
passed: score === PROVABILITY_METRIC_THRESHOLDS.replayIntegrity,
|
|
356
|
+
score,
|
|
357
|
+
rationale: `${matched}/${replays.size} replayed ${plural(replays.size, "execution")} matched (must be 1.0)${mismatched.length > 0 ? ` — replay mismatch: ${mismatched.join(", ")}` : ""}`,
|
|
358
|
+
};
|
|
359
|
+
};
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// 6. provability.costPerProvenClaim
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
const usd = (micros) => `$${(micros / 1_000_000).toFixed(4)}`;
|
|
364
|
+
/**
|
|
365
|
+
* Gate-caused spend per met claim (see `./cost` for exactly what counts and
|
|
366
|
+
* how it is priced) — the `continuity.costPerProvenOutcome` precedent, but
|
|
367
|
+
* REPORTED, with no threshold: it never fails a measured sample (only a
|
|
368
|
+
* missing or unverified bundle does, like every grader here). Score is USD
|
|
369
|
+
* per claim met at the floor. Three outcomes are told apart by a pinned
|
|
370
|
+
* rationale prefix, which the roll-up reads:
|
|
371
|
+
* - {@link INFINITE_COST_PREFIX} — spend (priced, or tokens nothing could
|
|
372
|
+
* price) with zero met claims: the infinite ratio, kept JSON-safe as
|
|
373
|
+
* score 0;
|
|
374
|
+
* - {@link UNPRICED_COST_PREFIX} — some re-run tokens had no price, so the
|
|
375
|
+
* score is the PRICED part per met claim, a lower bound, never a $0 that
|
|
376
|
+
* reads as free;
|
|
377
|
+
* - otherwise the exact ratio.
|
|
378
|
+
*
|
|
379
|
+
* `pricing` prices re-run calls on a run no cost-tracker metered (the eval
|
|
380
|
+
* runner passes its own `RunEvalOptions.pricing`); without it those calls
|
|
381
|
+
* are unpriced.
|
|
382
|
+
*/
|
|
383
|
+
export function createCostPerProvenClaim(opts = {}) {
|
|
384
|
+
return async (sample, run) => {
|
|
385
|
+
const s = standing(sample, run, true);
|
|
386
|
+
if ("result" in s)
|
|
387
|
+
return s.result;
|
|
388
|
+
const { bundle, floor } = s;
|
|
389
|
+
const { met } = coverageAt(bundle, floor);
|
|
390
|
+
const spend = gateSpendOf(run.events, bundle, opts);
|
|
391
|
+
const breakdown = [
|
|
392
|
+
`re-runs ${usd(spend.rerunUsdMicros)} over ${spend.reruns} gate ${plural(spend.reruns, "pass", "passes")} after the first`,
|
|
393
|
+
`waiver judge ${usd(spend.judgeUsdMicros)}`,
|
|
394
|
+
...(spend.unpriced
|
|
395
|
+
? [
|
|
396
|
+
`${spend.unpricedTokens} of ${spend.rerunTokens} re-run tokens unpriced (no cost_accrual priced them, and no pricing row for their model)`,
|
|
397
|
+
]
|
|
398
|
+
: []),
|
|
399
|
+
...(spend.checks > 0
|
|
400
|
+
? [
|
|
401
|
+
`checker time ${spend.checkerMs}ms over ${spend.checks} ${plural(spend.checks, "check")}`,
|
|
402
|
+
]
|
|
403
|
+
: []),
|
|
404
|
+
].join("; ");
|
|
405
|
+
if (met === 0) {
|
|
406
|
+
// Unpriced tokens are real spend: with nothing met, the ratio is ∞.
|
|
407
|
+
return spend.totalUsdMicros > 0 || spend.unpriced
|
|
408
|
+
? {
|
|
409
|
+
passed: true,
|
|
410
|
+
score: 0,
|
|
411
|
+
rationale: `${INFINITE_COST_PREFIX} — spent ${usd(spend.totalUsdMicros)}${spend.unpriced ? ` + ${spend.unpricedTokens} unpriced tokens` : ""} with zero claims met at the ${floor} floor (${breakdown}; reported, no threshold)`,
|
|
412
|
+
}
|
|
413
|
+
: {
|
|
414
|
+
passed: true,
|
|
415
|
+
score: 0,
|
|
416
|
+
rationale: `$0.0000 per proven claim — no gate spend and no claims met at the ${floor} floor (${breakdown}; reported, no threshold)`,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const ratio = spend.totalUsdMicros / 1_000_000 / met;
|
|
420
|
+
if (spend.unpriced) {
|
|
421
|
+
return {
|
|
422
|
+
passed: true,
|
|
423
|
+
score: ratio,
|
|
424
|
+
rationale: `${UNPRICED_COST_PREFIX} — at least $${ratio.toFixed(4)} (${usd(spend.totalUsdMicros)} priced gate spend across ${met} met ${plural(met, "claim")}: ${breakdown}; reported, no threshold)`,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
passed: true,
|
|
429
|
+
score: ratio,
|
|
430
|
+
rationale: `$${ratio.toFixed(4)} per proven claim (${usd(spend.totalUsdMicros)} gate spend across ${met} met ${plural(met, "claim")}: ${breakdown}; reported, no threshold)`,
|
|
431
|
+
};
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
/** {@link createCostPerProvenClaim} with no pricing seam (the registry's instance). */
|
|
435
|
+
export const costPerProvenClaim = createCostPerProvenClaim();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@crewhaus/grader-provability` — the 0.8.0 provability release's eval layer
|
|
3
|
+
* (design/0.8.0-provability-release.md §9.1): six DETERMINISTIC graders over
|
|
4
|
+
* the proof bundle the eval runner captured for each sample
|
|
5
|
+
* (`RunResult.artifacts.proof`), following the grader-continuity /
|
|
6
|
+
* grader-12-metric-rubric conventions — one installer, per-sample scores,
|
|
7
|
+
* a cross-sample roll-up.
|
|
8
|
+
*
|
|
9
|
+
* provability.claimCoverage met / (claims − assumptions) at the floor ≥ 0.95
|
|
10
|
+
* (+ metadata.provability.expected_sources)
|
|
11
|
+
* provability.rungAchieved the highest floor passed, rank/4 ≥ the floor
|
|
12
|
+
* provability.citationValidity defined markers / markers ≥ 0.99
|
|
13
|
+
* provability.assumptionRate declared assumptions / claims (LOWER wins) ≤ 0.1
|
|
14
|
+
* provability.replayIntegrity matched replays / replays = 1.0
|
|
15
|
+
* provability.costPerProvenClaim gate-caused spend per met claim reported
|
|
16
|
+
*
|
|
17
|
+
* A sample's floor is `metadata.provability.level` when it sets one, else the
|
|
18
|
+
* bundle's (the spec's floor, or `crewhaus eval --provability <level>`). A run
|
|
19
|
+
* with no bundle fails every grader with {@link NO_PROOF_BUNDLE} — see
|
|
20
|
+
* `./graders` for why a missing verdict is never a pass, and for the other
|
|
21
|
+
* pinned "unmeasured" outcomes ({@link isUnmeasured}).
|
|
22
|
+
*
|
|
23
|
+
* Graders read only what the runner hands them; none calls a model, none
|
|
24
|
+
* re-runs the verifier, none touches the harness's live stores (Pillar 2: the
|
|
25
|
+
* eval layer measures end states, offline).
|
|
26
|
+
*/
|
|
27
|
+
import type { Grader } from "@crewhaus/eval-grader";
|
|
28
|
+
import type { GraderRegistry } from "@crewhaus/grader-registry";
|
|
29
|
+
import type { GatePricing } from "./cost";
|
|
30
|
+
export { FLOOR_ABOVE_BUNDLE, INFINITE_COST_PREFIX, INVALID_SAMPLE_METADATA, NO_PROOF_BUNDLE, PROVABILITY_METRIC_THRESHOLDS, UNMEASURED_PREFIXES, UNPRICED_COST_PREFIX, UNVERIFIED_BUNDLE, assumptionRate, citationValidity, claimCoverage, costPerProvenClaim, createCostPerProvenClaim, isUnmeasured, replayIntegrity, rungAchieved, } from "./graders";
|
|
31
|
+
export { type GatePricing, type GateSpend, gateSpendOf } from "./cost";
|
|
32
|
+
export { type SampleProvability, type SampleProvabilityRead, expectedSourceKey, sampleProvabilityOf, } from "./metadata";
|
|
33
|
+
export { PROVABILITY_METRIC_SPECS, type ProvabilityCostSummary, type ProvabilityMetricSpec, type ProvabilityMetricSummary, type ProvabilitySummary, renderProvabilitySummaryLines, summarizeCostPerProvenClaim, summarizeProvabilityMetrics, } from "./summary";
|
|
34
|
+
/** The six graders under their canonical names, in the design's table order. */
|
|
35
|
+
export declare const PROVABILITY_GRADERS: ReadonlyArray<readonly [string, Grader]>;
|
|
36
|
+
/**
|
|
37
|
+
* The six graders, with `costPerProvenClaim` pricing re-runs through
|
|
38
|
+
* `pricing` on a run no cost-tracker metered (see `./cost`). The eval runner
|
|
39
|
+
* builds its set here from its own `RunEvalOptions.pricing`; without a seam
|
|
40
|
+
* this is {@link PROVABILITY_GRADERS}.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createProvabilityGraders(opts?: {
|
|
43
|
+
readonly pricing?: GatePricing;
|
|
44
|
+
}): ReadonlyArray<readonly [string, Grader]>;
|
|
45
|
+
/**
|
|
46
|
+
* The graders that are REPORTED, never a pass/fail bar: their verdict is
|
|
47
|
+
* always a pass when measured, so a combination that let them vote would
|
|
48
|
+
* pass a sample on a dollar figure.
|
|
49
|
+
*/
|
|
50
|
+
export declare const PROVABILITY_REPORTED_GRADERS: ReadonlyArray<string>;
|
|
51
|
+
/** The canonical names, in order. */
|
|
52
|
+
export declare const PROVABILITY_GRADER_NAMES: ReadonlyArray<string>;
|
|
53
|
+
/**
|
|
54
|
+
* How one of the six joins a sample's overall grade (design §9.1), keyed on
|
|
55
|
+
* its canonical name — the one answer for every path that adds it, the
|
|
56
|
+
* `--provability` flag and a graders file's `type: registry` entry alike:
|
|
57
|
+
* - `"requirement"` — the five with a bar: the sample must pass each on top
|
|
58
|
+
* of its own `combine:` policy, never as a vote inside it (a vacuous pass
|
|
59
|
+
* or a lower-is-better rate cannot carry a `combine: any` or skew a
|
|
60
|
+
* `weighted` score);
|
|
61
|
+
* - `"report"` — {@link PROVABILITY_REPORTED_GRADERS}: recorded, never part
|
|
62
|
+
* of the verdict (more gate spend can never turn a fail into a pass).
|
|
63
|
+
* `undefined` for any other name.
|
|
64
|
+
*/
|
|
65
|
+
export declare function provabilityGraderRole(name: string): "requirement" | "report" | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Register all six graders. Skips names already present (like
|
|
68
|
+
* `registerContinuityGraders`, so a plugin registered first keeps its name)
|
|
69
|
+
* and returns the canonical names.
|
|
70
|
+
*/
|
|
71
|
+
export declare function registerProvabilityGraders(registry: GraderRegistry): ReadonlyArray<string>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { assumptionRate, citationValidity, claimCoverage, costPerProvenClaim, createCostPerProvenClaim, replayIntegrity, rungAchieved, } from "./graders";
|
|
2
|
+
export { FLOOR_ABOVE_BUNDLE, INFINITE_COST_PREFIX, INVALID_SAMPLE_METADATA, NO_PROOF_BUNDLE, PROVABILITY_METRIC_THRESHOLDS, UNMEASURED_PREFIXES, UNPRICED_COST_PREFIX, UNVERIFIED_BUNDLE, assumptionRate, citationValidity, claimCoverage, costPerProvenClaim, createCostPerProvenClaim, isUnmeasured, replayIntegrity, rungAchieved, } from "./graders";
|
|
3
|
+
export { gateSpendOf } from "./cost";
|
|
4
|
+
export { expectedSourceKey, sampleProvabilityOf, } from "./metadata";
|
|
5
|
+
export { PROVABILITY_METRIC_SPECS, renderProvabilitySummaryLines, summarizeCostPerProvenClaim, summarizeProvabilityMetrics, } from "./summary";
|
|
6
|
+
/** The six graders under their canonical names, in the design's table order. */
|
|
7
|
+
export const PROVABILITY_GRADERS = Object.freeze([
|
|
8
|
+
["provability.claimCoverage", claimCoverage],
|
|
9
|
+
["provability.rungAchieved", rungAchieved],
|
|
10
|
+
["provability.citationValidity", citationValidity],
|
|
11
|
+
["provability.assumptionRate", assumptionRate],
|
|
12
|
+
["provability.replayIntegrity", replayIntegrity],
|
|
13
|
+
["provability.costPerProvenClaim", costPerProvenClaim],
|
|
14
|
+
]);
|
|
15
|
+
/**
|
|
16
|
+
* The six graders, with `costPerProvenClaim` pricing re-runs through
|
|
17
|
+
* `pricing` on a run no cost-tracker metered (see `./cost`). The eval runner
|
|
18
|
+
* builds its set here from its own `RunEvalOptions.pricing`; without a seam
|
|
19
|
+
* this is {@link PROVABILITY_GRADERS}.
|
|
20
|
+
*/
|
|
21
|
+
export function createProvabilityGraders(opts = {}) {
|
|
22
|
+
if (opts.pricing === undefined)
|
|
23
|
+
return PROVABILITY_GRADERS;
|
|
24
|
+
const cost = createCostPerProvenClaim({ pricing: opts.pricing });
|
|
25
|
+
return Object.freeze(PROVABILITY_GRADERS.map(([name, grader]) => Object.freeze([name, name === "provability.costPerProvenClaim" ? cost : grader])));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The graders that are REPORTED, never a pass/fail bar: their verdict is
|
|
29
|
+
* always a pass when measured, so a combination that let them vote would
|
|
30
|
+
* pass a sample on a dollar figure.
|
|
31
|
+
*/
|
|
32
|
+
export const PROVABILITY_REPORTED_GRADERS = Object.freeze([
|
|
33
|
+
"provability.costPerProvenClaim",
|
|
34
|
+
]);
|
|
35
|
+
/** The canonical names, in order. */
|
|
36
|
+
export const PROVABILITY_GRADER_NAMES = Object.freeze(PROVABILITY_GRADERS.map(([name]) => name));
|
|
37
|
+
/**
|
|
38
|
+
* How one of the six joins a sample's overall grade (design §9.1), keyed on
|
|
39
|
+
* its canonical name — the one answer for every path that adds it, the
|
|
40
|
+
* `--provability` flag and a graders file's `type: registry` entry alike:
|
|
41
|
+
* - `"requirement"` — the five with a bar: the sample must pass each on top
|
|
42
|
+
* of its own `combine:` policy, never as a vote inside it (a vacuous pass
|
|
43
|
+
* or a lower-is-better rate cannot carry a `combine: any` or skew a
|
|
44
|
+
* `weighted` score);
|
|
45
|
+
* - `"report"` — {@link PROVABILITY_REPORTED_GRADERS}: recorded, never part
|
|
46
|
+
* of the verdict (more gate spend can never turn a fail into a pass).
|
|
47
|
+
* `undefined` for any other name.
|
|
48
|
+
*/
|
|
49
|
+
export function provabilityGraderRole(name) {
|
|
50
|
+
if (PROVABILITY_REPORTED_GRADERS.includes(name))
|
|
51
|
+
return "report";
|
|
52
|
+
return PROVABILITY_GRADER_NAMES.includes(name) ? "requirement" : undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Register all six graders. Skips names already present (like
|
|
56
|
+
* `registerContinuityGraders`, so a plugin registered first keeps its name)
|
|
57
|
+
* and returns the canonical names.
|
|
58
|
+
*/
|
|
59
|
+
export function registerProvabilityGraders(registry) {
|
|
60
|
+
for (const [name, grader] of PROVABILITY_GRADERS) {
|
|
61
|
+
if (!registry.has(name))
|
|
62
|
+
registry.register(name, grader);
|
|
63
|
+
}
|
|
64
|
+
return PROVABILITY_GRADER_NAMES;
|
|
65
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dataset sample's `metadata.provability` (design/0.8.0-provability-release.md
|
|
3
|
+
* §9.1): the per-sample floor override and the origins that must ground the
|
|
4
|
+
* answer.
|
|
5
|
+
*
|
|
6
|
+
* metadata:
|
|
7
|
+
* provability: grounded # shorthand ≡ { level: grounded }
|
|
8
|
+
* provability:
|
|
9
|
+
* level: checked # grade THIS sample at checked
|
|
10
|
+
* expected_sources: # each must be an ok source origin
|
|
11
|
+
* - https://acme.example/policy
|
|
12
|
+
* - docs/pricing.md
|
|
13
|
+
*
|
|
14
|
+
* `eval-dataset` keeps `metadata` free-form (`z.record(z.unknown())`), so the
|
|
15
|
+
* reading lives here, beside the only graders that use it. A malformed value
|
|
16
|
+
* is reported, never ignored: the graders turn it into a failing result that
|
|
17
|
+
* names the field, because a floor the author meant and the grader dropped
|
|
18
|
+
* would grade the sample against the wrong bar in silence.
|
|
19
|
+
*/
|
|
20
|
+
import type { Sample } from "@crewhaus/eval-dataset";
|
|
21
|
+
import { type ProvabilityLevel } from "@crewhaus/provability";
|
|
22
|
+
/** The parsed `metadata.provability`; both fields optional. */
|
|
23
|
+
export type SampleProvability = {
|
|
24
|
+
/** The floor this sample is graded at, overriding the run's. */
|
|
25
|
+
readonly level?: ProvabilityLevel;
|
|
26
|
+
/** Origins (URLs, `doi:` refs, workspace paths) that must ground the answer, as written. */
|
|
27
|
+
readonly expectedSources?: ReadonlyArray<string>;
|
|
28
|
+
};
|
|
29
|
+
export type SampleProvabilityRead = {
|
|
30
|
+
readonly ok: true;
|
|
31
|
+
readonly value: SampleProvability;
|
|
32
|
+
} | {
|
|
33
|
+
readonly ok: false;
|
|
34
|
+
readonly error: string;
|
|
35
|
+
};
|
|
36
|
+
/** Read `sample.metadata.provability`. Absent ⇒ `{ ok: true, value: {} }`. */
|
|
37
|
+
export declare function sampleProvabilityOf(sample: Pick<Sample, "metadata">): SampleProvabilityRead;
|
|
38
|
+
/**
|
|
39
|
+
* The key an expected source is compared under — the normalization the
|
|
40
|
+
* bundle's binding origins went through (`@crewhaus/provability`'s origin
|
|
41
|
+
* rules): an `http(s)` URL normalized, a `doi:` ref as its resolver URL, and
|
|
42
|
+
* anything else as a workspace-relative path measured from `workspaceRoot`
|
|
43
|
+
* (the eval's cwd, where the run's tools resolved paths). A path that lands
|
|
44
|
+
* outside the workspace keeps its written form (minus a leading `./`), so it
|
|
45
|
+
* can only match an origin spelled exactly that way.
|
|
46
|
+
*/
|
|
47
|
+
export declare function expectedSourceKey(source: string, workspaceRoot?: string): string;
|