portfolio-optimiser/tests/test_requirement_number_gate_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

275 lines
13 KiB
Python

"""P20 DEL B — a clause number is not a price, and the base's own vocabulary is what says so.
**What was measured.** Three paid rounds and one multi-base pass carried FOUR ``validated``
proposals whose cost code was a chapter number of a standard. Two survive in the recorded outboxes
and are this arm's known positives:
* ``10.4`` — tunnel-hauglia round 3, base ``vegnormal-n500-2024``, ``validated``;
* ``1.10.4`` — lindaas P17b, base ``vegnormal-r761-2025``, ``validated``.
Both are GROUNDED in P7's sense (they occur verbatim in the input) and neither is INERT in
P18/B1's sense (``10.4`` in 12 of 274 documents, ``1.10.4`` in 1 of 2 756). Stage 0 never ran:
no vegnormal base ships a cost baseline. Nothing in the gate could say what they are.
**THE ORDER'S OWN RULE WAS FELLED BY MEASUREMENT BEFORE ANYTHING WAS BUILT ON IT.** B1 reads: a
code is a requirement when it has form 2 or 3 AND "står som ``req_number``/``prosessnr`` i
toppnivå-frontmatter i minst ett av grunnlagets dokumenter" — refuse that. Measured 15.09:
* n500 declares ``seksjon: 10.4.1`` … ``10.4.4`` and ``req_number: Krav 10.4.3—2``. The bare
``10.4`` is declared NOWHERE — it is a section PREFIX;
* r761 declares 2 727 ``prosessnr`` and 2 753 ``seksjon``. ``1.10.4`` is NONE of them: it occurs
once, as prose, in "Krav til materialer skal være iht. vegnormal N200 Vegbygging kap. 1.10.4".
The ordered rule therefore fires on NEITHER of its own known positives. The COMPLEMENT fires on
BOTH, and it closes a hole ``_ground_against_input`` already admits in writing — "it fails OPEN …
on a coincidental match". For one shape, a clause number, the base hands us the vocabulary needed
to tell a real reference from a coincidence, and that is the rule built here.
The complement is also what SPARES the one context set built on real process codes: all five of
``contexts/kontrakt-sorasen-2027``'s codes are declared ``prosessnr`` and pass. Under the ordered
rule every one of them would have been refused on an unanchored r761 run, and the set's positive
arms would have become unmeasurable — the R761 risk the order names, arriving through the door it
was pointed away from.
Measured over EVERY code of round 3 and P17b (24 codes, 10 runs): exactly two are
requirement-shaped, they are the two known positives, and the replay flips exactly those two.
What each arm pins:
(a) known positive — the ``10.4`` proposal, replayed against the base it actually ran on, is
``rejected``, and the reason names the denominator;
(b) known positive — ditto ``1.10.4`` on r761;
(c) known negative — a code the base DOES declare (sorasen's real ``12.1``) still validates;
(d) known negative — the gate is OFF when the run is anchored, even for a clause-shaped code;
(e) the generality guard — an input that declares no reference numbers at all cannot trip the rule,
which is what leaves every pre-P20 fixture untouched rather than exempted;
(f) K2's identifier forms are untouched: ``SHA-01`` is not requirement-shaped;
(g) ``classify_codes``' third value, and its denominator-free reading (``grounding=None``) that
``stress.py`` re-derives with;
(h) the vocabulary travels WITH the text through ``_grounding_text``, so the gate the generation
loop runs sees what the run composed;
(i) a run composes the vocabulary from the base it opened — asserted end-to-end through
``run_project``, not on the composer.
"""
from __future__ import annotations
import json
import os
from pathlib import Path
import pytest
from portfolio_optimiser import okf
from portfolio_optimiser.generate import _grounding_text
from portfolio_optimiser.ir import CostBaseline, SavingsProposal
from portfolio_optimiser.reference_domain import Project
from portfolio_optimiser.validator import (
Grounding,
Rejection,
ValidatedProposal,
classify_codes,
has_requirement_form,
validate_proposal,
)
_DEFAULT_BUNDLE_ROOT = Path.home() / "repos" / "vegnormal-okf" / "build" / "ferdig"
_ROUND3 = Path("scratchpad/p19-stress/tunnel-hauglia-2027")
_P17B = Path("scratchpad/p17b-multibase/lindaas")
def _base(name: str) -> Path:
root = Path(os.environ.get("PORTFOLIO_VEGNORMAL_ROOT", str(_DEFAULT_BUNDLE_ROOT)))
if not (root / name).is_dir():
pytest.skip(f"knowledge base {name!r} is not mounted under {root}")
return root / name
def _grounding_over(name: str) -> Grounding:
"""The delivered base exactly as ``run_project`` composes it — documents AND vocabulary."""
bundle = okf.navigate_bundle(str(_base(name)))
return Grounding(
documents=tuple(
"\n".join([f.name, *f.frontmatter.values(), f.body]) for f in bundle.context_files
),
declared_references=tuple(
ref for f in bundle.context_files for ref in okf.declared_reference_numbers(f)
),
)
def _recorded(path: Path) -> SavingsProposal:
if not path.is_file():
pytest.skip(f"the recorded artefact {path} is not present in this checkout")
return SavingsProposal.model_validate(json.loads(path.read_text(encoding="utf-8"))["proposal"])
def _proposal(code: str, *, saving: float = 1000.0) -> SavingsProposal:
return SavingsProposal(
project_id="p",
measure="m",
affected_items=[{"code": code, "quantity": 10.0, "unit_cost": 1000.0}],
claimed_saving_nok=saving,
)
# ---------------------------------------------------------------------------------- known positives
def test_the_section_number_that_reached_validated_on_n500_is_refused() -> None:
"""(a) tunnel-04's ``10.4``, replayed against the base that run actually opened."""
proposal = _recorded(_ROUND3 / "tunnel-hauglia-2027-04-a4-enhetspris-ventilator-proposal.json")
assert [i.code for i in proposal.affected_items] == ["10.4"]
outcome = validate_proposal(proposal, baseline=None, grounding=_grounding_over("n500-2024"))
assert isinstance(outcome, Rejection)
assert "'10.4'" in outcome.reason
assert "not one of the 365 this knowledge base declares" in outcome.reason
def test_the_process_number_that_reached_validated_on_r761_is_refused() -> None:
"""(b) lindaas a4's ``1.10.4`` — a chapter of ANOTHER standard, quoted in one r761 document."""
proposal = _recorded(_P17B / "lindaas-01-vegnormal-r761-2025-a4-indeksregulering-proposal.json")
assert [i.code for i in proposal.affected_items] == ["1.10.4"]
outcome = validate_proposal(proposal, baseline=None, grounding=_grounding_over("r761-2025"))
assert isinstance(outcome, Rejection)
assert "not one of the 2765 this knowledge base declares" in outcome.reason
# ---------------------------------------------------------------------------------- known negatives
def test_a_process_code_the_base_declares_still_validates() -> None:
"""(c) The arm that keeps this a rule about the corpus and not about shapes.
``12.1`` is ``contexts/kontrakt-sorasen-2027``'s own first code and a REAL declared
``prosessnr`` of R761. Under the ordered rule it would have been refused; it must not be.
"""
grounding = _grounding_over("r761-2025")
assert "12.1" in grounding.reference_vocabulary
outcome = validate_proposal(_proposal("12.1"), baseline=None, grounding=grounding)
assert isinstance(outcome, ValidatedProposal), getattr(outcome, "reason", "")
def test_every_sorasen_code_is_in_the_bases_vocabulary() -> None:
"""(c) The whole context set, not one sample: five real codes, five declared numbers."""
codes = [
code
for approach in json.loads(
Path("contexts/kontrakt-sorasen-2027/mandate.json").read_text(encoding="utf-8")
)["approaches"]
for code in approach.get("affected_codes", [])
]
vocabulary = _grounding_over("r761-2025").reference_vocabulary
shaped = [c for c in codes if has_requirement_form(c)]
assert len(shaped) == 5, shaped
assert [c for c in shaped if c not in vocabulary] == []
def test_the_fasit_references_are_classified_requirement() -> None:
"""(g) known negative (c) of the order: a fasit reference IS a requirement, and says so."""
fasit = json.loads(
Path("contexts/kontrakt-sorasen-2027/fasit.json").read_text(encoding="utf-8")
)
refs = sorted({c["ref"] for entry in fasit["must_cite"] for c in entry["concepts"]})
forms = classify_codes(refs, _grounding_over("r761-2025"))
assert set(forms.values()) == {"requirement"}, forms
def test_an_anchored_run_is_untouched_by_the_rule() -> None:
"""(d) Stage 0 has already ruled; the weaker stage must not overrule the stronger."""
grounding = Grounding(documents=("12.9 is a clause",), declared_references=("12.1", "12.2"))
baseline = CostBaseline(project_id="p", items={"12.9": {"quantity": 10.0, "unit_cost": 1000.0}})
outcome = validate_proposal(_proposal("12.9"), baseline=baseline, grounding=grounding)
assert isinstance(outcome, ValidatedProposal), getattr(outcome, "reason", "")
# The control: the SAME code and the SAME text, unanchored, is refused.
unanchored = validate_proposal(_proposal("12.9"), baseline=None, grounding=grounding)
assert isinstance(unanchored, Rejection)
def test_an_input_that_declares_no_reference_numbers_cannot_trip_the_rule() -> None:
"""(e) The generality guard — and the reason every pre-P20 fixture is untouched."""
grounding = Grounding(documents=("a document mentioning 12.9 once",))
assert grounding.reference_vocabulary == frozenset()
outcome = validate_proposal(_proposal("12.9"), baseline=None, grounding=grounding)
assert isinstance(outcome, ValidatedProposal), getattr(outcome, "reason", "")
def test_a_cost_line_identifier_is_not_requirement_shaped() -> None:
"""(f) K2's 50 identifiers and this repo's own code: shape, measured."""
assert not has_requirement_form("SHA-01")
assert not has_requirement_form("ENERGI-TOTAL-EL")
assert not has_requirement_form("65 ASFALTDEKKER")
assert has_requirement_form("10.4") and has_requirement_form("Krav 4.1.2—1")
def test_classify_codes_without_a_grounding_is_the_pre_p20_answer() -> None:
"""(g) ``stress.py`` re-derives for runs written before the field existed."""
assert classify_codes(["12.1", "SHA-01", "impulsventilator"]) == {
"12.1": "identifier",
"SHA-01": "identifier",
"impulsventilator": "prose",
}
grounding = Grounding(documents=("x",), declared_references=("12.1",))
assert classify_codes(["12.1"], grounding) == {"12.1": "requirement"}
# ---------------------------------------------------------------------------------- the wiring
def test_the_vocabulary_travels_with_the_text_into_the_generation_gate() -> None:
"""(h) ``_grounding_text`` composes the run's three sources; the vocabulary must survive it."""
delivered = Grounding(documents=("d",), declared_references=("12.1",))
project = Project(
id="p", name="n", description="d", currency="NOK", cost_items=(), docs_dir="."
)
composed = _grounding_text(project, None, delivered)
assert composed.reference_vocabulary == frozenset({"12.1"})
def test_a_run_composes_the_vocabulary_from_the_base_it_opened(tmp_path: Path) -> None:
"""(i) End-to-end through ``run_project``: the stamp says ``requirement`` for a declared code.
Asserted on the ARTEFACT a run leaves, never on the composer — a vocabulary wired nowhere would
satisfy every arm above and none of this one.
"""
import asyncio
from agent_framework import BaseChatClient
from portfolio_optimiser.run import run_project
from portfolio_optimiser.simulation import ScriptedChatClient
base = tmp_path / "mini"
base.mkdir()
(base / "index.md").write_text(
"---\nbundle_id: mini\n---\n\n- [Krav](krav.md) — one clause.\n", encoding="utf-8"
)
(base / "krav.md").write_text(
"---\ntype: Krav\ntitle: Krav 4.1.2-1\nprosessnr: '12.1'\n---\n\nEn kostlinje 12.1.\n",
encoding="utf-8",
)
(base / "validator-input.json").write_text(
json.dumps({"project_id": "mini-p", "measure": "m", "affected_codes": ["12.1"]}),
encoding="utf-8",
)
reply = (
'{"measure":"m","affected_items":[{"code":"12.1","quantity":10,"unit_cost":1000}],'
'"claimed_saving_nok":1000}'
)
def factory(role: str) -> BaseChatClient:
return ScriptedChatClient(
"Reasoning holds.\nVERDICT: APPROVE" if role == "checker" else reply, role=role
)
result = asyncio.run(
run_project(
"mini-p",
"local",
docs_dir=str(base),
bundle_dir=str(base),
client_factory=factory,
)
)
assert result.provenance.code_forms == {"12.1": "requirement"}
assert result.provenance.validator_decision == "validated"