Skip to content

Commit 3ba67af

Browse files
authored
fix: close lifecycle terminal states (#74)
1 parent be64262 commit 3ba67af

7 files changed

Lines changed: 48 additions & 11 deletions

File tree

src/format_bench/model.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from __future__ import annotations
22

3+
from collections.abc import Mapping
34
from dataclasses import dataclass
45
from enum import StrEnum
56
from pathlib import Path
7+
from types import MappingProxyType
68

79

810
class Lane(StrEnum):
@@ -40,10 +42,18 @@ class ExecutionState(StrEnum):
4042
ExecutionState.FAILED: frozenset(),
4143
}
4244
_FAILURES = frozenset({ExecutionState.UNSUPPORTED, ExecutionState.FAILED})
45+
_ACTIVE = frozenset(
46+
{
47+
ExecutionState.DISCOVERED,
48+
ExecutionState.ENCODED,
49+
ExecutionState.ROUNDTRIP_VERIFIED,
50+
ExecutionState.BENCHMARKED,
51+
}
52+
)
4353

4454

4555
def transition(current: ExecutionState, target: ExecutionState) -> ExecutionState:
46-
allowed = _NEXT[current] | (_FAILURES if current not in _FAILURES else frozenset())
56+
allowed = _NEXT[current] | (_FAILURES if current in _ACTIVE else frozenset())
4757
if target not in allowed:
4858
raise ValueError(f"illegal evidence transition: {current} -> {target}")
4959
return target
@@ -65,7 +75,12 @@ class DatasetSpec:
6575
canonical_hash: str
6676
rows: int
6777
columns: tuple[ColumnSpec, ...]
68-
expected_counts: dict[str, int]
78+
expected_counts: Mapping[str, int]
79+
80+
def __post_init__(self) -> None:
81+
object.__setattr__(
82+
self, "expected_counts", MappingProxyType(dict(self.expected_counts))
83+
)
6984

7085
def asset_path(self, root: Path) -> Path:
7186
path = Path(self.asset_name)

src/format_bench/release.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ def _safe_slug(value: str) -> str:
2929
def package_run(run_dir: Path, output: Path, platform: str) -> Path:
3030
manifest = json.loads((run_dir / "manifest.json").read_text(encoding="utf-8"))
3131
results = json.loads((run_dir / "results.json").read_text(encoding="utf-8"))
32-
if manifest["state"] != ExecutionState.BENCHMARKED:
33-
raise ValueError("release packaging requires benchmarked evidence")
34-
if results["state"] != ExecutionState.BENCHMARKED:
35-
raise ValueError("release packaging requires benchmarked results")
32+
if manifest["state"] != ExecutionState.REPORTED:
33+
raise ValueError("release packaging requires reported evidence")
34+
if results["state"] != ExecutionState.REPORTED:
35+
raise ValueError("release packaging requires reported results")
3636
if manifest["dataset_id"] != results["dataset_id"]:
3737
raise ValueError("release manifest and results dataset mismatch")
3838

src/format_bench/report.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import json
44
from pathlib import Path
55

6-
from .model import Comparability, ExecutionState
6+
from .model import Comparability, ExecutionState, transition
77

88

99
def _cell(value: object) -> str:
@@ -164,8 +164,9 @@ def _prompt(results: dict) -> list[str]:
164164
def render_report(run_dir: Path) -> Path:
165165
manifest = json.loads((run_dir / "manifest.json").read_text(encoding="utf-8"))
166166
results = json.loads((run_dir / "results.json").read_text(encoding="utf-8"))
167-
if manifest["state"] != ExecutionState.BENCHMARKED or results["state"] != ExecutionState.BENCHMARKED:
168-
raise ValueError("report requires benchmarked manifest and results")
167+
reportable = {ExecutionState.BENCHMARKED, ExecutionState.REPORTED}
168+
if manifest["state"] not in reportable or results["state"] not in reportable:
169+
raise ValueError("report requires benchmarked or reported manifest and results")
169170
if manifest["dataset_id"] != results["dataset_id"]:
170171
raise ValueError("manifest and results dataset mismatch")
171172
profile = results["profile"]
@@ -188,4 +189,14 @@ def render_report(run_dir: Path) -> Path:
188189
]
189190
path = run_dir / "report.md"
190191
path.write_text("\n".join(lines), encoding="utf-8")
192+
# LLM contract: BENCHMARKED -> REPORTED after the human-readable evidence exists.
193+
for payload, json_path in (
194+
(manifest, run_dir / "manifest.json"),
195+
(results, run_dir / "results.json"),
196+
):
197+
if payload["state"] == ExecutionState.BENCHMARKED:
198+
payload["state"] = transition(ExecutionState.BENCHMARKED, ExecutionState.REPORTED)
199+
json_path.write_text(
200+
json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8"
201+
)
191202
return path

