portfolio-optimiser/tests/test_binding_requirement_loadbearing.py
Kjell Tore Guttormsen c8f0c8f7c4 feat(p20): the requirement that is RIGHT, and a clause number that is not a price
Three seams, one commit: A, B and C touch the same four modules (run.py carries
the debate task, the grounding composition and the announcement; okf.py carries
one reference-number vocabulary read by both A and B), so splitting them into
three commits would have meant hunk-level staging of entangled files. Stated
rather than silently restructured.

A — the declaration answers with the DOCUMENT's own words. Measured: 13
declarations over round 3 and P17b, not one naming a fasit concept, while the
tool answered {"declared": true, ...} by echoing the caller's own arguments. It
now returns the document's title and req_number, read off Bundle.context_files
(so the type: verdict layer can never be named back), plus the sentence saying
what the declaration binds. A path the base carries as no concept answers with
empty strings rather than refusing. The commission's success_criteria now reach
the DEBATE task through mandate.criteria_block, the one renderer, empty when
there are none — which is what keeps every un-commissioned prompt, and the
golden, byte-identical.

B — a clause number is not a price. THE ORDER'S OWN RULE WAS FELLED BY
MEASUREMENT: it asks to refuse a code that IS declared req_number/prosessnr,
and neither of its two known positives is. n500 declares seksjon 10.4.1..10.4.4
but never the bare 10.4; r761 declares 2727 prosessnr and 2753 seksjon, none of
them 1.10.4, which occurs once, as prose ("iht. vegnormal N200 kap. 1.10.4").
The COMPLEMENT fires on both and closes the hole _ground_against_input already
admits in writing -- "it fails OPEN on a coincidental match". Unanchored run +
requirement-shaped code + the base declares a vocabulary + the code is not in
it -> refused, naming the denominator. All five of kontrakt-sorasen's real
process codes ARE declared and pass, which is what keeps the one context set
built on real codes measurable. Replayed over all 24 codes of round 3 + P17b:
exactly the two known positives flip validated -> rejected, 22 unchanged.

C — a parse failure no longer burns the round ledger blind. _fetch_parsed takes
a BUILDER instead of a finished message list, so the retry carries the parse
reason; measured, kontrakt-sorasen-04 spent 11 of 12 rounds re-asking the same
question. And announced_subject names the routed bases instead of saying "the
portfolio" for a two-base commission.

Suite 1807/5 (from 1781, +26, 0 removed), golden demo-transcript.stdout
BYTE-UNCHANGED (shasum -a 1 of the CONTENT = ea8c534773acdbe41ae68f2c55724d69aaf8be4f),
ruff and mypy clean.

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

409 lines
18 KiB
Python

