relay: a forced step 2 is not a try (attempts 4 -> 3)

phase.step2_attempts counted every step-2 grade with grade "fail",
and a force repeats the last try's grade, so three named-a-tool fails
plus "Continue anyway" showed attempts: 4 in the state. Measured
against the live relay through the app's desktop path; the contract
(and the snapshot's _coach_grades) already exclude forces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-10-23 21:44:12 +02:00
parent 2531f9eeb3
commit a78c1e971b
2 changed files with 17 additions and 2 deletions

View File

@ -102,9 +102,14 @@ def step2_attempts(events: list[dict[str, Any]]) -> int:
A ``fail`` (the list read as code, or named an operation) never reaches
the checker, so it has no ``req_check``; a ``partial`` does, and is
already counted there. CLI and app count one file alike.
already counted there. CLI and app count one file alike. A force
repeats the last try's grade but is not a try, so it is not counted.
"""
fails = sum(1 for e in _step2_grades(events) if e.get("grade") == "fail")
fails = sum(
1
for e in _step2_grades(events)
if e.get("grade") == "fail" and e.get("forced") is not True
)
return req_state(events).tries + fails

View File

@ -84,6 +84,16 @@ def test_step_two_attempts_count_incomplete_lists_and_fail_grades() -> None:
assert phase.step2_attempts(events) == 3
def test_a_force_is_not_a_step_two_try() -> None:
"""A force repeats the last try's grade (`fail`); it is no try itself.
Measured against the live relay 2026-10-23: three named-a-tool fails and
a force showed ``attempts: 4`` in the state.
"""
fails = [_grade(2, "fail"), _grade(2, "fail"), _grade(2, "fail")]
assert phase.step2_attempts([*fails, _grade(2, "fail", forced=True)]) == 3
def _gate(log: Path, answers: list[bool] | None = None) -> phase.PhaseGate:
asked: list[str] = []