The context sets, the packaged knowledge bases and the example bundles are replaced by one fictitious example set about IT operations in an invented organisation: three context sets (serverrom-2027, driftsavtale-2027 and the two-base drift-og-avtale-2027), two synthetic knowledge bases under src/portfolio_optimiser/data/kunnskapsbaser and two example bundles under src/portfolio_optimiser/data/bundles. Numbers, codes and structural values in tests and fixtures are kept; names, ids and wording change. Dated measurement documents that only recorded runs on the replaced material are deleted. Gate figures measured on the new set are not comparable with earlier ones. The exclusion gate from the previous commit is green: 0 tracked files hit outside the shared/ subtree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
257 lines
12 KiB
Python
257 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"
|
|
_BUNDLES = Path(__file__).resolve().parents[1] / "src" / "portfolio_optimiser" / "data" / "bundles"
|
|
_KJOLING = _BUNDLES / "driftssenter-kjoling"
|
|
_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 requirement bases 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 Sikkerhetskopi\nreq_number: Krav 4.1.2-1\n"
|
|
"seksjon: '4.1.2'\n---\n\nEn sikkerhetskopi 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 Sikkerhetskopi"
|
|
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 Sikkerhetskopi"
|
|
|
|
|
|
def test_the_cooling_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(_KJOLING)
|
|
files = [f.name for f in okf.navigate_bundle(str(_KJOLING)).context_files]
|
|
path = files[0]
|
|
answer = _declare(tools, opened, "driftssenter-kjoling", path, also=files[1:3])
|
|
assert answer["title"].startswith("Kilder: kjøling")
|
|
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(_KJOLING),), opened=[], requirements=[])}
|
|
description = tools["declare_requirement"].description or ""
|
|
assert "filter='sikkerhetskopi'" in description
|
|
assert "own title and number" in description
|