Retire exercise_status: maturity is the only notion; add CLI tests (RBT-WP-0009-T01)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 295952@bnt-lap001
Assistant-Session: e93f64ad-516c-46eb-9666-aad8d300c477
This commit is contained in:
tegwick 2026-09-29 21:28:56 +02:00
parent b89e8eaa74
commit 6d3414d8b7
29 changed files with 444 additions and 218 deletions

View file

@ -44,19 +44,19 @@ class Run:
def path(self) -> Path:
return runs_dir(self.pack) / f"{self.data['run_id']}.json"
def save(self, allow: tuple = ()) -> None:
"""Write the record. A closed run is locked: nothing may change except the keys named in `allow`."""
def save(self) -> None:
"""Write the record. A closed run is locked: nothing may change."""
disk = _read_json(self.path)
if disk and disk.get("outcome"):
keys = set(disk) | set(self.data)
changed = {k for k in keys if disk.get(k) != self.data.get(k)} - {"updated"} - set(allow)
changed = {k for k in keys if disk.get(k) != self.data.get(k)} - {"updated"}
if changed:
raise RunError(f"this run is closed and locked ({disk['outcome']}); it cannot be changed "
f"({', '.join(sorted(changed))})")
self.data["updated"] = _now()
self.path.parent.mkdir(parents=True, exist_ok=True)
if self.path.exists():
self.path.chmod(0o644) # a sealed file is read-only; the allowed update reopens it briefly
self.path.chmod(0o644) # a sealed file is read-only; only the closing write reaches here
self.path.write_text(json.dumps(self.data, indent=2, sort_keys=True) + "\n")
if self.data.get("outcome"):
self.path.chmod(0o444)
@ -270,25 +270,6 @@ def skip(run: Run, step_id: str, reason: str) -> None:
run.save()
STATUS_LINE = re.compile(r"^exercise_status:.*$", re.M)
def exercise_update(run: Run, today: date | None = None) -> tuple[str, str]:
"""Return (old, new) pack.yaml text with exercised status. Does not write.
A run counts only if every step is done (skips do not exercise the pack).
"""
if any(v["state"] != "done" for v in run.data["steps"].values()):
raise RunError("every step must be done (not skipped) to exercise the pack")
if run.data.get("deviations"):
raise RunError("a run with deviations (overrides, waivers, push-through) cannot exercise the pack")
today = today or date.today()
old = run.pack.path.read_text()
new = STATUS_LINE.sub(f"exercise_status: exercised {today.isoformat()} by {run.data['operator']}",
old, count=1)
return old, new
RATINGS = {"good": ":)", "neutral": ":|", "poor": ":("}
@ -379,7 +360,8 @@ STALE_DAYS = 7
def _seal_of(data: dict) -> str:
"""Fingerprint of a closed record. Timestamps of allowed later updates and the exercise marker are not part of it."""
"""Fingerprint of a closed record. `updated` is not part of it, nor is `exercise_recorded`, which records closed
before exercise recording was retired may still carry."""
import hashlib
body = {k: v for k, v in data.items() if k not in ("seal", "updated", "exercise_recorded")}
return hashlib.sha256(json.dumps(body, sort_keys=True).encode()).hexdigest()
@ -503,16 +485,3 @@ def scorecard(pack: Pack, now: datetime | None = None, stale_days: int = STALE_D
card["fastest_seconds"] = durations[0] if durations else None
card["timed_runs"] = len(durations)
return card
def apply_exercise(run: Run, today: date | None = None) -> str:
"""Write 'exercised <date> by <operator>' into pack.yaml for a succeeded run. Returns the new status."""
if run.data.get("outcome") != "succeeded":
raise RunError("only a run finished as succeeded can exercise the pack")
_, new = exercise_update(run, today)
run.pack.path.write_text(new)
status = STATUS_LINE.search(new).group(0).split(": ", 1)[1]
run.pack.data["exercise_status"] = status
run.data["exercise_recorded"] = status
run.save(allow=("exercise_recorded",)) # the one thing a closed run may still receive
return status