feat(major2): prior_feedback is the third composable block of the hypothesis prompt [skip-docs]

Ordre 20260904T173146Z-8102814273-from-portfolio-optimiser, steg 2 av 10.

_build_messages faar prior_feedback (keyword-only, None => byte-identisk base-prompt,
samme kontrakt prior_rejection og approach alt oppgir). Ekspertens ORD, ordrett - aldri
forrige forslags JSON, av samme grunn prior_rejection kun baerer grunnen.

Blokken beskriver noe annet enn en avvisning: en kandidat validatoren AKSEPTERTE og et
menneske likevel ba om aa endre. Aa slaa dem sammen ville fortalt modellen at maskinen
protesterte da en person gjorde det.

Rekkefoelgen er fast: base -> approach-hode -> avvisning -> tilbakemelding. En prompt
kan lovlig baere BEGGE - det er forsoeket etter en revise hvis kjoepte forsoek validatoren
saa avviste: menneskets instruks STAAR til mennesket svarer neste gang, mens maskinens
grunn er per forsoek (kun den nyeste, som i dag).

RODT foer impl paa tre armer (TypeError: uventet keyword). Kontrollen (None => byte-identisk)
er halvdelen som holder hver eksisterende kjoering, golden og nav-fixtur uroert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-05 06:46:49 +02:00
commit d4c8691326
2 changed files with 92 additions and 3 deletions

View file

@ -28,10 +28,13 @@ import json
from pathlib import Path
import pytest
from spikes._harness import message_texts
from portfolio_optimiser import proposal_review as pr
from portfolio_optimiser.generate import _build_messages
from portfolio_optimiser.ir import AffectedItem, SavingsProposal
from portfolio_optimiser.validator import ValidatedProposal
from portfolio_optimiser.reference_domain import load_reference_projects
from portfolio_optimiser.validator import Rejection, ValidatedProposal
_REPO = Path(__file__).resolve().parents[1]
_BUNDLE_DIR = _REPO / "shared" / "examples" / "bygg-energi-mikro"
@ -203,3 +206,71 @@ def test_the_payload_carries_every_field_and_takes_its_key_from_the_injected_fun
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
# ---------------------------------------------------------------------------------------------
# Group A1 (Step 2) — ``prior_feedback``: the third composable block of the hypothesis prompt.
# ---------------------------------------------------------------------------------------------
@pytest.fixture(scope="module")
def project():
return load_reference_projects()[0] # FV42-GSV-E1
def test_prior_feedback_none_keeps_the_base_prompt_byte_identical(project) -> None:
"""CONTROL: the new block is INERT when nobody reviewed. A test that can only go green
proves nothing, and this is the half that keeps every existing run, golden and nav-fixture
untouched the contract ``prior_rejection`` and ``approach`` already state."""
base = message_texts(_build_messages(project, "ctx"))[0]
assert base == message_texts(_build_messages(project, "ctx", prior_feedback=None))[0]
assert _FEEDBACK_SENTINEL not in base
def test_the_experts_words_reach_the_next_prompt_verbatim(project) -> None:
"""Detach point: dropping the ``prior_feedback`` block (M3 — print-and-discard).
VERBATIM and only the text: never the previous proposal JSON. That is the ``prior_rejection``
rule, and it is the same reason the model must address what the human said, not parrot the
candidate they were unhappy with."""
text = message_texts(
_build_messages(
project, "ctx", prior_feedback=f"Use 150000, not 200000. {_FEEDBACK_SENTINEL}"
)
)[0]
assert _FEEDBACK_SENTINEL in text
assert "asked for a revision" in text
assert "Use 150000, not 200000." in text
def test_a_rejection_and_a_standing_feedback_compose_in_order(project) -> None:
"""The two blocks compose, and the ORDER is fixed: base -> approach head -> rejection ->
feedback.
This is the shape T6 measures end to end: a revise whose bought attempt the validator then
rejects leaves the attempt after it carrying BOTH the human's instruction stands until the
human next answers, the machine's reason is per-attempt (only the most recent, as today)."""
rejection = Rejection(proposal=_proposal(), reason="claimed saving 270000 exceeds P90 121057")
text = message_texts(
_build_messages(
project, "ctx", prior_rejection=rejection, prior_feedback=_FEEDBACK_SENTINEL
)
)[0]
assert text.count(rejection.reason) == 1
assert text.count(_FEEDBACK_SENTINEL) == 1
assert text.index("REJECTED by the deterministic validator") < text.index(_FEEDBACK_SENTINEL)
def test_the_feedback_sentinel_cannot_arrive_from_the_knowledge_base() -> None:
"""The known-positive control on every sentinel arm in this file: the marker exists NOWHERE
in the fixture bundle, so a prompt that carries it can only have got it through the expert's
feedback block. Paired with an assertion that the scan actually reads the files a scan that
silently finds nothing makes a gate that can only go green."""
texts = [
path.read_text(encoding="utf-8")
for path in sorted(_BUNDLE_DIR.rglob("*"))
if path.is_file()
]
assert texts, "the fixture bundle must exist for this control to mean anything"
assert any("ENERGI-TOTAL-EL" in text for text in texts) # the scan CAN find a known string
assert not any(_FEEDBACK_SENTINEL in text for text in texts)