Skip to content

Commit 1b67e81

Browse files
committed
fix(evals): enforce paid blinding boundary
1 parent 64c5043 commit 1b67e81

5 files changed

Lines changed: 153 additions & 25 deletions

File tree

‎docs/plans/2026-07-17-h1-method-hardening-design.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# H1 Method Hardening Design
22

3-
**Linear:** DSE-708
4-
**Dependency:** PR #52 at `4365c28`
3+
**Linear:** DSE-708
4+
**Dependency:** PR #52 at `4365c28`
55
**Gate:** Required before any paid study call
66

77
## Decision
@@ -19,17 +19,17 @@ Confirmatory reports fail closed if any frozen field is missing or its observed
1919

2020
## Experimental unit and randomization
2121

22-
The unit of analysis is the task. Roster and replicate are repeated measurements averaged within task. Create every task x roster x condition x replicate cell before execution. Derive an independent condition permutation from the master seed plus task and roster IDs; interleave blocks so time or provider order cannot align with one condition. All failed, timed-out, malformed, over-budget, abstained, incomplete, ungraded, and unresolved cells remain failures.
22+
The unit of analysis is the task. Roster and replicate are repeated measurements averaged within task. Create every task x roster x condition x replicate cell before execution. Derive an independent condition permutation from the master seed plus task and roster IDs so condition order varies across blocks. All failed, timed-out, malformed, over-budget, abstained, incomplete, ungraded, and unresolved cells remain failures.
2323

2424
## Grading contract
2525

2626
Two independent graders score every successful output. Atomic records include rubric item, error category and severity, severe-error flag, holistic dimensions, reviewer seconds, confidence or abstention, rubric version/hash, grader batch/order, and condition/provider guess. Adjudication sees only genuine disagreements, cites every source record, and never overwrites raw judgments.
2727

28-
Non-success records bypass the human queue and are automatic failures. Successful grader views strip provider, model, condition, mode, and formatting fingerprints; normalize presentation deterministically; run a leakage scan; and retain a separate hashed/access-controlled blind map.
28+
Non-success records bypass the human queue and are automatic failures. Successful grader views expose only normalized presentation text, scan it for predeclared provider/model/condition labels, and retain a separate hashed/access-controlled blind map. Paid and confirmatory scoring rejects direct planned-run targets, unhashed maps, and maps that do not cover exactly the successful output set.
2929

3030
## Analysis and gates
3131

32-
Study reporting uses failure-inclusive Wilson rates, task-clustered paired bootstrap differences with roster/replicate averaging, raw agreement, prevalence, kappa, adjudication rate, severe errors, reviewer effort, token/cost distributions, latency distributions, deviations, and leakage guesses.
32+
Study reporting uses failure-inclusive Wilson rates, task-clustered paired bootstrap differences with roster/replicate averaging, raw agreement, prevalence, kappa, adjudication rate, severe errors, reviewer effort, total and analysis-set cost, p95 latency, deviations, and retained leakage guesses. A paid workflow must preregister and apply its material-leakage rule before claiming method readiness.
3333

3434
Reliability is undefined—not perfect—when expected agreement is one. A future paid pilot is
3535
method-ready only with at least 95% double grading, raw agreement at least 80%, kappa at least

‎docs/plans/2026-07-17-h1-method-hardening.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
## Phase 3 — Confirmatory estimands and refusal gates
1818

19-
1. Add tests for task-level roster averaging, task-clustered paired bootstrap, one-sided severe-error and effort/latency noninferiority boundaries, symmetric task exclusions, deviations, cost/latency distributions, and report refusal on freeze drift or exploratory evidence.
19+
1. Add tests for task-level roster averaging, task-clustered paired bootstrap, one-sided severe-error and effort/latency noninferiority boundaries, symmetric task exclusions, deviations, complete cost/latency receipts and summaries, and report refusal on freeze drift or exploratory evidence.
2020
2. Extend `scoring.py`, `reporting.py`, and CLI artifact validation. Archive every gate input and seed.
2121
3. Re-run focused tests and commit.
2222

‎src/conclave/evals/scoring.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,7 +912,19 @@ def score_study(
912912
if len({item.adjudication_id for item in adjudications}) != len(adjudications):
913913
raise ValueError("adjudication_id values must be unique")
914914

915+
paid_study = manifest.evidence_classification != "synthetic_exploratory"
916+
if paid_study:
917+
if blind_map is None or blind_map.blind_map_hash is None:
918+
raise ValueError("paid scoring requires a hashed blind map")
919+
if any(item.planned_run_id is not None for item in (*raw_judgments, *adjudications)):
920+
raise ValueError("paid judgments and adjudications require opaque output IDs")
915921
mapping = _target_map(blind_map)
922+
if paid_study:
923+
successful_run_ids = {
924+
record.planned_run_id for record in study_run.records if record.outcome == "success"
925+
}
926+
if set(mapping.values()) != successful_run_ids or len(mapping) != len(successful_run_ids):
927+
raise ValueError("paid blind map must cover exactly the successful output set")
916928
judgments_by_run: dict[str, list[GraderJudgment]] = defaultdict(list)
917929
judgment_ids = {item.judgment_id for item in raw_judgments}
918930
seen_grader_targets: set[tuple[str, str]] = set()

‎tests/evals/test_analysis_gates.py‎

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
recompute_simultaneous_upper_bounds,
1515
score_study,
1616
)
17-
from tests.evals.test_method_grading import _judgment
17+
from tests.evals.test_method_grading import _blind_targets, _judgment
1818
from tests.evals.test_study_design import _design, _tasks
1919

