Daily Lab · recorded replay · run 008af826 · interrupted
keyed-v2
all 6 criteria satisfied over 60 applicable casesUnder the current criteria, generation 2. This run faced generation 1 when it executed — both verdicts are below.
Proposed contract: every verdict names its article, the id set must match exactly, and a non-stop finish_reason is a failure.
Verdict
2 criteria generationsCriteria generation
spec f027762ab4d08b35
all 6 criteria satisfied over 60 applicable cases
| Criterion | Satisfied | Result |
|---|---|---|
| universal-refusalDoes it refuse every response from which no association can be recovered? | 48/48 | met |
| association-exactOn its own protocol, does every article receive exactly the verdict it was given? | 3/3 | met |
| protocol-violation-refusalDoes it refuse duplicate, unknown, missing ids and unusable scores? | 9/9 | met |
| no-crashDoes it terminate on every case without crashing or hanging? | 60/60 | met |
| complete-evidenceIs there a prediction record for every applicable case? | 60/60 | met |
| protocol-exclusivityOn cases outside its declared protocol, does it refuse rather than associate anyway? | 4/4 | met |
Accepted means eligible for human review under this spec hash, against a public case suite. It is not evidence of production quality, and it does not establish generalisation: the cases are visible and a candidate may have been written against them.
The patch
against positional_v0.pyUnified diff · 163 lines · applies with git apply
diff --git a/backend/lab/contract/versions/positional_v0.py b/backend/lab/contract/versions/keyed_v2.py--- a/backend/lab/contract/versions/positional_v0.py+++ b/backend/lab/contract/versions/keyed_v2.py@@ -1,18 +1,26 @@-"""Historical behaviour, transcribed from `origin/main`.+"""The proposed contract: every verdict identifies its own article. -Source: backend/app/services/openai_service.py, score_articles_batch, the-`normalized` loop. Verbatim semantics:+Modelled on the id-keyed validation Daily already ships in+`backend/app/services/ranking_contract.py:131` (`validate_judgments`), which is+the pattern this repository already trusts for the S7 ranking path: - if len(results_list) != len(articles):- logger.warning("... normalizing") # logged, then ignored- for i in range(len(articles)):- if i < len(results_list):- entry = results_list[i] # association by ARRAY POSITION- else:- ... {"relevant": False, "score": 0.0, "reason": "scoring incomplete"}+ packs = {e.article_id: e for e in evidence}+ ids = [v.article_id for v in values]+ if len(ids) != len(set(ids)) or set(ids) != set(packs):+ raise ValueError('ranker must return exact article ID set') -This version is preserved so the experiment can measure the defect rather than-describe it. It is not a control: it is what production does today.+One set-equality check covers duplicate, unknown and missing ids at once, and+refuses to salvage a partial answer. The same shape is reproduced here, plus+the truncation check `ranking_provider.py:296` performs and+`score_articles_batch` does not:++ if choice.get('finish_reason') != 'stop':+ raise ProviderFailure('incomplete_response')++What this version does NOT claim: that it ranks better. Association is a+correctness property, and correctness is all that is under test. Relevance+quality under this protocol is unmeasured — it needs recordings that do not+exist, because no budgeted keyed run has been made. """ from __future__ import annotations@@ -54,45 +62,88 @@ import json from typing import Any -VERSION_ID = "positional-v0"-PROTOCOL = "positional-v0"+VERSION_ID = "keyed-v2"+PROTOCOL = "keyed-v2" def parse(articles: list[dict[str, Any]], response: dict[str, Any]) -> dict[str, Any]:- if response.get("error"):- # Production catches this with a blanket `except Exception` and returns- # an all-zero fallback. Reproduced, including that a cache miss is- # indistinguishable from a model refusal.- return ok([verdict(a["id"], False, 0.0, "scoring unavailable") for a in articles])+ error = response.get("error")+ if error:+ if error == "no_recording":+ return refuse("no_recording", "no recorded response for this request")+ if error in {"timeout", "cancelled"}:+ return refuse(error, error)+ if error == "budget_exceeded":+ return refuse("retries_exhausted", error)+ return refuse("retries_exhausted", str(error))++ # A truncated completion is a failure, not a shorter answer. Production+ # never reads finish_reason, which is why 33 recorded responses that stopped+ # at the output ceiling were indistinguishable from malformed ones.+ finish_reason = response.get("finish_reason")+ if finish_reason is not None and finish_reason != "stop":+ return refuse("truncated_response", f"finish_reason={finish_reason}") content = response.get("content") if content is None:- return ok([verdict(a["id"], False, 0.0, "scoring unavailable") for a in articles])+ return refuse("retries_exhausted", "no content returned") try: result = json.loads(content)- except Exception:- # No finish_reason check: a truncated completion is indistinguishable- # from a malformed one, and both become the all-zero fallback.- return ok([verdict(a["id"], False, 0.0, "scoring unavailable") for a in articles])-- results_list = result.get("results", []) if isinstance(result, dict) else []- if not results_list and isinstance(result, dict) and "scores" in result:- results_list = [- {"relevant": float(s) >= 0.5, "score": float(s), "reason": ""}- for s in result["scores"]- ]+ except Exception as exc:+ return refuse("malformed_json", str(exc))++ if not isinstance(result, dict) or not isinstance(result.get("results"), list):+ return refuse("unexpected_shape", "expected an object with a results array")++ entries = result["results"]+ expected = {a["id"]: a for a in articles}++ seen: list[str] = []+ for entry in entries:+ if not isinstance(entry, dict):+ return refuse("invalid_type", f"entry is {type(entry).__name__}")+ article_id = entry.get("article_id", entry.get("id"))+ if not isinstance(article_id, str):+ return refuse("invalid_type", "article_id missing or not a string")+ seen.append(article_id)++ # Duplicate, unknown and missing are distinguished for the operator even+ # though any one of them is fatal. `validate_judgments` collapses all three+ # into one message; the Lab separates them so a counterexample can name the+ # exact failure, then refuses just as hard.+ if len(seen) != len(set(seen)):+ dupes = sorted({i for i in seen if seen.count(i) > 1})+ return refuse("duplicate_id", f"repeated ids: {', '.join(dupes[:5])}")+ unknown = [i for i in seen if i not in expected]+ if unknown:+ return refuse("unknown_id", f"ids not in the request: {', '.join(sorted(unknown)[:5])}")+ missing = [i for i in expected if i not in set(seen)]+ if missing:+ return refuse("missing_id", f"no verdict for: {', '.join(sorted(missing)[:5])}") out: list[dict[str, Any]] = []- for i, article in enumerate(articles):- if i < len(results_list):- entry = results_list[i] if isinstance(results_list[i], dict) else {}- try:- score = max(0.0, min(1.0, float(entry.get("score", 0.5))))- except Exception:- score = 0.5- relevant = bool(entry.get("relevant", score >= 0.5))- out.append(verdict(article["id"], relevant, score, str(entry.get("reason", ""))))- else:- out.append(verdict(article["id"], False, 0.0, "scoring incomplete"))- return ok(out)+ for entry in entries:+ article_id = entry.get("article_id", entry.get("id"))+ score = finite_unit_score(entry.get("score"))+ if score is None:+ raw = entry.get("score")+ if isinstance(raw, (int, float)) and not isinstance(raw, bool):+ # Distinguishes 9e9 (out of range) from NaN (not finite); both+ # are clamped silently by production.+ return refuse(+ "score_not_finite" if raw != raw or raw in (float("inf"), float("-inf"))+ else "score_out_of_range",+ f"score={raw!r} for {article_id}",+ )+ return refuse("invalid_type", f"score is {type(raw).__name__} for {article_id}")+ relevant = entry.get("relevant", score >= 0.5)+ if not isinstance(relevant, bool):+ return refuse("invalid_type", f"relevant is {type(relevant).__name__}")+ out.append(verdict(article_id, relevant, score, str(entry.get("reason", ""))))++ # Emit in request order so downstream consumers see a stable sequence. The+ # association itself is by id and does not depend on this ordering — the+ # permutation property test asserts exactly that.+ by_id = {v["article_id"]: v for v in out}+ return ok([by_id[a["id"]] for a in articles])
Reproduce this run
cd backend EVAL_OFFLINE=1 venv/bin/python -m lab.orchestrate \ --candidate keyed-v2 --tag clean cd ../web && npm run export:lab -- --check
Source under test
backend/lab/contract/versions/keyed_v2.pysha256 bcde0d5383b54e71… · 6390 bytestranscribed from modelled on backend/app/services/ranking_contract.py:131
Timeline
2 attempts- 01unknown-outcomeorchestrator died with this attempt in flight; records file absent on recoverylocal-known · started 2026-09-22T19:47:57.889Z · ended 2026-09-22T19:47:57.913Z
- 02succeededcompleted 64 cases in 25.5mslocal-known · started 2026-09-22T19:47:57.913Z · ended 2026-09-22T19:47:57.939Z
Durability makes orchestration recoverable; it does not make a sandbox creation or a publish happen exactly once. An attempt the orchestrator never saw finish is recorded as unknown-outcome rather than assumed to have failed.
Cases
60 scored, 4 not applicable- Recorded cases
- 39real batches, replayed
- Fault-injected
- 21labelled synthetic
- Correct
- 60of the scored cases
- Wrong
- 0see the table
Every scored case was correct. That is what the verdict above is asserting, and nothing more.
Unscored
Measured, and deliberately not gradedA criterion decides; a diagnostic reports. Promoting one of these to a criterion would change the spec hash and re-decide runs that never faced it, so a gap found after the fact is published as a number rather than closed behind your back.
- 0/4
On cases outside its declared protocol, did it refuse — or associate anyway?
It produced no association on any case outside its declared protocol.
Provenance
What can and cannot be established- Executed at revision
- 4e8bee7625820107e84184f8e24c6bd7125f2e86+dirtyrecorded when the harness ran, not re-derived at export
- Inputs sha256
- 02da1ab3e0e18c82cases, records, candidates and event logs
- Evaluator sha256
- 277ec81521ba14d8
- Spec hash
- f027762ab4d08b35
- Execution mode
- offline-replayThe harness reads committed responses from disk and makes no network call. The candidate imports nothing beyond the standard library.
- Python
- 3.12.13
- Model calls
- 0
- Spend for this run
- $0Offline replay of committed recordings: no inference call was made, so provider spend for this run is $0. What the original recordings cost is not attributed per batch anywhere in this repository, so it is left unknown rather than estimated.
- Recording cost
- unknown
- Sandbox limits
- python3.13, network disabled120s wall clock, none secrets. This candidate matched a committed implementation, so it ran locally and the boundary was not exercised here.
Case suites
backend/lab/cases/observed.json42 cases · sha256 3d7f4b4143d85ab3…backend/evals/.cache/llm via offline replay
backend/lab/cases/synthetic.json22 cases · sha256 21c92da332a9837e…lab/build_synthetic.py — fault injection, ground truth by construction
- Note. A fault was deliberately injected into this run: the orchestrator was SIGKILLed with an attempt in flight. The recovery that follows is real.backend/lab/orchestrate.py --kill-after
- Note. Every case ran offline against responses already committed to this repository. No inference call was made and no provider was charged.backend/evals/.cache/llm
- Caution. The case suite is public. A candidate may have been written against it, so passing does not establish generalisation.backend/lab/cases/
- Caution. Association correctness is measured; relevance quality under keyed-v2 is not. Sending article ids changes the request, which invalidates every recorded response for this runner — new budgeted recordings would be required and none exist.backend/evals/llm_cache.py:133
The full artifact, as published
Validated against the schema in web/lib/lab/artifact.ts before it was written. Download the JSON.