portfolio-optimiser/tests/test_proposal_review_loop_loadbearing.py
Kjell Tore Guttormsen ce4c15e6cb feat(major2): proposal-review types, record, error and renderer in a MAF-free module [skip-docs]
Ordre 20260904T173146Z-8102814273-from-portfolio-optimiser, steg 1 av 10.

Vokabularet for MAJOR-2-doeren: forespoerselen en ekspert vises, svaret de gir,
posten som registreres, feilen en stillhet reiser, og de to rendererne.

D5(c): egen MAF-fri modul. Maalt import-grense - generate.py er kallstedet og
importerer agent_framework core, men ikke explore; explore.py (F4-soesknenes hjem)
importerer agent_framework.orchestrations. Typene der ville enten dratt
orkestrerings-importen inn i genererings-stien eller definert typen to ganger.
Gatet av tests/test_okf.py::test_okf_is_maf_free.

Klassen ER kanalen: ProposalReviewInputError er en RuntimeError og IKKE en
ValueError - begge halvdeler assertert, fordi issubclass(X, RuntimeError) alene
staar groenn paa en klasse som arver begge.

verdict_key INJISERES i renderen (key_of), fordi verdicts.py importerer MAF og
run er en sykel herfra - _features_of forblir eneste hjem for regelen.

Notice-en sier fra ogsaa paa NULL anmeldelser naar en reviewer VAR tilbudt: et
bevisst avvik fra announce-regelens null-er-stillhet-halvdel, fordi en operatoer
som ga --proposal-review og ser ingenting ikke kan skille "ingen kandidat ble
validert" fra "doeren hang".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 06:44:56 +02:00

205 lines
9.3 KiB
Python

