portfolio-optimiser/tests/test_requirement_number_gate_loadbearing.py
Kjell Tore Guttormsen 6b1046bc23
docs: general wording for the example-base document counts
Replace the exact document counts of earlier example bases (and the
per-base counts in the sources-format note) with general wording or
N-of-N in prose, comments and docstrings. Percentages and numerators
stay; no constant, assertion or test data changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 17:48:44 +02:00

355 lines
16 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 survived in the recorded
outboxes and were this arm's known positives:
* ``10.4`` — a stress round on a requirements base, ``validated``;
* ``1.10.4`` — the multi-base pass, on the process catalogue, ``validated``.
Both were GROUNDED in P7's sense (they occur verbatim in the input) and neither was INERT in
P18/B1's sense (``10.4`` in 12 of a few hundred documents, ``1.10.4`` in 1 of a few thousand). Stage 0 never ran:
no requirements 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:
* the requirements base declared ``seksjon: 10.4.1`` … ``10.4.4`` and ``req_number: Krav
10.4.3—2``. The bare ``10.4`` was declared NOWHERE — it is a section PREFIX;
* ``1.10.4`` was NONE of the process catalogue's declared ``prosessnr`` and ``seksjon`` values:
it occurred once, as prose, a chapter reference into ANOTHER standard.
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 context set built on the catalogue's own process codes:
all five of ``contexts/driftsavtale-2027``'s codes are declared ``prosessnr`` and pass. Under the
ordered rule every one of them would have been refused on an unanchored run on the catalogue, and
the set's positive arms would have become unmeasurable — the risk the order names (a process
number is both a clause and a settlement post), arriving through the door it was pointed away from.
Measured over EVERY code of that round and the multi-base pass (24 codes, 10 runs): exactly two
were requirement-shaped, they were the two known positives, and the replay flipped exactly those
two. Those recordings were made against corpora this repository no longer carries, so (a) and (b)
are now CONSTRUCTED in the same two shapes against the package's example bases: ``4.2.5`` — a
section PREFIX in ``driftskrav-2027`` (it declares ``seksjon: 4.2.5.1`` and ``Krav 4.2.5.1—1`` …
``—3``; the bare ``4.2.5`` stands in 3 of 306 documents and is no document's own number) — and
``1.10.4`` — a clause of another document quoted once, as prose, in ``prosesskatalog-2027``.
What each arm pins:
(a) known positive — the section prefix ``4.2.5``, unanchored against the requirements base, is
``rejected``, and the reason names the denominator;
(b) known positive — ditto ``1.10.4`` on the process catalogue;
(c) known negative — a code the base DOES declare (driftsavtale's ``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
from pathlib import Path
import pytest
from portfolio_optimiser import frozen_bundles
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,
)
def _base(name: str) -> Path:
"""The FROZEN copy this repository pins, resolved at call time.
Absence SKIPS (a user's own store, named by ``PORTFOLIO_FROZEN_BUNDLES``, may not hold it),
drift is allowed to propagate and FAIL — a measurement of the wrong corpus is not a missing one.
"""
try:
return frozen_bundles.bundle_dir(name)
except frozen_bundles.FrozenBundleMissing as exc:
pytest.skip(str(exc))
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 _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_a_section_prefix_of_the_requirements_base_is_refused() -> None:
"""(a) The ``10.4`` shape: a section PREFIX that stands in the base, is not inert, and is no
document's own number. The CONTROL is what makes the refusal this rule's and not another
stage's: the prefix IS in the text, in too few documents to be inert, and not declared."""
grounding = _grounding_over("driftskrav-2027")
assert "4.2.5" in grounding.text and "4.2.5" not in grounding.reference_vocabulary
assert grounding.document_frequency("4.2.5") == 3
outcome = validate_proposal(_proposal("4.2.5"), baseline=None, grounding=grounding)
assert isinstance(outcome, Rejection)
assert "'4.2.5'" in outcome.reason
assert "not one of the 279 this knowledge base declares" in outcome.reason
def test_a_clause_of_another_document_quoted_in_the_catalogue_is_refused() -> None:
"""(b) The ``1.10.4`` shape: a clause of ANOTHER document, quoted once in the catalogue."""
grounding = _grounding_over("prosesskatalog-2027")
assert grounding.document_frequency("1.10.4") == 1
assert "1.10.4" not in grounding.reference_vocabulary
outcome = validate_proposal(_proposal("1.10.4"), baseline=None, grounding=grounding)
assert isinstance(outcome, Rejection)
assert "not one of the 306 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/driftsavtale-2027``'s own first code and a declared ``prosessnr`` of
the catalogue. Under the ordered rule it would have been refused; it must not be.
"""
grounding = _grounding_over("prosesskatalog-2027")
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_driftsavtale_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/driftsavtale-2027/mandate.json").read_text(encoding="utf-8")
)["approaches"]
for code in approach.get("affected_codes", [])
]
vocabulary = _grounding_over("prosesskatalog-2027").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/driftsavtale-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("prosesskatalog-2027"))
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 LAGRINGSSYSTEMER")
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", "nødstrømsaggregat"]) == {
"12.1": "identifier",
"SHA-01": "identifier",
"nødstrømsaggregat": "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"
def test_each_approachs_artefact_carries_its_own_code_forms(tmp_path: Path) -> None:
"""(j) MEASURED on round 3 AND round 4: every per-approach artefact carried the SELECTED
proposal's codes.
``code_forms``' own comment says it is derived "off the proposal being stamped", and the
per-approach branch copied the run's stamp while overriding only ``validator_decision``. So an
artefact about approach 2 reported approach 1's codes — and ``stress.py``, which reads this
field before re-deriving, then produced an EMPTY ``prose_codes`` for every approach but the
first, because none of that approach's codes was a key in the map it was handed.
Run-level was the drift, not the intent: model, citations and token usage ARE the run's, and
they stay so.
"""
import asyncio
from agent_framework import BaseChatClient
from portfolio_optimiser.mandate import Approach, Mandate
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\nLines 12.1 and PRIS-EN.\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",
)
outbox = tmp_path / "out"
# Each approach's prompt names its own label, so the selector answers with ITS OWN code.
def select(prompt: str, role: str) -> str:
if role == "checker":
return "Reasoning holds.\nVERDICT: APPROVE"
code = "PRIS-EN" if "SECOND" in prompt else "12.1"
return (
'{"measure":"m","affected_items":[{"code":"%s","quantity":10,"unit_cost":1000}],'
'"claimed_saving_nok":1000}' % code
)
def factory(role: str) -> BaseChatClient:
return ScriptedChatClient(reply_selector=select, role=role)
asyncio.run(
run_project(
"mini-p",
"local",
docs_dir=str(base),
bundle_dir=str(base),
client_factory=factory,
mandate=Mandate(
objective="o",
approaches=(
Approach(id="a1", label="FIRST", description="d"),
Approach(id="a2", label="SECOND", description="d"),
),
allow_own_proposals=False,
),
outbox_dir=str(outbox),
run_id="r1",
)
)
forms = {}
for name in ("a1", "a2"):
payload = json.loads((outbox / f"r1-{name}-proposal.json").read_text(encoding="utf-8"))
codes = [i["code"] for i in payload["proposal"]["affected_items"]]
forms[name] = (codes, payload["provenance"]["code_forms"])
assert forms["a1"] == (["12.1"], {"12.1": "requirement"}), forms["a1"]
assert forms["a2"] == (["PRIS-EN"], {"PRIS-EN": "identifier"}), forms["a2"]