src/format_bench/workflow.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ def prepare_run(
6464
_write_json(input_dir / "manifest.json", effective)
6565

6666
entries = []
67+
# LLM contract: DISCOVERED -> ENCODED -> ROUNDTRIP_VERIFIED -> BENCHMARKED -> REPORTED.
68+
# Active evidence may terminate as UNSUPPORTED or FAILED; terminal evidence never ranks.
6769
for adapter in selected or adapters():
6870
description = adapter.describe()
6971
artifact_path = destination / "artifacts" / (
@@ -116,6 +118,7 @@ def verify_run(run_dir: Path, selected: dict[str, FormatAdapter] | None = None)
116118
(run_dir / run_manifest["input"]["manifest"]).read_text(encoding="utf-8")
117119
)
118120
registered = selected or adapter_map()
121+
# LLM contract: only ENCODED evidence can advance to ROUNDTRIP_VERIFIED here.
119122
for entry in run_manifest["formats"]:
120123
if entry["state"] != ExecutionState.ENCODED:
121124
continue

tests/test_model.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ def test_lifecycle_rejects_skipped_or_terminal_transitions() -> None:
2727
transition(ExecutionState.DISCOVERED, ExecutionState.BENCHMARKED)
2828
with pytest.raises(ValueError, match="illegal evidence transition"):
2929
transition(ExecutionState.FAILED, ExecutionState.DISCOVERED)
30+
with pytest.raises(ValueError, match="illegal evidence transition"):
31+
transition(ExecutionState.REPORTED, ExecutionState.FAILED)
32+
with pytest.raises(ValueError, match="illegal evidence transition"):
33+
transition(ExecutionState.REPORTED, ExecutionState.UNSUPPORTED)
3034

3135

3236
def test_failure_is_available_from_an_active_state() -> None:
@@ -45,6 +49,8 @@ def test_dataset_asset_path_stays_under_the_run_root() -> None:
4549
expected_counts={"rows": 1},
4650
)
4751
assert spec.asset_path(Path("datasets")) == Path("datasets/source.csv")
52+
with pytest.raises(TypeError):
53+
spec.expected_counts["rows"] = 2
4854

4955
unsafe = DatasetSpec(**{**spec.__dict__, "asset_name": "../source.csv"})
5056
with pytest.raises(ValueError, match="safe relative path"):

tests/test_release.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
def test_release_package_is_deterministic_and_relative(tmp_path: Path) -> None:
1212
run = tmp_path / "run"
1313
(run / "input").mkdir(parents=True)
14-
manifest = {"state": "BENCHMARKED", "dataset_id": "fixture"}
14+
manifest = {"state": "REPORTED", "dataset_id": "fixture"}
1515
results = {
16-
"state": "BENCHMARKED",
16+
"state": "REPORTED",
1717
"dataset_id": "fixture",
1818
"profile": "fair",
1919
"run_id": "run-1",

tests/test_report.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ def test_prompt_report_is_deterministic_and_includes_exact_tokens(tmp_path: Path
4848
first = path.read_text()
4949
assert "| compact_tsv | 10 | 4 | 14 | 3 | 4 |" in first
5050
assert "Direct token counts for binary formats are N/A." in first
51+
assert json.loads((tmp_path / "manifest.json").read_text())["state"] == "REPORTED"
52+
assert json.loads((tmp_path / "results.json").read_text())["state"] == "REPORTED"
5153
assert render_report(tmp_path).read_text() == first
5254

5355

0 commit comments

Comments
 (0)