portfolio-optimiser/tests/test_right_requirement_loadbearing.py
Kjell Tore Guttormsen ac0bfdba27 feat(p21): a declaration that must have LOOKED, and a refusal that names the neighbours
C1. Round 4 produced 13 declarations over six runs and NOT ONE named a fasit concept.
The distinct documents opened before each were 1,1,1,1,1,1,1,2,5,5,6,13,13: seven
declared the base's FIRST requirement after opening exactly ONE document.

The order offered two rules and asked which discriminates. Replayed against the real
listings: "the declared document must have come back from a read_dir filtered on a word
from the approach's label" refuses 13 of 13 -- including Soraasen's 12.11, the closest
any run came -- because ZERO of the 13 were reached through a filtered listing at all.
A gate that refuses every measured case, right and wrong alike, cannot discriminate.
"fewer than k distinct documents opened" at k=3 refuses 8 of 13 and keeps the five that
navigated. k=3, 4 and 5 refuse the SAME eight -- the distribution has a gap between 2
and 5 -- so the threshold is not on a cliff, and 3 is the lowest of that plateau.
DISTINCT paths, not calls, and capped by the base's own size so a small base stays
declarable.

C2. Over the same traces 18 of 143 path-bearing calls named a path the base does not
hold, ELEVEN of them one run walking R761/4-3, 4.3, 4-2, 4-1, 4-0, 4-5, 4-6 while the
real names are R761/4, R761/41, R761/42. The refusal already named the nearest listable
ancestor; now it also names up to five of that rung's own subdirectories, ranked by
longest common prefix with the segment that failed. ONE copy shared by both refusal
sites, built from context_files through in_dimension, so every name handed back resolves
and the verdict layer can never be advertised in an apology. MEASURED after: 16 of 18.

Load-bearing MEASURED, four mutations all red against the WHOLE suite, green control
1863/5, golden byte-unchanged: C3(i) the declaration gate detached (2 red) . C3(ii) the
neighbour list empty (6) . C3(iii) built from files (1, the verdict arm alone) . C3(iv)
count CALLS instead of distinct documents (1, the repetition arm alone).

Three existing arms REWRITTEN, not weakened: all three read one document and declared,
which is the measured failure class exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 11:38:57 +02:00

250 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(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")]
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