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>
250 lines
12 KiB
Python
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
|