portfolio-optimiser/tests/test_proposal_from_mandate_loadbearing.py
Kjell Tore Guttormsen a79e2f8965 feat(mandate): forslaget OPPSTAAR fra kommisjonen og basens eget prisskjema - null modellkall
S7b-forberedelse, fase 2. Bruksscenarioet "dokumenter + konkret oppgave ->
forslag" hadde ingen vei: eneste kandidat-kilde var generate_via_llm.

DEN TILFOEYDE VEIEN. Eksperten sier HVA (label -> measure, verbatim), HVILKE
linjer (affected_codes) og HVOR MYE (claimed_saving_nok); dokumentet sier
MENGDE og PRIS (derive_cost_baseline, MAJOR-4). Ingenting her oppfinner et
tall. Tre nekter ved navn - manglende anslag, ingen koder, ukjent kode - og
hver av dem er ekspertens aa skrive, aldri vaar aa defaulte
(write_concept_file-regelen).

ANSLAGET BOR PAA APPROACH, IKKE PAA MANDATE, og det foelger av settle sin egen
regel: tilnaerminger er ALTERNATIVER og summeres aldri, saa ett tall paa
mandatnivaa ville vaert tvetydig over N. Det er heller ikke kjoeringens MAAL -
contracts.GoalContract eier nettopp ett saant, og skillet staar skrevet der
feltet innfoeres, ellers leses det som den driften modulen forbyr.

NULL MODELLKALL ER STRUKTURELT: evaluate_mandate_candidates er SYNC, saa den kan
ikke aware et chat-kall - ingen mutasjon av kroppen kan stille innfoere ett.
CLI-armen asserterer det likevel ATFERDSMESSIG (_default_factory patchet til aa
raise), fordi rc 0 alene ogsaa er utfallet til en doer som gjorde ingenting.

allow_own_proposals faar en not_evaluated-RAD, ikke en nekt: raden kan ikke
fylles uten en modell, men aa utelate den gjoer den uskillbar fra en
tilnaerming ingen bestilte (ApproachOutcome sin egen regel), og aa nekte hele
kjoeringen ville vaert feil andre veien - feltet defaulter til True.

Fire CLI-nekter, alle ved navn: krever --mandate, krever
--derive-cost-baseline, nektet i --portfolio (ved NAVN, ikke ved gjennomfall)
og i report_forbidden (der en utelatelse er et stille DROPP, ikke en nekt -
F4-gapet).

LOAD-BEARING MAALT: 15 mutasjoner, 14 ROEDE mot HELE suiten + groenn kontroll
1274/5 (fra 1257, supersett, 0 fjernet) og golden demo-transcript.stdout
BYTE-UENDRET (shasum -a 1 = ea8c534773acdbe41ae68f2c55724d69aaf8be4f).

TO MUTASJONER FALSIFISERTE TESTEN FOERST (repoets vakuoes-gate-klasse):
* M5 (bygg lazily) sto GROENN - armen asserterte kun pytest.raises, og BEGGE
  implementasjoner reiser; ingen rad naar kalleren uansett, saa de er
  uskillbare utenfra. Oekt 57s regel ("en nekt etter forbruket ser identisk ut
  ved exit-koden") anvendt paa CBC-solves: testen TELLER naa solves, med en
  kontroll som beviser at telleren faktisk beveger seg.
* M14 (rut paa mount-navnet) sto GROENN - armen brukte f"not-{declared}", som
  matcher verken mount eller erklaering. Fixturene erklaerer ingen bundle_id
  (S7a-3 maalte null ^bundle_id-treff under tests/), saa de to SAMMENFALLER
  der. Ny arm bygger en base som erklaerer en id ulik katalognavnet og
  asserterer BEGGE halvdeler: erklaert ruter, mount nektes.

EN MUTASJON FORBLIR GROENN, OG DET ER EN AERLIGHETS-GRENSE - IKKE EN GATE:
M7 (doem UTEN baselinen) er strukturelt uobserverbar, fordi kandidaten er
BYGGET fra baselinen og stage 0 derfor avstemmer med 0 % avvik ved
konstruksjon. baseline= staar som en DEFENSIV, uvitnet soem
(budget_stop-presedensen), ikke som noe en test holder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 22:27:24 +02:00

428 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""A candidate proposal that ARISES from the commission + the bundle's own priced schedule, with
no model in the loop (S7b-forberedelse, ordre ``20260903T014107Z-72282048``).
**The order's premise was felled before anything was built on it, and the measurement is what
made this two seams rather than one** (``docs/2026-09-03-forslag-fra-mandat.md``). The order says
the proposal is *read from a hand-written* ``validator-input.json``. The symptom is real — a base
without that file refuses with ``FileNotFoundError`` before the first model call — but the
diagnosis is not: ``SavingsProposal`` has ALWAYS been built by ``generate._parse_ir`` from the
MODEL's reply. The file is a fixture BESIDE the run path, required only as the project's identity
(``run._project_from_bundle``, plus two other readers). So what was missing was never a way to stop
reading a file; it was a DETERMINISTIC CANDIDATE SOURCE beside ``generate_via_llm``. This module
gates that source. Making the IR projection optional is the OTHER seam, measured in the document
and deliberately not built here.
**The fixture is used UNTOUCHED, and that is the arm's whole point.**
``test_cost_baseline_derivation_loadbearing`` has to call ``_runnable()`` — copy the fixture and
write a ``validator-input.json`` into the copy — before ``run_project`` will look at it. Here the
same fixture runs as it stands. A test that had to author that file first would have proved the
opposite of what it claims.
**Null model calls is STRUCTURAL, not merely measured.** ``run.evaluate_mandate_candidates`` is a
SYNC function: it cannot await a chat call, so no mutation of its body can quietly introduce one.
The CLI arm still asserts the property behaviourally — ``run._default_factory`` is patched to raise,
which is the seam ``test_run_cli_loadbearing`` uses, and it is the only way to prove the door
reaches the offline path rather than merely returning 0 by some other route.
**The discriminating arm is (a2), not (a).** Copying ``affected_items`` out of the derived baseline
makes stage 0 reconcile at 0 % deviation by construction, and with no assumption bands the Monte
Carlo is degenerate (P10 == P50 == P90). "The validator ran" is therefore nearly green by
construction — this repo's vacuous-gate class. The live constraint is ``MAX_SAVING_FRACTION``: a
claim above 30 % of the affected total must be REJECTED by the ordinary gate, which is what proves
the candidate goes THROUGH ``validate_proposal`` rather than around it.
Refusals assert the NAMED class and the distinguishing token, never bare ``ValueError``: pydantic's
``ValidationError`` subclasses ``ValueError`` and ``SavingsProposal`` carries two model validators,
so a source that fabricated a figure would raise ``ValueError`` too and a loose arm would stay green
against the very mutation it exists to catch (MAJOR-4's own M17 lesson).
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import pytest
from portfolio_optimiser import okf, run
from portfolio_optimiser.mandate import (
OWN_PROPOSAL_ID,
Approach,
Mandate,
MandateCandidateError,
candidate_from_approach,
)
_FIXTURES = Path(__file__).parent / "fixtures"
#: MAJOR-4's priced fixture, used EXACTLY as it sits on disk — no ``validator-input.json`` added.
_PRICED = str(_FIXTURES / "k2-prisskjema-SYNTETISK")
_UNPRICED = str(_FIXTURES / "k2-prisskjema-uprisert-SYNTETISK")
_PROJECT = "K2"
#: Transcribed from the fixture's table, not from the deriver: ``21.1`` is 1250 × 850.
_LINE_211_TOTAL = 1250.0 * 850.0
#: ``validator.MAX_SAVING_FRACTION`` is 0.30, so this is the boundary the ordinary gate enforces.
_FEASIBLE_211 = _LINE_211_TOTAL * 0.30
def _baseline() -> Any:
return okf.derive_cost_baseline(okf.navigate_bundle(_PRICED), project_id=_PROJECT)
def _approach(**overrides: Any) -> Approach:
fields: dict[str, Any] = {
"id": "a1",
"label": "Redusert sprengningsvolum i sone A",
"description": "Eksperten mener massetaket kan flyttes.",
"affected_codes": ("21.1",),
"claimed_saving_nok": 200_000.0,
}
fields.update(overrides)
return Approach(**fields)
def _mandate(*approaches: Approach, allow_own: bool = False) -> Mandate:
return Mandate(
objective="Finn kostnadsbesparelser i K2",
approaches=tuple(approaches),
allow_own_proposals=allow_own,
)
# --------------------------------------------------------------------------------------------
# (a) the candidate is built from the commission + the derived baseline
# --------------------------------------------------------------------------------------------
def test_candidate_carries_the_experts_own_words_and_the_bundles_own_numbers() -> None:
"""``measure`` is the expert's label VERBATIM; the quantities are the BASELINE's, never the
approach's — the expert says WHAT and HOW MUCH, the document says at what price."""
candidate = candidate_from_approach(_approach(), baseline=_baseline(), project_id=_PROJECT)
assert candidate.project_id == _PROJECT
assert candidate.measure == "Redusert sprengningsvolum i sone A"
assert candidate.claimed_saving_nok == 200_000.0
assert [(i.code, i.quantity, i.unit_cost) for i in candidate.affected_items] == [
("21.1", 1250.0, 850.0)
]
# Stated as a measured honesty limit rather than left implicit: no band can be derived from a
# single price, so the Monte Carlo stage is degenerate on this path.
assert candidate.assumptions == {}
def test_named_codes_select_their_lines_in_the_order_the_expert_named_them() -> None:
candidate = candidate_from_approach(
_approach(affected_codes=("36.1", "21.1"), claimed_saving_nok=100_000.0),
baseline=_baseline(),
project_id=_PROJECT,
)
assert [i.code for i in candidate.affected_items] == ["36.1", "21.1"]
def test_the_ordinary_gate_validates_a_claim_inside_the_feasible_bound() -> None:
rows = run.evaluate_mandate_candidates(
_mandate(_approach()), bundle_dir=_PRICED, project_id=_PROJECT
)
assert [(r.id, r.status) for r in rows] == [("a1", "validated")]
assert rows[0].saving_nok == 200_000.0
def test_the_ordinary_gate_rejects_a_claim_above_the_feasible_bound() -> None:
"""THE DISCRIMINATOR. Everything else about a baseline-copied candidate reconciles at 0 %
deviation by construction, so this is the one arm that can tell a candidate that goes THROUGH
``validate_proposal`` from one that goes around it. The figure is above 30 % of the affected
total and below the total itself, so pydantic's own ``claimed <= total`` validator does NOT
fire — the rejection has to come from the validator's stages."""
over = _FEASIBLE_211 + 50_000.0
assert over < _LINE_211_TOTAL # pydantic would otherwise refuse at construction
rows = run.evaluate_mandate_candidates(
_mandate(_approach(claimed_saving_nok=over)), bundle_dir=_PRICED, project_id=_PROJECT
)
assert [(r.id, r.status) for r in rows] == [("a1", "rejected")]
assert "feasible" in rows[0].detail
# --------------------------------------------------------------------------------------------
# (b) what the expert did not say is REFUSED BY NAME, never invented
# --------------------------------------------------------------------------------------------
def test_an_approach_without_an_estimate_is_refused_by_name() -> None:
with pytest.raises(MandateCandidateError) as excinfo:
candidate_from_approach(
_approach(claimed_saving_nok=None), baseline=_baseline(), project_id=_PROJECT
)
message = str(excinfo.value)
assert "a1" in message
assert "claimed_saving_nok" in message
def test_an_approach_without_cost_codes_is_refused_by_name() -> None:
with pytest.raises(MandateCandidateError) as excinfo:
candidate_from_approach(
_approach(affected_codes=()), baseline=_baseline(), project_id=_PROJECT
)
message = str(excinfo.value)
assert "a1" in message
assert "affected_codes" in message
def test_a_code_the_baseline_does_not_carry_is_refused_by_name() -> None:
"""``route_by_bundle``'s rule, one level down: a key that names something not configured is
refused BY NAME rather than resolved by position. The known codes are listed, because the
operator's next move is to correct the mandate against the document."""
with pytest.raises(MandateCandidateError) as excinfo:
candidate_from_approach(
_approach(affected_codes=("99.9",)), baseline=_baseline(), project_id=_PROJECT
)
message = str(excinfo.value)
assert "99.9" in message
assert "21.1" in message # the known codes are named
def test_the_refusal_fires_before_any_approach_is_evaluated(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Fail-fast on a commission that cannot be executed AS WRITTEN (``load_mandate``'s rule): one
unfillable approach refuses the whole run rather than doing work for the approaches ahead of it.
The good approach is FIRST, so a source that built lazily would have solved it before raising.
**The assert is on the WORK, not on the exception**, and that is measured rather than stylistic:
an earlier version of this arm asserted only ``pytest.raises`` and stayed GREEN against the lazy
mutation, because both implementations raise and no row ever reaches the caller either way —
from the outside the two are indistinguishable. This is økt 57's rule ("a refusal after the
spend looks identical at the exit code") applied to CBC solves instead of model calls: count the
solves."""
solves = 0
real = run.validate_proposal
def _counting(*args: Any, **kwargs: Any) -> Any:
nonlocal solves
solves += 1
return real(*args, **kwargs)
monkeypatch.setattr(run, "validate_proposal", _counting)
with pytest.raises(MandateCandidateError):
run.evaluate_mandate_candidates(
_mandate(_approach(), _approach(id="a2", claimed_saving_nok=None)),
bundle_dir=_PRICED,
project_id=_PROJECT,
)
assert solves == 0
# The control: with the same wiring and a fillable commission the counter DOES move, so a
# zero above is a property of the refusal rather than of a patch that never took effect.
run.evaluate_mandate_candidates(_mandate(_approach()), bundle_dir=_PRICED, project_id=_PROJECT)
assert solves == 1
def test_an_unpriced_schedule_still_refuses_in_full() -> None:
"""MAJOR-4's refusal is not routed around: the candidate source has nothing to quantify with,
so the derivation's own named refusal propagates rather than degrading to an un-anchored run."""
with pytest.raises(okf.CostBaselineDerivationError):
run.evaluate_mandate_candidates(
_mandate(_approach()), bundle_dir=_UNPRICED, project_id=_PROJECT
)
# --------------------------------------------------------------------------------------------
# (c) the coverage report stays honest about what this path CANNOT do
# --------------------------------------------------------------------------------------------
def test_own_proposals_are_reported_as_not_evaluated_rather_than_omitted() -> None:
"""A run's OWN proposal needs a model, and this path has none. The row is REPORTED with a named
reason rather than dropped: ``ApproachOutcome``'s own rule is that an omitted row is
indistinguishable from an approach nobody commissioned. Refusing the whole run would be wrong
the other way — ``allow_own_proposals`` defaults to True, so every mandate written so far
carries it."""
rows = run.evaluate_mandate_candidates(
_mandate(_approach(), allow_own=True), bundle_dir=_PRICED, project_id=_PROJECT
)
assert [(r.id, r.status) for r in rows] == [
("a1", "validated"),
(OWN_PROPOSAL_ID, "not_evaluated"),
]
assert "no model" in rows[1].detail
def _declaring_base(tmp_path: Path, declared: str) -> str:
"""A copy of the priced fixture that DECLARES a bundle id differing from its mount name.
Needed because the shipped fixtures declare none — S7a-3 measured zero ``^bundle_id`` matches
anywhere under ``tests/`` — so on them ``reconcile_bundle_id`` falls through to the mount's
basename and the declared id and the mount COINCIDE. A routing arm written against such a base
cannot tell the two apart, which is exactly what let the mount-name mutation stay green.
"""
import shutil
root = tmp_path / "mounted-under-another-name"
shutil.copytree(_PRICED, root)
for path in root.glob("*.md"):
text = path.read_text(encoding="utf-8")
path.write_text(
text.replace("---\ntype:", f"---\nbundle_id: {declared}\ntype:", 1), "utf-8"
)
return str(root)
def test_routing_follows_the_declared_id_and_refuses_the_mount_name(tmp_path: Path) -> None:
"""Routing is not re-implemented here: ``route_by_bundle`` is called with the base's DECLARED id
(S7a-3 pkt. 1), so a base delivered under a directory name of its own routes as ITSELF.
Both halves are asserted, because a mutation that routed on the mount name differs from the
shipped code in BOTH directions — and an arm that only refused an id matching neither (an
earlier version used ``f"not-{declared}"``) is green against it, since both implementations
refuse a name that is nobody's."""
declared = "k2-trinn1-20260903"
base = _declaring_base(tmp_path, declared)
assert okf.reconcile_bundle_id(base).id == declared
assert Path(base).name != declared # the two are genuinely distinct here
rows = run.evaluate_mandate_candidates(
_mandate(_approach(bundle_id=declared)), bundle_dir=base, project_id=_PROJECT
)
assert [(r.id, r.status) for r in rows] == [("a1", "validated")]
with pytest.raises(run.MandateRoutingError):
run.evaluate_mandate_candidates(
_mandate(_approach(bundle_id=Path(base).name)),
bundle_dir=base,
project_id=_PROJECT,
)
# --------------------------------------------------------------------------------------------
# (d) the CLI door
# --------------------------------------------------------------------------------------------
def _argv(*extra: str) -> list[str]:
return [
_PROJECT,
"--docs-dir",
_PRICED,
"--bundle-dir",
_PRICED,
"--derive-cost-baseline",
"--proposals-from-mandate",
*extra,
]
def _mandate_file(tmp_path: Path, mandate: Mandate) -> str:
path = tmp_path / "mandate.json"
path.write_text(mandate.model_dump_json(), encoding="utf-8")
return str(path)
def test_cli_runs_the_whole_path_without_building_a_single_model_client(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""The behavioural half of "null model calls". ``_default_factory`` is the one injection point
the CLI leaves (``test_run_cli_loadbearing``'s seam), so a door that fell through to the debate
would raise here instead of returning 0. Asserting rc 0 alone would pass against a door that
quietly did nothing."""
def _refuse(_profile: Any) -> Any:
raise AssertionError("a model client was built on a path that must make no model calls")
monkeypatch.setattr(run, "_default_factory", _refuse)
rc = run.main(_argv("--mandate", _mandate_file(tmp_path, _mandate(_approach()))))
assert rc == 0
out = capsys.readouterr().out
assert "VALIDATED" in out
assert "a1" in out
def test_cli_refuses_the_flag_without_a_mandate(capsys: pytest.CaptureFixture[str]) -> None:
rc = run.main(_argv())
assert rc == 1
assert "--mandate" in capsys.readouterr().err
def test_cli_refuses_the_flag_without_a_derived_baseline(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""The derived baseline is the candidate's only source of quantities, so without it there is
nothing to build an ``affected_items`` from. Refused by NAME rather than surfacing later as a
missing-baseline failure that names neither flag."""
rc = run.main(
[
_PROJECT,
"--docs-dir",
_PRICED,
"--bundle-dir",
_PRICED,
"--proposals-from-mandate",
"--mandate",
_mandate_file(tmp_path, _mandate(_approach())),
]
)
assert rc == 1
assert "--derive-cost-baseline" in capsys.readouterr().err
def test_cli_refuses_the_flag_in_portfolio_mode(capsys: pytest.CaptureFixture[str]) -> None:
rc = run.main(["--portfolio", "--proposals-from-mandate"])
assert rc == 1
err = capsys.readouterr().err
# Named, not fallen through to "requires --mandate": an operator who wrote --portfolio has to
# hear which of the two is wrong (the --explore precedent).
assert "--portfolio" in err
assert "--proposals-from-mandate" in err
def test_cli_refuses_the_flag_in_report_mode(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Report mode returns BEFORE the run dispatch, so a flag missing from ``report_forbidden`` is
a SILENT DROP rather than a refusal (the gap F4 measured). The argv is one report mode would
otherwise ACCEPT, so rc 1 is the mutant's opposite outcome."""
ledger = tmp_path / "ledger.json"
ledger.write_text(json.dumps([]), encoding="utf-8")
assert run.main(["--report", "--ledger", str(ledger)]) == 0
capsys.readouterr()
rc = run.main(["--report", "--ledger", str(ledger), "--proposals-from-mandate"])
assert rc == 1
assert "mode-exclusive" in capsys.readouterr().err
# --------------------------------------------------------------------------------------------
# (e) the control: the hand-written projection is still preferred, and untouched
# --------------------------------------------------------------------------------------------
async def test_a_bundle_carrying_the_ir_projection_runs_exactly_as_before(tmp_path: Path) -> None:
"""The control the order asks for. This path is ADDITIVE: a base with a hand-written
``validator-input.json`` reaches the unchanged bundle arm, anchoring included. If this arm ever
goes red, the new door has started competing with the old one instead of standing beside it."""
import shutil
root = tmp_path / "runnable"
shutil.copytree(_PRICED, root)
(root / "validator-input.json").write_text(
json.dumps(
{
"project_id": _PROJECT,
"measure": "energy_efficiency",
"affected_items": [{"code": "21.1", "quantity": 1250.0, "unit_cost": 850.0}],
"claimed_saving_nok": 100_000.0,
}
),
encoding="utf-8",
)
report = await run.run_project(
_PROJECT,
"local",
docs_dir=str(root),
bundle_dir=str(root),
derive_cost_baseline=True,
live_dry_run=True,
)
assert isinstance(report, run.DryRunReport)
assert report.cost_baseline_anchored is True