Daily Lab · recorded replay · run 008af826 · clean
count-guard-v1
association-exact: 0/3 cases satisfied, threshold 1Under the current criteria, generation 2. This run faced generation 1 when it executed — both verdicts are below.
PR #59: discards the batch when the verdict count differs from the article count, and lets CacheMiss and BudgetExceeded propagate.
Verdict
2 criteria generationsCriteria generation
spec f027762ab4d08b35
association-exact: 0/3 cases satisfied, threshold 1
| 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? | 0/3 | not met |
| protocol-violation-refusalDoes it refuse duplicate, unknown, missing ids and unusable scores? | 0/1 | not met |
| no-crashDoes it terminate on every case without crashing or hanging? | 52/52 | met |
| complete-evidenceIs there a prediction record for every applicable case? | 52/52 | met |
| protocol-exclusivityOn cases outside its declared protocol, does it refuse rather than associate anyway? | 5/12 | not 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 · 120 lines · applies with git apply
diff --git a/backend/lab/contract/versions/positional_v0.py b/backend/lab/contract/versions/count_guard_v1.py--- a/backend/lab/contract/versions/positional_v0.py+++ b/backend/lab/contract/versions/count_guard_v1.py@@ -1,18 +1,23 @@-"""Historical behaviour, transcribed from `origin/main`.+"""The count-mismatch guard, transcribed from PR #59 (head 81b2019). -Source: backend/app/services/openai_service.py, score_articles_batch, the-`normalized` loop. Verbatim semantics:+Source: backend/app/services/openai_service.py on+`mmarufov/batch-scoring-alignment`: 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"}--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.+ logger.warning("... discarding the batch rather than assigning "+ "verdicts by position")+ continue # retry, then report unscored++That commit also stops swallowing the harness's stop signals:++ except Exception as exc:+ if type(exc).__name__ in {"CacheMiss", "BudgetExceeded"}:+ raise++Both are reproduced. The guard is a real improvement over positional-v0 and+this experiment is expected to show that — and also to show its ceiling: it+compares *lengths*, so an equal-length reordered response still passes through+and is still associated by position. """ from __future__ import annotations@@ -54,45 +59,55 @@ import json from typing import Any -VERSION_ID = "positional-v0"+VERSION_ID = "count-guard-v1" PROTOCOL = "positional-v0" 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:+ # CacheMiss / BudgetExceeded propagate instead of degrading to zeros.+ if error in {"no_recording", "budget_exceeded"}:+ return refuse("no_recording" if error == "no_recording" else "retries_exhausted", error)+ if error in {"timeout", "cancelled"}:+ return refuse(error, error)+ return refuse("retries_exhausted", str(error)) 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])+ except Exception as exc:+ return refuse("malformed_json", str(exc)) - results_list = result.get("results", []) if isinstance(result, dict) else []- if not results_list and isinstance(result, dict) and "scores" in result:+ if not isinstance(result, dict):+ return refuse("unexpected_shape", f"top level is {type(result).__name__}")++ results_list = result.get("results", [])+ if not results_list and "scores" in result: results_list = [ {"relevant": float(s) >= 0.5, "score": float(s), "reason": ""} for s in result["scores"] ]+ if not isinstance(results_list, list):+ return refuse("unexpected_shape", "results is not a list")++ if len(results_list) != len(articles):+ return refuse(+ "count_mismatch",+ f"{len(results_list)} results for {len(articles)} articles",+ ) 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"))+ for article, entry in zip(articles, results_list):+ if not isinstance(entry, dict):+ return refuse("invalid_type", f"entry is {type(entry).__name__}")+ try:+ score = max(0.0, min(1.0, float(entry.get("score", 0.5))))+ except Exception:+ return refuse("invalid_type", "score is not numeric")+ relevant = bool(entry.get("relevant", score >= 0.5))+ out.append(verdict(article["id"], relevant, score, str(entry.get("reason", "")))) return ok(out)
Reproduce this run
cd backend EVAL_OFFLINE=1 venv/bin/python -m lab.orchestrate \ --candidate count-guard-v1 --tag clean cd ../web && npm run export:lab -- --check
Source under test
backend/lab/contract/versions/count_guard_v1.pysha256 2c2fb884ebdf38e2… · 4140 bytestranscribed from 81b20198:backend/app/services/openai_service.py
Timeline
1 attempt- 01succeededcompleted 64 cases in 28.9mslocal-known · started 2026-09-22T19:47:57.523Z · ended 2026-09-22T19:47:57.552Z
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
52 scored, 12 not applicable- Recorded cases
- 42real batches, replayed
- Fault-injected
- 10labelled synthetic
- Correct
- 48of the scored cases
- Wrong
- 4see the table
| Case | Group | What happened | Why it is wrong |
|---|---|---|---|
| observed-2026-09-02-005 | observed | wrong-association | no ground truth recorded |
| observed-2026-09-02-035 | observed | wrong-association | no ground truth recorded |
| observed-2026-09-02-041 | observed | wrong-association | no ground truth recorded |
| syn-positional-reordered | synthetic | should-have-refused | equal length, internally reordered — a length check cannot detect this |
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.
- 7/12
On cases outside its declared protocol, did it refuse — or associate anyway?
It produced a complete association on 7 case(s) outside its declared protocol. Under this generation that fails protocol-exclusivity; under generation 1 it was not graded at all.
syn-keyed-in-order, syn-keyed-reversed, syn-keyed-rotated, syn-unknown-id, syn-score-nan, syn-score-out-of-range, syn-relevant-not-bool
Provenance
What can and cannot be established- Executed at revision
- 4e8bee7625820107e84184f8e24c6bd7125f2e86+dirtyrecorded when the harness ran, not re-derived at export
- Inputs sha256
- ab8320e5dd1220c1cases, 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. 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/
The full artifact, as published
Validated against the schema in web/lib/lab/artifact.ts before it was written. Download the JSON.