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>
This commit is contained in:
parent
bcf3337d8a
commit
ce4c15e6cb
3 changed files with 423 additions and 0 deletions
|
|
@ -28,6 +28,7 @@ _MAF_FREE_MODULES = [
|
|||
"hitl.py",
|
||||
"notify.py",
|
||||
"semretrieval.py",
|
||||
"proposal_review.py",
|
||||
]
|
||||
|
||||
_EXAMPLES_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples"
|
||||
|
|
|
|||
205
tests/test_proposal_review_loop_loadbearing.py
Normal file
205
tests/test_proposal_review_loop_loadbearing.py
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue