@crewhaus/grader-provability 0.0.0-stage → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ import { PROVABILITY_LEVELS, doiUrl, isProvabilityLevel, normalizePath, normalizeUrl, } from "@crewhaus/provability";
2
+ const RUNGS = PROVABILITY_LEVELS.join(" | ");
3
+ /** Read `sample.metadata.provability`. Absent ⇒ `{ ok: true, value: {} }`. */
4
+ export function sampleProvabilityOf(sample) {
5
+ const raw = sample.metadata?.["provability"];
6
+ if (raw === undefined)
7
+ return { ok: true, value: {} };
8
+ if (typeof raw === "string") {
9
+ return isProvabilityLevel(raw)
10
+ ? { ok: true, value: { level: raw } }
11
+ : {
12
+ ok: false,
13
+ error: `metadata.provability must be a rung (${RUNGS}) or { level?, expected_sources? }, got ${JSON.stringify(raw)}`,
14
+ };
15
+ }
16
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
17
+ return {
18
+ ok: false,
19
+ error: `metadata.provability must be a rung (${RUNGS}) or { level?, expected_sources? }, got ${Array.isArray(raw) ? "a list" : JSON.stringify(raw)}`,
20
+ };
21
+ }
22
+ const obj = raw;
23
+ const unknown = Object.keys(obj).filter((k) => k !== "level" && k !== "expected_sources");
24
+ if (unknown.length > 0) {
25
+ return {
26
+ ok: false,
27
+ error: `metadata.provability has unknown key(s) ${unknown.map((k) => JSON.stringify(k)).join(", ")} — accepted: level, expected_sources`,
28
+ };
29
+ }
30
+ const level = obj["level"];
31
+ if (level !== undefined && !isProvabilityLevel(level)) {
32
+ return {
33
+ ok: false,
34
+ error: `metadata.provability.level must be one of ${RUNGS}, got ${JSON.stringify(level)}`,
35
+ };
36
+ }
37
+ const sources = obj["expected_sources"];
38
+ if (sources !== undefined &&
39
+ (!Array.isArray(sources) || sources.some((s) => typeof s !== "string" || s.trim() === ""))) {
40
+ return {
41
+ ok: false,
42
+ error: "metadata.provability.expected_sources must be a list of non-empty origin strings",
43
+ };
44
+ }
45
+ return {
46
+ ok: true,
47
+ value: {
48
+ ...(level !== undefined ? { level: level } : {}),
49
+ ...(sources !== undefined ? { expectedSources: sources } : {}),
50
+ },
51
+ };
52
+ }
53
+ /**
54
+ * The key an expected source is compared under — the normalization the
55
+ * bundle's binding origins went through (`@crewhaus/provability`'s origin
56
+ * rules): an `http(s)` URL normalized, a `doi:` ref as its resolver URL, and
57
+ * anything else as a workspace-relative path measured from `workspaceRoot`
58
+ * (the eval's cwd, where the run's tools resolved paths). A path that lands
59
+ * outside the workspace keeps its written form (minus a leading `./`), so it
60
+ * can only match an origin spelled exactly that way.
61
+ */
62
+ export function expectedSourceKey(source, workspaceRoot) {
63
+ const trimmed = source.trim();
64
+ if (/^doi:/i.test(trimmed))
65
+ return doiUrl(trimmed);
66
+ const url = normalizeUrl(trimmed);
67
+ if (url !== undefined)
68
+ return url;
69
+ const path = normalizePath(trimmed, workspaceRoot !== undefined ? { workspaceRoot } : {});
70
+ return path ?? trimmed.replace(/^\.\//, "");
71
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * The cross-sample roll-up of the six provability graders — the
3
+ * `summarizeContinuityMetrics` / `summarize12MetricRubric` shape (`byMetric`
4
+ * in, per-metric means + pass fractions + percentiles out), so a report
5
+ * surface treats the three rubrics alike.
6
+ *
7
+ * Two metrics do not have a fixed numeric bar: `rungAchieved` passes against
8
+ * each sample's own floor (it breaches when any sample fell short), and
9
+ * `costPerProvenClaim` is reported with no threshold (it never breaches; the
10
+ * infinite samples — spend with zero met claims — and the partly unpriced
11
+ * ones — a lower bound, not a price — are counted apart, read off the
12
+ * grader's pinned rationale prefixes).
13
+ *
14
+ * An UNMEASURED result (no bundle, unverified, invalid metadata — the
15
+ * grader's pinned prefixes, `isUnmeasured`) carries a placeholder score of 0.
16
+ * It always counts as a failure in `passFraction`, but it is kept OUT of a
17
+ * lower-is-better mean (`assumptionRate`), where 0 is the best value a sample
18
+ * can have: averaging it in would let unmeasured samples hide a breach. A
19
+ * lower-is-better metric with no measured sample breaches.
20
+ */
21
+ import type { GradeResult } from "@crewhaus/eval-grader";
22
+ export type ProvabilityMetricSpec = {
23
+ readonly name: string;
24
+ /** Absent: the bar is per sample (`rungAchieved`) or there is none (`costPerProvenClaim`). */
25
+ readonly threshold?: number;
26
+ /** True for ≥-threshold metrics; false for ≤ (rate, cost). */
27
+ readonly higherIsBetter: boolean;
28
+ };
29
+ /** The six graders, in the design's table order. */
30
+ export declare const PROVABILITY_METRIC_SPECS: ReadonlyArray<ProvabilityMetricSpec>;
31
+ export type ProvabilityMetricSummary = {
32
+ readonly name: string;
33
+ readonly threshold?: number;
34
+ readonly higherIsBetter: boolean;
35
+ readonly count: number;
36
+ /** Results that replaced their measurement with a failure (see the module doc). */
37
+ readonly unmeasured: number;
38
+ /** Over every result when higher is better; over the measured ones when lower is. */
39
+ readonly mean: number;
40
+ readonly passFraction: number;
41
+ readonly thresholdBreach: boolean;
42
+ readonly p50: number;
43
+ readonly p95: number;
44
+ };
45
+ export type ProvabilityCostSummary = {
46
+ readonly name: "provability.costPerProvenClaim";
47
+ readonly count: number;
48
+ /** Samples with an exact finite ratio (≥1 met claim, or no gate spend). */
49
+ readonly finiteCount: number;
50
+ /** Samples that spent with zero met claims (the ∞ ratio). */
51
+ readonly infiniteCount: number;
52
+ /** Samples whose re-runs were partly unpriced: a lower bound, kept out of the means. */
53
+ readonly unpricedCount: number;
54
+ /** Samples whose grade was a failure (no bundle / unverified) — no ratio at all. */
55
+ readonly unmeasured: number;
56
+ readonly meanUsd: number;
57
+ readonly p50Usd: number;
58
+ readonly p95Usd: number;
59
+ };
60
+ export type ProvabilitySummary = {
61
+ /** The five 0..1 metrics, in spec order. */
62
+ readonly metrics: ReadonlyArray<ProvabilityMetricSummary>;
63
+ readonly cost: ProvabilityCostSummary;
64
+ /** Metrics whose roll-up breached their bar. */
65
+ readonly breaches: number;
66
+ /** Mean pass fraction across the five gated metrics (cost is reported, not gated). */
67
+ readonly overall: number;
68
+ };
69
+ /** The cost metric's roll-up, the ∞ samples counted apart. */
70
+ export declare function summarizeCostPerProvenClaim(results: ReadonlyArray<GradeResult>): ProvabilityCostSummary;
71
+ /**
72
+ * Fold per-sample `GradeResult`s keyed by grader name into the provability
73
+ * roll-up. Metrics with no results report count 0 and never breach.
74
+ */
75
+ export declare function summarizeProvabilityMetrics(byMetric: Readonly<Record<string, ReadonlyArray<GradeResult>>>): ProvabilitySummary;
76
+ /** One line per metric — the report surface for eval summaries. */
77
+ export declare function renderProvabilitySummaryLines(summary: ProvabilitySummary): string[];
@@ -0,0 +1,114 @@
1
+ import { INFINITE_COST_PREFIX, PROVABILITY_METRIC_THRESHOLDS, UNPRICED_COST_PREFIX, isUnmeasured, } from "./graders";
2
+ /** The six graders, in the design's table order. */
3
+ export const PROVABILITY_METRIC_SPECS = Object.freeze([
4
+ {
5
+ name: "provability.claimCoverage",
6
+ threshold: PROVABILITY_METRIC_THRESHOLDS.claimCoverage,
7
+ higherIsBetter: true,
8
+ },
9
+ { name: "provability.rungAchieved", higherIsBetter: true },
10
+ {
11
+ name: "provability.citationValidity",
12
+ threshold: PROVABILITY_METRIC_THRESHOLDS.citationValidity,
13
+ higherIsBetter: true,
14
+ },
15
+ {
16
+ name: "provability.assumptionRate",
17
+ threshold: PROVABILITY_METRIC_THRESHOLDS.assumptionRate,
18
+ higherIsBetter: false,
19
+ },
20
+ {
21
+ name: "provability.replayIntegrity",
22
+ threshold: PROVABILITY_METRIC_THRESHOLDS.replayIntegrity,
23
+ higherIsBetter: true,
24
+ },
25
+ { name: "provability.costPerProvenClaim", higherIsBetter: false },
26
+ ]);
27
+ function percentile(values, p) {
28
+ if (values.length === 0)
29
+ return 0;
30
+ const sorted = [...values].sort((a, b) => a - b);
31
+ return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))] ?? 0;
32
+ }
33
+ const mean = (xs) => xs.length === 0 ? 0 : xs.reduce((a, b) => a + b, 0) / xs.length;
34
+ /** The cost metric's roll-up, the ∞ samples counted apart. */
35
+ export function summarizeCostPerProvenClaim(results) {
36
+ const measured = results.filter((r) => r.passed);
37
+ const infinite = measured.filter((r) => r.rationale.startsWith(INFINITE_COST_PREFIX));
38
+ const unpriced = measured.filter((r) => r.rationale.startsWith(UNPRICED_COST_PREFIX));
39
+ const finite = measured.filter((r) => !r.rationale.startsWith(INFINITE_COST_PREFIX) &&
40
+ !r.rationale.startsWith(UNPRICED_COST_PREFIX));
41
+ const usd = finite.map((r) => r.score);
42
+ return {
43
+ name: "provability.costPerProvenClaim",
44
+ count: results.length,
45
+ finiteCount: finite.length,
46
+ infiniteCount: infinite.length,
47
+ unpricedCount: unpriced.length,
48
+ unmeasured: results.length - measured.length,
49
+ meanUsd: mean(usd),
50
+ p50Usd: percentile(usd, 0.5),
51
+ p95Usd: percentile(usd, 0.95),
52
+ };
53
+ }
54
+ /**
55
+ * Fold per-sample `GradeResult`s keyed by grader name into the provability
56
+ * roll-up. Metrics with no results report count 0 and never breach.
57
+ */
58
+ export function summarizeProvabilityMetrics(byMetric) {
59
+ const metrics = [];
60
+ for (const spec of PROVABILITY_METRIC_SPECS) {
61
+ if (spec.name === "provability.costPerProvenClaim")
62
+ continue;
63
+ const results = byMetric[spec.name] ?? [];
64
+ const count = results.length;
65
+ const unmeasured = results.filter(isUnmeasured).length;
66
+ // A placeholder 0 is the WORST value of a higher-is-better metric (kept:
67
+ // conservative) and the BEST of a lower-is-better one (dropped).
68
+ const scored = spec.higherIsBetter ? results : results.filter((r) => !isUnmeasured(r));
69
+ const scores = scored.map((r) => r.score);
70
+ const m = mean(scores);
71
+ const passFraction = count === 0 ? 0 : results.filter((r) => r.passed).length / count;
72
+ const thresholdBreach = count > 0 &&
73
+ (spec.threshold === undefined
74
+ ? passFraction < 1
75
+ : spec.higherIsBetter
76
+ ? m < spec.threshold
77
+ : scored.length === 0 || m > spec.threshold);
78
+ metrics.push({
79
+ name: spec.name,
80
+ ...(spec.threshold !== undefined ? { threshold: spec.threshold } : {}),
81
+ higherIsBetter: spec.higherIsBetter,
82
+ count,
83
+ unmeasured,
84
+ mean: m,
85
+ passFraction,
86
+ thresholdBreach,
87
+ p50: percentile(scores, 0.5),
88
+ p95: percentile(scores, 0.95),
89
+ });
90
+ }
91
+ const cost = summarizeCostPerProvenClaim(byMetric["provability.costPerProvenClaim"] ?? []);
92
+ return {
93
+ metrics,
94
+ cost,
95
+ breaches: metrics.filter((m) => m.thresholdBreach).length,
96
+ overall: mean(metrics.map((m) => m.passFraction)),
97
+ };
98
+ }
99
+ /** One line per metric — the report surface for eval summaries. */
100
+ export function renderProvabilitySummaryLines(summary) {
101
+ const lines = [];
102
+ for (const m of summary.metrics) {
103
+ const bound = m.threshold === undefined
104
+ ? "≥ each sample's floor"
105
+ : m.higherIsBetter
106
+ ? `≥${m.threshold}`
107
+ : `≤${m.threshold}`;
108
+ lines.push(`${m.name.padEnd(32)} n=${m.count} · mean ${m.mean.toFixed(3)} · pass ${Math.round(m.passFraction * m.count)}/${m.count}${m.unmeasured > 0 ? ` (unmeasured ${m.unmeasured})` : ""} · p50 ${m.p50.toFixed(3)} p95 ${m.p95.toFixed(3)} · ${bound}${m.thresholdBreach ? " BREACH" : ""}`);
109
+ }
110
+ const c = summary.cost;
111
+ lines.push(`${c.name.padEnd(32)} n=${c.count} · mean $${c.meanUsd.toFixed(4)}/proven claim (finite ${c.finiteCount}, ∞ ${c.infiniteCount}${c.unpricedCount > 0 ? `, partly unpriced ${c.unpricedCount}` : ""}${c.unmeasured > 0 ? `, unmeasured ${c.unmeasured}` : ""}) · p50 $${c.p50Usd.toFixed(4)} p95 $${c.p95Usd.toFixed(4)} · reported, no threshold`);
112
+ lines.push(`provability overall: ${(summary.overall * 100).toFixed(1)}% pass · ${summary.breaches} breach(es)`);
113
+ return lines;
114
+ }
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@crewhaus/grader-provability",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.8.1",
4
+ "type": "module",
5
+ "description": "Deterministic eval graders over proof bundles: claim coverage, rung achieved, citation validity, assumption rate, replay integrity, cost per proven claim",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "test": "bun test src"
17
+ },
18
+ "dependencies": {
19
+ "@crewhaus/eval-dataset": "0.8.1",
20
+ "@crewhaus/eval-grader": "0.8.1",
21
+ "@crewhaus/grader-registry": "0.8.1",
22
+ "@crewhaus/provability": "0.8.1",
23
+ "@crewhaus/trace-event-bus": "0.8.1"
24
+ },
25
+ "license": "Apache-2.0",
26
+ "author": {
27
+ "name": "Max Meier",
28
+ "email": "max@crewhaus.ai",
29
+ "url": "https://crewhaus.ai"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/crewhaus/factory.git",
34
+ "directory": "packages/grader-provability"
35
+ },
36
+ "homepage": "https://github.com/crewhaus/factory/tree/main/packages/grader-provability#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/crewhaus/factory/issues"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "files": ["dist", "README.md", "LICENSE", "NOTICE"],
44
+ "engines": {
45
+ "bun": ">=1.3.14"
46
+ }
47
+ }