2020

@@ -74,16 +74,22 @@ def _confirmatory_report(*, minimum_tasks: int = 2):
7474
frozen_design=design,
7575
)
7676
study_run = _confirmatory_report_run(manifest)
77+
blind_map, opaque_by_run = _blind_targets(study_run)
7778
records = study_run.records
7879
judgments = tuple(
79-
_judgment(record.planned_run_id, grader, value=planned.condition_id != "self_refine")
80+
_judgment(
81+
opaque_by_run[record.planned_run_id],
82+
grader,
83+
value=planned.condition_id != "self_refine",
84+
)
8085
for planned, record in zip(manifest.planned_runs, records, strict=True)
8186
for grader in ("grader-a", "grader-b")
8287
)
8388
report = score_study(
8489
manifest=manifest,
8590
study_run=study_run,
8691
raw_judgments=judgments,
92+
blind_map=blind_map,
8793
bootstrap_seed=design.bootstrap.seed,
8894
bootstrap_samples=design.bootstrap.samples,
8995
)
@@ -115,11 +121,14 @@ def _confirmatory_report_run(manifest):
115121

116122

117123
def _gate(manifest, report):
124+
study_run = _confirmatory_report_run(manifest)
125+
blind_map, _ = _blind_targets(study_run)
118126
return evaluate_confirmatory_gates(
119127
manifest=manifest,
120-
study_run=_confirmatory_report_run(manifest),
128+
study_run=study_run,
121129
raw_judgments=report.raw_judgments,
122130
report=report,
131+
blind_map=blind_map,
123132
)
124133

125134

@@ -253,8 +262,11 @@ def test_confirmatory_gate_recomputes_secondary_bounds_from_task_inputs(
253262

254263
def test_caller_cannot_substitute_optimistic_bounds_to_turn_redesign_into_go() -> None:
255264
manifest, report = _confirmatory_report()
256-
condition_by_run = {
257-
planned.planned_run_id: planned.condition_id for planned in manifest.planned_runs
265+
study_run = _confirmatory_report_run(manifest)
266+
blind_map, opaque_by_run = _blind_targets(study_run)
267+
condition_by_output = {
268+
opaque_by_run[planned.planned_run_id]: planned.condition_id
269+
for planned in manifest.planned_runs
258270
}
259271
severe = AtomicError(
260272
rubric_item="safety",
@@ -270,15 +282,15 @@ def test_caller_cannot_substitute_optimistic_bounds_to_turn_redesign_into_go() -
270282
"severe_error": True,
271283
}
272284
)
273-
if condition_by_run[judgment.planned_run_id] == "elite_full"
285+
if condition_by_output[judgment.opaque_output_id] == "elite_full"
274286
else judgment
275287
for judgment in report.raw_judgments
276288
)
277-
study_run = _confirmatory_report_run(manifest)
278289
redesign_report = score_study(
279290
manifest=manifest,
280291
study_run=study_run,
281292
raw_judgments=bad_judgments,
293+
blind_map=blind_map,
282294
bootstrap_seed=manifest.frozen_design.bootstrap.seed,
283295
bootstrap_samples=manifest.frozen_design.bootstrap.samples,
284296
)
@@ -288,6 +300,7 @@ def test_caller_cannot_substitute_optimistic_bounds_to_turn_redesign_into_go() -
288300
study_run=study_run,
289301
raw_judgments=bad_judgments,
290302
report=redesign_report,
303+
blind_map=blind_map,
291304
).status
292305
== "REDESIGN"
293306
)
@@ -322,6 +335,7 @@ def test_caller_cannot_substitute_optimistic_bounds_to_turn_redesign_into_go() -
322335
study_run=study_run,
323336
raw_judgments=bad_judgments,
324337
report=substituted,
338+
blind_map=blind_map,
325339
)
326340

327341

@@ -405,6 +419,8 @@ def test_confirmatory_gate_uses_single_model_latency_baseline_and_reliability()
405419

406420
def test_rotating_grader_pairs_still_produce_complete_reliability() -> None:
407421
manifest, report = _confirmatory_report()
422+
study_run = _confirmatory_report_run(manifest)
423+
blind_map, _ = _blind_targets(study_run)
408424
raw = []
409425
for index, judgment in enumerate(report.raw_judgments):
410426
pair = index // 2
@@ -418,8 +434,9 @@ def test_rotating_grader_pairs_still_produce_complete_reliability() -> None:
418434
)
419435
rescored = score_study(
420436
manifest=manifest,
421-
study_run=_confirmatory_report_run(manifest),
437+
study_run=study_run,
422438
raw_judgments=tuple(raw),
439+
blind_map=blind_map,
423440
bootstrap_seed=manifest.frozen_design.bootstrap.seed,
424441
bootstrap_samples=manifest.frozen_design.bootstrap.samples,
425442
)

0 commit comments

Comments
 (0)