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:
parent
ce4c15e6cb
commit
d4c8691326
2 changed files with 92 additions and 3 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue