portfolio-optimiser/tests/test_right_requirement_loadbearing.py
Kjell Tore Guttormsen 938a1ca30e feat(row6): a proposal whose approach declared no requirement is unsupported
Stress round 6 validated three falsification arms, and every validated
approach rested only on run-level declarations nobody can attribute to one
approach. declare_requirement now takes a required approach_id (a mandate
id or own-proposal; an unknown id is refused naming the valid ones), and a
ValidatedProposal whose approach has neither a mandate requirement nor a
declaration under its own id becomes validator.Unsupported - a Rejection
subclass carrying the validator's own ruling, reported as `unsupported` in
coverage, the outcome artefact, the settlement and the judge, and never
counted or summed. The rule is active whenever the debate held the
declaration tool, the micro base included; the road and pre-pass paths are
untouched. Declaration quality is not judged, so the rule can be satisfied
by declaring any document the run read.

The v1 gate's row 6 probes pass; its artefact half reads IKKE MÅLT because
stress round 6 predates approach-addressed declarations, and IKKE MÅLT is
never green - it fails the exit code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 16:40:54 +02:00

256 lines
12 KiB
Python

"""P20 DEL A — the requirement that is RIGHT, and the commission's criteria reaching the reader.
**The measured silence, three rounds and one multi-base pass deep.** P19 DEL A made a direction
NAME the requirement that binds it and made the run refuse a declaration naming a path it never
opened. The declarations then happened — and MEASURED (P19 round 3: 9 declarations over 5 paid
runs; P17b: 4 over one two-base pass) **not one of them named a fasit concept**. The rung works;
what nothing asked for was that the requirement be the RIGHT one. Two halves were missing:
* the tool answered ``{"declared": true, ...}`` to every accepted declaration, echoing back the
caller's own three arguments. A model that had declared a requirement about something else was
told, in the only words it got, that it had succeeded;
* the commission's own statement of what a good answer looks like — ``Mandate.success_criteria`` —
reached ``announce`` and NOTHING else (P19 F2). It was printed for a human and withheld from the
only reader who could act on it.
What each arm pins:
(a) A1 — the reply carries the DOCUMENT's own ``title`` and ``req_number``, read off the base, plus
the sentence that says what the declaration BINDS. Without the document's own words the reply
cannot contradict a wrong declaration, which is the whole correction;
(b) A1 — a path the base carries as no navigated concept (``index.md`` is the reachable case)
answers with empty strings rather than raising: the read trace has already accepted the
declaration, and turning "I cannot restate your title" into a refusal would fail a declaration
the run's own evidence proves was read;
(c) A1 — the reply is read off ``Bundle.context_files``, so a ``type: verdict`` document can never
be named back by title. The one layer no listing names stays unnamed even in an answer;
(d) A2 — ``criteria_block`` is the ONE renderer, and it is EMPTY without criteria. That omission is
what keeps every prompt of every un-commissioned run byte-identical, the demo's included;
(e) A2 — the criteria reach the DEBATE's task message, the prompt where ``declare_requirement`` is
available. Asserted on the text the client actually received, never on the composer;
(f) A2 — a run with NO mandate sends the task message byte-identically to before, which is the
half that keeps ``demo-transcript.stdout`` unchanged and is asserted here rather than left to
the golden;
(g) A3 — the navigator instruction and the tool description both name the ``read_dir(filter=...)``
route with a worked example. A description that lies about the body IS the model's instruction
(the Fase-3 class), and here the body is new.
"""
from __future__ import annotations
from collections.abc import Sequence
from pathlib import Path
from typing import Any
import pytest
from portfolio_optimiser import explore, okf
from portfolio_optimiser.explore import DeclaredRequirement, ToolCall, navigator_tools
from portfolio_optimiser.mandate import Approach, Mandate, criteria_block
_EXAMPLES = Path(__file__).resolve().parents[1] / "shared" / "examples"
_TUNNEL = _EXAMPLES / "tunnel-hauglia"
_BYGG = _EXAMPLES / "bygg-energi-mikro"
_PID = "BYGG-KONTOR-NORD"
def _wired(bundle_dir: Path) -> tuple[dict[str, Any], list[ToolCall], list[DeclaredRequirement]]:
opened: list[ToolCall] = []
declared: list[DeclaredRequirement] = []
tools = navigator_tools((str(bundle_dir),), opened=opened, requirements=declared)
return {t.name: t for t in tools}, opened, declared
def _declare(
tools: dict[str, Any],
opened: list[ToolCall],
base: str,
path: str,
*,
also: Sequence[str] = (),
) -> dict[str, Any]:
"""Read it the way a run does, then declare it — the (b) path of P19 DEL A.
``also`` carries the OTHER documents the run opened. P21/C1 made "it opened enough of the base
to have looked" part of the declaration's precondition, capped by the base's own size, so a
crafted two-document base needs no extras while a five-document example does.
"""
for other in also:
opened.append(ToolCall(name="read_file", bundle_id=base, path=other))
opened.append(ToolCall(name="read_file", bundle_id=base, path=path))
return tools["declare_requirement"].func(
approach_id="a1", bundle_id=base, path=path, ref="Krav 4.1.2-1"
)
def _base_with_a_requirement(root: Path) -> Path:
"""A base declaring its own ``req_number`` — the form the delivered N corpora carry.
Crafted rather than taken from ``shared/examples``: MEASURED, no example base declares a
``req_number`` at all, so an arm built on one of them could not tell a reply that reads the
document from one that returns an empty string.
"""
base = root / "n-mini"
base.mkdir()
(base / "index.md").write_text(
"---\nbundle_id: n-mini\n---\n\n- [Krav](krav.md) — one requirement.\n", encoding="utf-8"
)
(base / "krav.md").write_text(
"---\ntype: Krav\ntitle: Krav 4.1.2-1 Rundkjoring\nreq_number: Krav 4.1.2-1\n"
"seksjon: '4.1.2'\n---\n\nEn rundkjoring skal ha …\n",
encoding="utf-8",
)
return base
# ---------------------------------------------------------------------------------------------
# (a)-(c) the reply the model can be contradicted by
# ---------------------------------------------------------------------------------------------
def test_the_reply_carries_the_documents_own_title_and_number(tmp_path: Path) -> None:
"""(a) The base's words, not the caller's — the only thing that can say "wrong one"."""
base = _base_with_a_requirement(tmp_path)
tools, opened, declared = _wired(base)
answer = _declare(tools, opened, "n-mini", "krav.md")
assert answer["title"] == "Krav 4.1.2-1 Rundkjoring"
assert answer["req_number"] == "Krav 4.1.2-1"
assert "is the requirement the proposal rests on" in answer["binds"]
assert declared == [
DeclaredRequirement(
bundle_id="n-mini", path="krav.md", ref="Krav 4.1.2-1", approach_id="a1"
)
]
def test_a_path_that_is_no_concept_answers_with_empty_strings(tmp_path: Path) -> None:
"""(b) Accepted-but-unrestatable is not a refusal; the read trace already ruled."""
base = _base_with_a_requirement(tmp_path)
tools, opened, _declared = _wired(base)
answer = _declare(tools, opened, "n-mini", "index.md")
assert answer["declared"] is True
assert (answer["title"], answer["req_number"]) == ("", "")
def test_a_verdict_document_is_never_named_back_by_title(tmp_path: Path) -> None:
"""(c) Read off ``context_files``, the property that drops the ``type: verdict`` layer."""
base = _base_with_a_requirement(tmp_path)
(base / "dom.md").write_text(
"---\ntype: verdict\ntitle: A PRIOR EXPERT JUDGEMENT\nid: v1\n---\n\napproved.\n",
encoding="utf-8",
)
tools, opened, _declared = _wired(base)
answer = _declare(tools, opened, "n-mini", "dom.md")
assert answer["declared"] is True
assert answer["title"] == "", "the verdict layer must not be readable through this answer"
# The control: the SAME base answers the concept's title, so the empty string above is the
# layer being excluded rather than the reader being broken.
assert _declare(tools, opened, "n-mini", "krav.md")["title"] == "Krav 4.1.2-1 Rundkjoring"
def test_the_tunnel_example_answers_its_own_title_and_no_number() -> None:
"""(a) control on a REAL example base: title present, reference number honestly absent."""
tools, opened, _declared = _wired(_TUNNEL)
files = [f.name for f in okf.navigate_bundle(str(_TUNNEL)).context_files]
path = files[0]
answer = _declare(tools, opened, "tunnel-hauglia", path, also=files[1:3])
assert answer["title"].startswith("Kilder: tunnelbelysning")
assert answer["req_number"] == "", "this base declares none, and the reply must not invent one"
# ---------------------------------------------------------------------------------------------
# (d)-(f) the criteria reaching the prompt
# ---------------------------------------------------------------------------------------------
def test_the_criteria_renderer_is_empty_without_criteria() -> None:
"""(d) Omission, never an empty heading — the ``announce`` rule, and the golden's guarantee."""
assert criteria_block("") == ""
rendered = criteria_block("a saving the price schedule can carry")
assert "a saving the price schedule can carry" in rendered
assert rendered.startswith("\n")
def test_the_commissions_criteria_reach_the_debate_task(monkeypatch: pytest.MonkeyPatch) -> None:
"""(e) Asserted on what the CLIENT received, never on the composer."""
sent = _run_and_capture_task(
monkeypatch,
mandate=Mandate(
objective="cut cost",
approaches=(Approach(id="a1", label="L", description="D"),),
success_criteria="SENTINEL-CRITERION-ONLY-THE-COMMISSION-CARRIES",
),
)
assert "SENTINEL-CRITERION-ONLY-THE-COMMISSION-CARRIES" in sent[0]
assert "What the commissioner counts as success" in sent[0]
def test_a_run_without_a_mandate_sends_the_task_unchanged(monkeypatch: pytest.MonkeyPatch) -> None:
"""(f) The byte-identity half. Without it (e) could be satisfied by always appending."""
sent = _run_and_capture_task(monkeypatch, mandate=None)
assert sent[0].startswith(f"Find a cost-saving measure for {_PID}.\nContext:\n")
def _run_and_capture_task(monkeypatch: pytest.MonkeyPatch, *, mandate: Mandate | None) -> list[str]:
"""Drive a real ``run_project`` and capture every prompt the debate's clients received.
The same recording-factory seam ``test_debate_navigation_cost_loadbearing`` measures S2c with:
the assertion is on what the CLIENT was handed, never on ``criteria_block``'s return value, so
a renderer wired nowhere cannot satisfy it.
"""
import asyncio
from agent_framework import BaseChatClient
from portfolio_optimiser.run import run_project
from portfolio_optimiser.simulation import ScriptedChatClient
sink: list[str] = []
valid = (
'{"measure":"LED-retrofit","affected_items":'
'[{"code":"ENERGI-TOTAL-EL","quantity":300000,"unit_cost":1.0}],'
'"claimed_saving_nok":30000}'
)
def factory(role: str) -> BaseChatClient:
client = ScriptedChatClient(
"Reasoning holds.\nVERDICT: APPROVE" if role == "checker" else valid, role=role
)
original = client._inner_get_response # type: ignore[attr-defined]
def recording(*, messages, options, stream=False, **kwargs): # type: ignore[no-untyped-def]
sink.append("\n".join(getattr(m, "text", "") or "" for m in messages))
return original(messages=messages, options=options, stream=stream, **kwargs)
client._inner_get_response = recording # type: ignore[attr-defined,method-assign]
return client
asyncio.run(
run_project(
_PID,
"local",
docs_dir=str(_BYGG),
bundle_dir=str(_BYGG),
verdict_input={"decision": "approved", "rationale": "expert reviewed (test)"},
client_factory=factory,
mandate=mandate,
)
)
assert sink, "the debate never reached the client"
return sink
# ---------------------------------------------------------------------------------------------
# (g) the instruction that describes the new body
# ---------------------------------------------------------------------------------------------
def test_both_descriptions_name_the_filter_route() -> None:
"""(g) A description that lies about the body IS the instruction (the Fase-3 class)."""
instruction = explore._INSTRUCTIONS[explore.HYPOTHESISER_ROLE]
assert "read_dir a 'filter' word" in instruction
assert "Krav 4.1.2-1" in instruction, "the worked example is what makes the route concrete"
tools = {t.name: t for t in navigator_tools((str(_TUNNEL),), opened=[], requirements=[])}
description = tools["declare_requirement"].description or ""
assert "filter='rundkjoring'" in description
assert "own title and number" in description