"""P19 DEL A — a direction must NAME the requirement that binds it, and it must have READ it.
**The measured silence, two paid rounds deep.** P16 (`docs/2026-09-12-p14-kontekstsett.md` § 4.1,
`docs/2026-09-14-p18-stressrunde-2.md`) and P18 both scored **0 of 26** fasit concepts opened —
the same number twice, over four and then five paid runs, while every run still produced proposals
the deterministic gate then judged. P18 closed the navigation side of it (a listing is a window; an
invented path is refused by name) and the number did not move at all. That is what turns it from a
LADDER question into a ROLE one: nothing anywhere in the loop ever asked the model to say what
requirement of the corpus binds the direction it committed to, so opening one was never on the
critical path of producing an answer.
**A PREMISE OF THE ORDER WAS FELLED BEFORE ANYTHING WAS BUILT ON IT.** A1 places the demand in
``_INSTRUCTIONS[HYPOTHESISER_ROLE]`` alone. MEASURED: the stress command (P18 D1, repeated
verbatim in P19 E1) passes ``--mandate`` and NOT ``--explore``, the two are refused together by
name, and not one of the nine round-1/round-2 outboxes holds a ``{run_id}-exploration.json``. The
hypothesiser therefore never runs in a stress round, and a demand that only it can carry would be
structurally inert in exactly the paid runs this order commissions — A3 ("the requirement reaches
the proposal") unreachable along with it.
A2's own sentence is what resolves it: the refusal must go to the model "som en tur den kan rette
(samme mekanisme som ``quick_validate``s nekt), ikke som en ``raise``". ``quick_validate`` is a
TOOL. So the demand is a tool — ``declare_requirement`` — and it lives in ``navigator_tools``,
which since S2c is held by BOTH roles that navigate: the exploration's navigator/hypothesiser and
the debate's proposer/checker. One instruction, one refusal, one record, two doors.
What each arm pins, and what it refuses:
(a) the refusal itself — a declaration naming a path this run never opened comes back REFUSED, in
the funn-99 form (``refusal`` carries the kind ``RequirementNotRead``), and NOTHING is
recorded. Without it the tool accepts any string and the whole demand is decorative;
(b) the correction works — the same path, once ``read_file`` has actually returned it, is accepted
and recorded. A gate that can only refuse is the mirror image of one that can only pass, and
both prove nothing;
(c) an unknown base is refused by the SAME index every read rung uses, so a declaration cannot
name a corpus this run was never given;
(d) the tool EXISTS only when the caller offers both sinks — which is what keeps every other
``navigator_tools`` call site byte-identical — and passing one without the other is refused at
construction, because a sink with no read trace would accept every declaration (the repo's
vacuous-gate class, stated in advance rather than discovered);
(e) the marked hypothesis carries ``requirement`` as a REQUIRED key: omitted is a hard error of the
same class as an unreadable marked line, explicit ``null`` is legal and needs ``why_none``, and
a half-named requirement is refused (validation, never repair);
(f) a MINTED approach carries it and a SEED does not acquire one — § C.6 door 1 is a preservation
rule, and filling the field in on an expert's behalf would put their name on a claim about the
corpus they did not make;
(g) A3 — the requirement reaches the proposer's prompt VERBATIM, and a prompt whose approach has
none is byte-identical to before. That second half is what keeps ``demo-transcript.stdout``
unchanged, and it is asserted here rather than left to the golden;
(h) A4 — the judge counts a hit against THIS APPROACH'S fasit concepts, never against the base. A
judge matching the whole base would mark every declaration a hit on a 2 756-document corpus,
which is the P16 vacuity ``citation_scope`` already exists to refuse;
(i) the declaration reaches ``{run_id}-debate.json``, so a paid run's evidence survives the process
that produced it.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import pytest
from portfolio_optimiser import explore, stress
from portfolio_optimiser.explore import (
DeclaredRequirement,
HypothesisParseError,
ToolCall,
navigator_tools,
)
from portfolio_optimiser.generate import _build_messages
from portfolio_optimiser.mandate import Approach, BindingRequirement
from portfolio_optimiser.reference_domain import Project
_EXAMPLES = Path(__file__).resolve().parents[1] / "shared" / "examples"
_TUNNEL = _EXAMPLES / "tunnel-hauglia"
def _wired(
bundle_dir: Path = _TUNNEL,
) -> tuple[dict[str, Any], list[ToolCall], list[DeclaredRequirement]]:
"""The tools as a RUN holds them: the declaration rung reading the recorder's own trace."""
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 _a_concept(bundle_dir: Path = _TUNNEL) -> str:
"""One real concept path in the base, taken from the navigated listing rather than guessed."""
from portfolio_optimiser import okf
return okf.navigate_bundle(str(bundle_dir)).context_files[0].name
# ---------------------------------------------------------------------------------------------
# (a)-(c) the tool
# ---------------------------------------------------------------------------------------------
def test_a_requirement_the_run_never_opened_is_refused_and_recorded_nowhere() -> None:
"""(a) The declaration's one falsifier is the run's own read trace."""
tools, _opened, declared = _wired()
answer = tools["declare_requirement"].func(
bundle_id="tunnel-hauglia", path=_a_concept(), ref="12.1"
)
assert answer["refusal"] == "RequirementNotRead"
assert "0 document(s)" in answer["refused"]
assert declared == [], "a refused declaration must leave no record"
def test_the_correction_is_to_read_it_and_then_it_is_accepted() -> None:
"""(b) The refusal is a turn the model can correct — the half that makes (a) a gate."""
tools, opened, declared = _wired()
path = _a_concept()
body = tools["read_file"].func(bundle_id="tunnel-hauglia", path=path)
assert not body.startswith("REFUSED"), body[:120]
# The recorder is middleware in a real run; here the trace is appended directly, which is the
# SAME list the tool reads.
opened.append(ToolCall(name="read_file", bundle_id="tunnel-hauglia", path=path))
answer = tools["declare_requirement"].func(
bundle_id="tunnel-hauglia", path=path, ref="Krav 12.1"
)
# P20/A1 widened the reply: the three arguments PLUS the document's own title and number and
# the sentence saying what the declaration binds. Asserted key by key rather than by equality,
# because an exact-dict assert here would fail on every future field while saying nothing about
# the one property this arm exists for — that the declaration was ACCEPTED and RECORDED.
assert answer["declared"] is True
assert (answer["bundle_id"], answer["path"], answer["ref"]) == (
"tunnel-hauglia",
path,
"Krav 12.1",
)
assert set(answer) == {"declared", "bundle_id", "path", "ref", "title", "req_number", "binds"}
assert declared == [DeclaredRequirement(bundle_id="tunnel-hauglia", path=path, ref="Krav 12.1")]
def test_a_declaration_naming_an_unknown_base_is_refused() -> None:
"""(c) The same base index every read rung uses, so no second, laxer resolution exists."""
tools, opened, declared = _wired()
opened.append(ToolCall(name="read_file", bundle_id="nope", path="a.md"))
answer = tools["declare_requirement"].func(bundle_id="nope", path="a.md", ref="1")
assert answer["refusal"] == "ExplorationError"
assert declared == []
def test_the_rung_exists_only_when_both_sinks_are_offered() -> None:
"""(d) Every pre-P19 call site is byte-identical, and a half-wired one is refused."""
plain = {t.name for t in navigator_tools((str(_TUNNEL),))}
assert plain == {"list_bundles", "read_bundle", "read_dir", "read_file"}
wired, _, _ = _wired()
assert set(wired) == plain | {"declare_requirement"}
with pytest.raises(explore.ExplorationError, match="together or not at all"):
navigator_tools((str(_TUNNEL),), requirements=[])
with pytest.raises(explore.ExplorationError, match="together or not at all"):
navigator_tools((str(_TUNNEL),), opened=[])
# ---------------------------------------------------------------------------------------------
# (e)-(f) the marked hypothesis
# ---------------------------------------------------------------------------------------------
def _line(**payload: Any) -> str:
return f"{explore.HYPOTHESIS_MARKER} " + json.dumps({"label": "L", "rationale": "R", **payload})
def test_the_requirement_key_is_never_omitted() -> None:
"""(e) A marked line that skips the question is the unreadable-marked-line class."""
with pytest.raises(HypothesisParseError, match="must carry a 'requirement'"):
explore._parse_hypotheses([_line()], ())
def test_an_explicit_null_is_legal_and_must_say_why() -> None:
"""(e) "the base holds none" is a FINDING; an unexplained null is a silence."""
((label, rationale, _base, requirement),) = explore._parse_hypotheses(
[_line(requirement=None, why_none="the base is a standard, not a price schedule")], ()
)
assert (label, rationale, requirement) == ("L", "R", None)
with pytest.raises(HypothesisParseError, match="why_none"):
explore._parse_hypotheses([_line(requirement=None)], ())
def test_a_half_named_requirement_is_refused() -> None:
"""(e) Validation, never repair: a citation that points at nothing is worse than none."""
with pytest.raises(HypothesisParseError, match="non-empty 'path' and 'ref'"):
explore._parse_hypotheses([_line(requirement={"path": "a.md"})], ())
with pytest.raises(HypothesisParseError, match="non-empty 'path' and 'ref'"):
explore._parse_hypotheses([_line(requirement={"ref": "12.1"})], ())
def test_a_minted_approach_carries_it_and_a_seed_never_acquires_one() -> None:
"""(f) § C.6 door 1 is a preservation rule."""
seed = Approach(id="expert-1", label="Seeded", description="the expert's own")
minted = explore._mint_approaches(
(seed,), [("Shaped", "in the loop", "", BindingRequirement(path="k/12-1.md", ref="12.1"))]
)
assert minted[0] is seed and minted[0].requirement is None
assert minted[1].requirement == BindingRequirement(path="k/12-1.md", ref="12.1")
# ---------------------------------------------------------------------------------------------
# (g) A3 — the requirement reaches the proposal
# ---------------------------------------------------------------------------------------------
def _project() -> Project:
return Project(id="X", name="Y", description="d", currency="NOK", docs_dir=".", cost_items=())
def test_the_binding_requirement_reaches_the_proposer_verbatim() -> None:
"""(g) A3, and the byte-identical half is what keeps the golden transcript unchanged."""
bare = Approach(id="a1", label="L", description="D")
with_req = bare.model_copy(
update={"requirement": BindingRequirement(path="R761/12-1/x.md", ref="12.1")}
)
without = _build_messages(_project(), "ctx", approach=bare)[0].text
withit = _build_messages(_project(), "ctx", approach=with_req)[0].text
assert "Binding requirement" not in without
assert "Binding requirement: 12.1 (R761/12-1/x.md)" in withit
# The ONLY difference is the block — an approach without one is byte-identical to before.
assert (
withit.replace(
"Binding requirement: 12.1 (R761/12-1/x.md)\nName that requirement verbatim in 'measure'.\n",
"",
)
== without
)
# ---------------------------------------------------------------------------------------------
# (h) A4 — the judge
# ---------------------------------------------------------------------------------------------
def test_a_hit_is_counted_against_this_approachs_fasit_never_the_base() -> None:
"""(h) The P16 vacuity, one column over: on a 2 756-document base everything is 'in the base'."""
wanted = {"krav/12-1/a.md", "krav/12-12/b.md"}
approach = Approach(
id="a1",
label="L",
requirement=BindingRequirement(path="krav/12-1/a.md", ref="12.1"),
)
paths, source = stress._attributable(approach, ["krav/99-9/elsewhere.md"])
assert (paths, source) == (("krav/12-1/a.md",), "approach")
assert set(paths) & wanted
# A run-level declaration that is NOT one of this approach's fasit concepts is reported and is
# not a hit — the base holds it, which is exactly what must not count.
bare = Approach(id="a2", label="M")
paths, source = stress._attributable(bare, ["krav/99-9/elsewhere.md"])
assert (paths, source) == (("krav/99-9/elsewhere.md",), "run")
assert not set(paths) & wanted
# And "declared nothing" is a third, distinct finding.
assert stress._attributable(bare, []) == ((), "absent")
_GOOD = "krav/N1/id-good.md"
_OTHER = "krav/N1/id-other.md"
def _minibase(root: Path) -> Path:
base = root / "minibase"
(base / "krav" / "N1").mkdir(parents=True)
(base / "index.md").write_text(
"---\nbundle_id: minibase\n---\n\n- [good](krav/N1/id-good.md)\n"
"- [other](krav/N1/id-other.md)\n",
encoding="utf-8",
)
(base / _GOOD).write_text(
'---\ntype: concept\ntitle: "T"\nreq_number: "Krav 1.2.3-4"\n---\n\nBody.\n',
encoding="utf-8",
)
(base / _OTHER).write_text(
'---\ntype: concept\ntitle: "Other"\nreq_number: "Krav 9.9.9-9"\n---\n\nOther.\n',
encoding="utf-8",
)
return base
def _context_dir(root: Path) -> Path:
ctx = root / "ctx"
(ctx / "docs").mkdir(parents=True)
(ctx / "bundle.txt").write_text("name: minibase\nbundle_id: minibase\n", encoding="utf-8")
(ctx / "mandate.json").write_text(
json.dumps(
{
"objective": "o",
"success_criteria": "s",
"approaches": [
{
"id": "a1",
"label": "L",
"affected_codes": ["CODE-1"],
"bundle_id": "minibase",
}
],
}
),
encoding="utf-8",
)
(ctx / "fasit.json").write_text(
json.dumps(
{
"project_id": "proj",
"bundle": "minibase",
"bundle_id": "minibase",
"must_cite": [
{
"approach_id": "a1",
"rationale": "why",
"concepts": [{"path": _GOOD, "title": "T", "ref": "Krav 1.2.3-4"}],
}
],
"must_refuse": [],
"honesty": "synthetic",
}
),
encoding="utf-8",
)
return ctx
def _outbox(root: Path, *, declared: str) -> Path:
out = root / "out"
out.mkdir(parents=True, exist_ok=True)
(out / "r1-a1-proposal.json").write_text(
json.dumps(
{
"proposal": {
"project_id": "proj",
"measure": "m",
"affected_items": [{"code": "CODE-1", "quantity": 1.0, "unit_cost": 2.0}],
"claimed_saving_nok": 1.0,
"assumptions": {},
},
"provenance": {"citations": [], "token_usage": 10},
}
),
encoding="utf-8",
)
(out / "r1-a1-outcome.json").write_text(
json.dumps({"outcome_type": "rejected", "reason": "no"}), encoding="utf-8"
)
(out / "r1-debate.json").write_text(
json.dumps(
{
"run_id": "r1",
"tool_calls": [],
"requirements": [
{"bundle_id": "minibase", "path": declared, "ref": "Krav 9.9.9-9"}
],
}
),
encoding="utf-8",
)
return out
def test_a_declaration_in_the_base_but_not_in_the_fasit_is_not_a_hit(tmp_path: Path) -> None:
"""(h) END-TO-END, and this arm exists because the helper-only one was VACUOUS.
MEASURED: mutation A5-(iv) — the judge matching ``concept_names`` instead of this approach's
own ``wanted`` set — left the WHOLE suite green (1710/5), because the arm above drives
``_attributable`` while the hit is computed at the call site in ``score_context_set``. The
declaration here names a document the base really does hold; what it is not is the one the
fasit asks for, which is the only distinction a 2 756-document corpus leaves standing.
"""
base = _minibase(tmp_path)
ctx = _context_dir(tmp_path)
miss = stress.score_context_set(ctx, _outbox(tmp_path, declared=_OTHER), "r1", base)
assert miss.approaches[0].requirement_declared == (_OTHER,)
assert miss.approaches[0].requirement_source == "run"
assert miss.approaches[0].requirement_hit is False
assert miss.requirements_declared == (_OTHER,)
# The positive control: a gate that can only come out false proves nothing either.
hit = stress.score_context_set(ctx, _outbox(tmp_path, declared=_GOOD), "r1", base)
assert hit.approaches[0].requirement_hit is True
# ---------------------------------------------------------------------------------------------
# (i) the artefact
# ---------------------------------------------------------------------------------------------
def test_the_declaration_reaches_the_debate_artefact(tmp_path: Path) -> None:
"""(i) A paid run's evidence must survive the process that produced it."""
from portfolio_optimiser import outbox
outbox.write_debate_tools(
str(tmp_path),
"r1",
tool_calls=explore.tool_call_payload(
[ToolCall(name="read_file", bundle_id="b", path="k/a.md")]
),
requirements=explore.requirement_payload(
[DeclaredRequirement(bundle_id="b", path="k/a.md", ref="12.1")]
),
)
payload = json.loads((tmp_path / "r1-debate.json").read_text(encoding="utf-8"))
assert payload["requirements"] == [{"bundle_id": "b", "path": "k/a.md", "ref": "12.1"}]
# An empty list is the honest positive statement every pre-P19 run makes.
outbox.write_debate_tools(str(tmp_path), "r2", tool_calls=[])
assert (
json.loads((tmp_path / "r2-debate.json").read_text(encoding="utf-8"))["requirements"] == []
)