"""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