@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.
@@ -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();
@@ -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;