"""MAJOR-2 — a human must be able to answer the proposal that is ON THE TABLE, and the answer
must be USED.
Measured before this door existed (``docs/2026-09-02-misjonsreview-v2.md`` § 3, re-measured at
HEAD ``da5f10f``): ``grep -n "prior_" generate.py`` finds exactly ONE seam — ``prior_rejection``,
the MACHINE's own falsification fed back inside the same run — and the only human seam in the
loop is ``plan_reviewer``, which fires BEFORE any hypothesis exists. A human's verdict lands in
the NEXT run (Step 7's inbox). So "improved proposals given feedback" was, for a person, a
two-run loop with days between it.
**The gate is the SECOND half of that phrase.** A door that renders the candidate, reads a line
and discards it passes "the expert was asked" and fails the målbilde — this repo's vacuous-gate
class, fifteen times over. Every arm here is therefore built so that an **always-approve
reviewer is RED**: the discriminator is that a ``revise`` reaches the NEXT generation attempt's
prompt VERBATIM, that the proposer answers differently because of it, and that the run carries
the validator's ruling on THAT attempt (D6).
Three things the door must NOT do, gated as hard as the things it must: ``approve`` mints no
``Verdict`` (F2 stands); a ``revise`` never writes ``provenance.validator_decision`` and never
touches the checker gate (the two falsifiers stay unblended — the human is a third voice that
gates nothing except asking for one more attempt); and a run with NO reviewer is byte-identical
to today, golden transcript included.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from portfolio_optimiser import proposal_review as pr
from portfolio_optimiser.ir import AffectedItem, SavingsProposal
from portfolio_optimiser.validator import ValidatedProposal
_REPO = Path(__file__).resolve().parents[1]
_BUNDLE_DIR = _REPO / "shared" / "examples" / "bygg-energi-mikro"
_PID = "BYGG-KONTOR-NORD"
_RUN_ID = "proposal-review-door"
#: A string that can reach a generation prompt ONLY by travelling through the expert's feedback:
#: asserted absent from every file of the fixture bundle and from the base prompt (Step 2), so a
#: prompt that carries it can have got it from exactly one place.
_FEEDBACK_SENTINEL = "PROPOSAL-REVIEW-SENTINEL-9c41ae"
#: The shape of the defect BLOCKER-1 measured, asserted NEGATIVELY: any object rendered by its
#: default repr onto a surface a human reads. Stronger than a positive sentinel alone, which a
#: renderer that prints nothing at all would also satisfy.
_REPR_LEAK = " object at 0x"
def _proposal(*, amount: float = 30000.0, measure: str = "LED-retrofit") -> SavingsProposal:
return SavingsProposal(
project_id=_PID,
measure=measure,
affected_items=[AffectedItem(code="ENERGI-TOTAL-EL", quantity=300000.0, unit_cost=1.0)],
claimed_saving_nok=amount,
)
def _validated(*, amount: float = 30000.0, p50: float = 42.0) -> ValidatedProposal:
return ValidatedProposal(
proposal=_proposal(amount=amount),
p10=p50 - 1.0,
p50=p50,
p90=p50 + 1.0,
nominal_feasible=p50 + 2.0,
)
def _review(
*,
approach_id: str | None = None,
attempt: int = 0,
decision: str = "approve",
feedback: str = "",
honoured: bool = True,
p50: float = 42.0,
) -> pr.ProposalReview:
return pr.ProposalReview(
approach_id=approach_id,
attempt=attempt,
decision=decision, # type: ignore[arg-type]
feedback=feedback,
honoured=honoured,
proposal=_validated(p50=p50),
)
# ---------------------------------------------------------------------------------------------
# Group A0 (Step 1) — the types, the error's CLASS, and the two renderers.
# ---------------------------------------------------------------------------------------------
def test_a_revision_must_say_what_to_revise() -> None:
"""Detach point: ``ProposalReviewDecision.revise`` accepting an empty string. A bare
``revise`` is not a decision — it is a line the door must ask again for. The type refuses it
at construction so no surface can invent the distinction for itself."""
with pytest.raises(ValueError):
pr.ProposalReviewDecision.revise("")
with pytest.raises(ValueError):
pr.ProposalReviewDecision.revise(" ")
assert pr.ProposalReviewDecision.revise("do X").feedback == "do X"
assert pr.ProposalReviewDecision.approve().feedback is None
def test_the_input_error_is_a_runtime_error_and_never_a_value_error() -> None:
"""Detach point: making ``ProposalReviewInputError`` a ``ValueError``. **The class IS the
channel**, decided by three measurements (brief § Constraints): a ``ValueError`` would land
on ``run.py``'s refusal tuple and print ``run refused:`` for a run whose argv was fine and
which had already spent tokens; and it would sit ONE frame from ``_fetch_parsed``'s
``except (ValidationError, ValueError, TypeError)`` catch-all, where a human-input error
would be appended to ``parse_failures`` and the model re-called until the ledger fired.
Both halves are asserted, because ``issubclass(X, RuntimeError)`` alone stays green on a
class that inherits from both."""
assert issubclass(pr.ProposalReviewInputError, RuntimeError)
assert not issubclass(pr.ProposalReviewInputError, ValueError)
def test_the_notice_is_none_exactly_when_no_reviewer_was_offered() -> None:
"""Detach point: a renderer that always returns its line. The ``announce`` rule: a run that
offered no reviewer has nothing to say, and an empty row would read as a dropped line."""
assert pr.proposal_review_notice((), offered=False) is None
assert pr.proposal_review_notice((_review(),), offered=False) is None
assert pr.proposal_review_notice((), offered=True) is not None
def test_zero_reviews_with_a_reviewer_offered_says_so_in_words() -> None:
"""Detach point: returning ``None`` on zero reviews with a reviewer present (M33).
A DELIBERATE departure from the announce rule's zero-is-silence half, and the reason is the
operator: someone who passed ``--proposal-review`` and sees nothing cannot tell "no candidate
was ever validated, so nobody was asked" from "the door hung". The departure is stated in the
invariant row rather than left as an inconsistency."""
line = pr.proposal_review_notice((), offered=True)
assert line == " proposal review offered, never consulted (no candidate validated)"
def test_the_notice_counts_answers_candidates_and_the_unhonoured_ones() -> None:
"""Detach point: dropping the un-honoured tail, or counting entries instead of candidates.
An un-honoured revise (no attempt left to buy) is a FACT the operator must be able to read:
the expert asked for a change that the run could not make."""
reviews = (
_review(approach_id="a1", attempt=0, decision="revise", feedback="F", honoured=True),
_review(approach_id="a1", attempt=1, decision="approve"),
_review(approach_id="a2", attempt=0, decision="revise", feedback="G", honoured=False),
)
assert pr.proposal_review_notice(reviews, offered=True) == (
" proposal review: 3 answer(s) across 2 candidate(s) — 1 approve, 2 revise "
"(1 not honoured)"
)
honoured_only = (
_review(approach_id="a1", attempt=0, decision="revise", feedback="F", honoured=True),
_review(approach_id="a1", attempt=1, decision="approve"),
)
assert pr.proposal_review_notice(honoured_only, offered=True) == (
" proposal review: 2 answer(s) across 1 candidate(s) — 1 approve, 1 revise"
)
def test_the_payload_carries_every_field_and_takes_its_key_from_the_injected_function() -> None:
"""Detach point: dropping ``verdict_key`` (M28), or deriving it inside the module.
``verdicts.verdict_key`` takes ``ProposalFeatures`` and the only derivation from a
``SavingsProposal`` is ``run._features_of`` — but ``verdicts.py`` imports MAF and ``run`` is
a cycle from here, so the key function is INJECTED and ``_features_of`` stays the single
home (kø-(p)). The arm asserts the injected callable is handed the REVIEWED proposal, not a
re-derived one."""
seen: list[SavingsProposal] = []
def key_of(proposal: SavingsProposal) -> str:
seen.append(proposal)
return f"key-{len(seen)}"
reviews = (
_review(approach_id="a1", attempt=0, decision="revise", feedback="F", honoured=False),
_review(approach_id=None, attempt=1, decision="approve", p50=99.5),
)
payload = pr.proposal_reviews_payload(reviews, key_of=key_of)
assert list(payload) == ["reviews"]
assert payload["reviews"] == [
{
"approach_id": "a1",
"attempt": 0,
"decision": "revise",
"feedback": "F",
"honoured": False,
"verdict_key": "key-1",
"p50": 42.0,
},
{
"approach_id": None,
"attempt": 1,
"decision": "approve",
"feedback": "",
"honoured": True,
"verdict_key": "key-2",
"p50": 99.5,
},
]
assert seen == [reviews[0].proposal.proposal, reviews[1].proposal.proposal]
# The payload is a PLAIN mapping: ``outbox`` stays MAF-free and byte-deterministic.
assert json.loads(json.dumps(payload)) == payload