feat(major2): terminal proposal reviewer - text never repr, closed vocabulary, EOF is never a sign-off [skip-docs]
Ordre 20260904T173146Z-8102814273-from-portfolio-optimiser, steg 5 av 10.
terminal_proposal_reviewer er et SOESKEN av explore.terminal_plan_reviewer ved FORM -
kopiert, ikke delt: en felles "terminal reviewer"-abstraksjon over to doerer er den
enkeltbruks-generaliseringen repoet nekter til en tredje doer finnes.
Kandidaten rendres som TEKST fra den typede IR-en (BLOCKER-1): maal, kostlinjer, krevd
besparelse, validatorens persentiler, checkerens dom (D3) og attempts remaining. Begge
halvdeler er gatet - en POSITIV sentinel bare tekst-stien kan sende, og den NEGATIVE
formen paa selve defekten (" object at 0x"), fordi den positive alene ville vaert
tilfreds med en renderer som printer ingenting.
Stroemmene resolveres ved KALL-tid, ikke i fabrikken.
Fail-closed paa ekspertens EGEN input: skrivefeil, blank linje og bar "revise" spoerres
paa nytt; D1(a) nekter en revise som ikke kan kjoepes AT THE DOOR med et faktum, aldri
med et botemiddel CLI-en ikke kan utfoere (det finnes ingen --max-attempts, og D1 legger
ingen til) - en tredje doer-TILSTAND, ikke et tredje ord. EOF reiser
ProposalReviewInputError: aa lese stillhet som godkjenning ville latt en kjoering baere
en kandidat ingen signerte, usynlig.
RODT foer impl: fem armer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
59f3fce20a
commit
c391fb5d67
2 changed files with 193 additions and 1 deletions
|
|
@ -24,7 +24,9 @@ to today, golden transcript included.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
|
@ -939,3 +941,105 @@ async def test_the_review_artefact_is_invisible_to_the_hitl_registry(tmp_path: P
|
|||
with_artefact = hitl.pending(str(outbox), str(inbox))
|
||||
_reviews_artefact(outbox).unlink()
|
||||
assert len(hitl.pending(str(outbox), str(inbox))) == len(with_artefact) == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# Group C (Step 5) — the terminal door, driven directly with in-memory streams.
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _request(
|
||||
*, attempts_remaining: int = 2, label: str = "Night setback"
|
||||
) -> pr.ProposalReviewRequest:
|
||||
return pr.ProposalReviewRequest(
|
||||
project_id=_PID,
|
||||
approach_id="a2",
|
||||
approach_label=label,
|
||||
attempt=0,
|
||||
attempts_remaining=attempts_remaining,
|
||||
proposal=_validated(p50=17_500.0),
|
||||
checker_verdict="approve",
|
||||
)
|
||||
|
||||
|
||||
def _door(text: str) -> tuple[pr.ProposalReviewer, io.StringIO]:
|
||||
out = io.StringIO()
|
||||
return pr.terminal_proposal_reviewer(stream_in=io.StringIO(text), stream_out=out), out
|
||||
|
||||
|
||||
def test_t14_the_candidate_is_shown_as_text_and_never_as_a_repr() -> None:
|
||||
"""T14 — BLOCKER-1's lesson, applied at construction. Detach point: rendering the request or
|
||||
the proposal with ``str()`` (M25).
|
||||
|
||||
An expert who answers ``approve`` on ``<...ValidatedProposal object at 0x10d...>`` has signed
|
||||
blindly. Both halves are asserted: a POSITIVE sentinel only the text path can emit (the
|
||||
measure, the claimed saving, the percentile), and the NEGATIVE shape of the defect itself —
|
||||
the positive alone would be satisfied by a renderer that prints nothing."""
|
||||
door, out = _door("approve\n")
|
||||
door(_request())
|
||||
rendered = out.getvalue()
|
||||
assert "LED-retrofit" in rendered # the measure, from the typed IR
|
||||
assert "30000" in rendered # the claimed saving
|
||||
assert "17500" in rendered # the validator's p50
|
||||
assert "Night setback" in rendered # WHICH candidate is being asked about
|
||||
assert "attempts remaining: 2" in rendered
|
||||
assert "checker: approve" in rendered
|
||||
assert "approve" in rendered and "revise" in rendered
|
||||
assert _REPR_LEAK not in rendered
|
||||
|
||||
|
||||
def test_t15_anything_outside_the_closed_vocabulary_is_asked_again() -> None:
|
||||
"""T15 — fail-closed on the expert's OWN input. Detach point: treating any non-revise line as
|
||||
an approval (M17).
|
||||
|
||||
Three things are re-asked and none of them is a decision: a typo, a blank line, and a bare
|
||||
``revise`` (which says nothing to revise). Validation, never repair."""
|
||||
door, out = _door("yes please\n\nrevise\nrevise F\napprove\n")
|
||||
first = door(_request())
|
||||
second = door(_request())
|
||||
assert first == pr.ProposalReviewDecision.revise("F")
|
||||
assert second == pr.ProposalReviewDecision.approve()
|
||||
rendered = out.getvalue()
|
||||
assert rendered.count("PROPOSAL REVIEW") == 2
|
||||
assert rendered.count("Not an answer:") == 3
|
||||
|
||||
|
||||
def test_the_door_refuses_a_revise_it_cannot_buy_and_says_why() -> None:
|
||||
"""D1(a) — the THIRD door state: a re-ask, not a third word. Detach point: letting the door
|
||||
return a revise with nothing left to buy.
|
||||
|
||||
The line states a FACT ("no attempts remain for this candidate; answer approve") rather than
|
||||
offering a remedy this CLI cannot perform — there is no ``--max-attempts`` flag, and D1 adds
|
||||
none. The expert still SEES the candidate, so silence is never read as consent."""
|
||||
door, out = _door("revise F\napprove\n")
|
||||
decision = door(_request(attempts_remaining=0))
|
||||
assert decision == pr.ProposalReviewDecision.approve()
|
||||
assert "no attempts remain for this candidate; answer approve" in out.getvalue()
|
||||
|
||||
|
||||
def test_t16_end_of_input_is_never_a_sign_off() -> None:
|
||||
"""T16 (unit) — EOF raises. Detach point: reading end of input as an approval (M15).
|
||||
|
||||
Reading silence as approval would let a run carry a candidate nobody signed for, and do it
|
||||
invisibly. The message names the flag and the condition so the CLI's ``run stopped:`` line can
|
||||
print it verbatim."""
|
||||
door, _ = _door("")
|
||||
with pytest.raises(pr.ProposalReviewInputError) as excinfo:
|
||||
door(_request())
|
||||
assert "end of input" in str(excinfo.value)
|
||||
assert "--proposal-review" in str(excinfo.value)
|
||||
|
||||
|
||||
def test_t20_the_streams_are_resolved_at_call_time(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""T20 — the ``shared_root()`` idiom, F4's measurement. Detach point: capturing
|
||||
``sys.stdin``/``sys.stdout`` at construction (M21).
|
||||
|
||||
A closure that bound the streams when the factory ran could not be driven by a caller — or a
|
||||
test — that replaces them afterwards, and the only way left to exercise the door would be a
|
||||
subprocess. The reviewer is built FIRST, the streams swapped AFTER, and only then called."""
|
||||
door = pr.terminal_proposal_reviewer()
|
||||
out = io.StringIO()
|
||||
monkeypatch.setattr(sys, "stdin", io.StringIO("revise later\n"))
|
||||
monkeypatch.setattr(sys, "stdout", out)
|
||||
assert door(_request()) == pr.ProposalReviewDecision.revise("later")
|
||||
assert "PROPOSAL REVIEW" in out.getvalue()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue