portfolio-optimiser/tests/test_optional_ir_projection_loadbearing.py
Kjell Tore Guttormsen b75387ca25 feat(s7b): IR-projeksjonen er valgfri paa alle tre kallsteder, og fraveret sies
DEL A + DEL C av ordre 20260903T204605Z-215167684-from-.claude.

Soem 1: okf.load_optional_ir_projection ved siden av den fail-faste, moensteret
fra load_cost_baseline/load_optional_cost_baseline. Et PAR, ikke et required=-flagg:
PM-tillegg 5 maalte hva en uoevet parameter koster (elleve evidence_for-kallsteder
brukte defaulten til den andre grenen raatnet), og et flagg ville dessuten gjort
usanne de fem docstringene som siterer load_ir_projection som DEN fail-faste
presedensen. Toleransen stopper ved fravaer: en malformed projeksjon reiser fortsatt.

De tre kallstedene fikk HVER SIN stilling:
  * _project_from_bundle - fravaer hopper over en fail-fast som ikke har noe aa
    sjekke mot; en projeksjon som FINNES og navngir et annet prosjekt nekter
    fortsatt. Divergens-vakten er kontrakten multi-base-dispatchen hviler paa.
  * run_mandate_across_bundles - FILA FOERST, basens ERKLAERTE bundle_id som
    fallback (S7a-3). Presedensen baerer i begge retninger: erklaering-foerst ville
    re-adressert hver eksisterende base der project_id != bundle_id.
  * bundle_candidate_features - optional_bundle_candidate_features, og de TO
    konsumentene svarer ULIKT paa fravaeret. Steg-1-folden HOPPER OVER og sier
    hvor mange tidligere dommer som dermed aldri naadde hypotese-prompten;
    seed_store_from_bundle NEKTER ved navn (VerdictKeyUnavailable), fordi aa mynte
    en noekkel for en dom som erklaerer ingen er nettopp defekten S3.2 lukker.

Synligheten: RunResult.unkeyed_verdicts (ANTALL, ikke flagg - koe-(y)-regelen) +
run.unkeyed_verdicts_notice som ENESTE renderer, None ved null (omisjon, aldri tom
rad). Baereren er MAALT: dry-run-kuttet returnerer OVER folden, saa et felt paa
DryRunReport kunne bare rapportert null - ulikt cost_baseline_anchored og
skipped_links, begge opploest over kuttet. Ikke paa ProvenanceStamp: stempelet
beskriver gaten som doemte EN kandidat.

Prosjektnavnet var et ikke-spoersmaal, og det er maalt: SavingsProposal har intet
navnefelt, saa projeksjonen har aldri vaert en navnekilde. Project.name kommer
fortsatt fra type: project-konseptets title med id-en som siste utvei.

DEL C: --mandate lagt i report_forbidden. Den var ELDRE enn partisjonen og hadde
aldri faatt en rad, saa --report --ledger X --mandate Y droppet kommisjonen i
STILLHET - F4-klassen. Testarmen kjoerer mot en argv report-modus ellers ville
AKSEPTERT, med en kontroll som beviser rc 0 uten flagget.

Kontroll: 1290 passed / 5 skipped (fra 1275/5 - supersett, 0 fjernet).
Golden demo-transcript.stdout BYTE-UENDRET, shasum -a 1 (INNHOLD, ikke git-blob)
= ea8c534773acdbe41ae68f2c55724d69aaf8be4f. ruff + mypy rene.

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

439 lines
20 KiB
Python

"""The hand-written IR projection is OPTIONAL, and its absence is said rather than raised
(S7b søm 1, ordre ``20260903T204605Z-215167684-from-.claude`` DEL A).
**The seam this closes was measured before it was built** (``docs/2026-09-03-forslag-fra-mandat.md``
§ 1.1): ``okf.load_ir_projection`` is fail-fast required at THREE call sites, and an ingested tender
corpus carries no hand-written ``validator-input.json``, so such a base could be navigated
(``read_bundle``), catalogued (``list_bundles``) and judged through the deterministic mandate door
(``evaluate_mandate_candidates``, which never reads the file) — and still could not run the
eight-step loop at all. The refusal fired before the first model call, which is the one mercy it
had.
**The pattern is the repo's own, not a new one.** ``load_cost_baseline`` /
``load_optional_cost_baseline`` already say exactly this: ABSENCE is tolerated and the caller takes
a stance; a file that EXISTS but is malformed, or that disagrees with the run, still refuses. The
S4.0 row states the rule in one line — *"toleransen stopper ved fravær"* — and reading a corrupt
projection as "no projection" would hand back an unkeyed run wearing a keyed one's clothes.
**A pair, never a ``required=`` flag.** PM-tillegg 5 measured what an unexercised parameter costs:
all eleven ``evidence_for`` call sites used the default, so the other branch rotted until it raised.
A pair also keeps true the five docstrings across ``hitl``/``ledger``/``verdicts``/``contracts`` that
cite ``load_ir_projection`` as *the* fail-fast precedent.
**The project NAME was a non-question, and that is measured rather than assumed.** The archived
self-order flagged the fallback as a real decision. Measured: ``SavingsProposal`` has five fields
(``project_id``, ``measure``, ``affected_items``, ``claimed_saving_nok``, ``assumptions``) and none
of them is a name — the projection has never been a name source. ``Project.name`` comes from the
``type: project`` concept's ``title`` with the id as last resort, before and after, so there is no
new implicit ``project_id`` here; there is an old one, unchanged.
**Each of the three sites needed its OWN stance, which is why this is not one loader swap.**
* ``run._project_from_bundle`` — absence skips a fail-fast that has nothing to check against;
a projection that is PRESENT and names another project still refuses. That divergence guard is
what multi-base dispatch rests on ("den eksisterende fail-fasten blir rutingsnøkkelen"), so
loosening absence must not loosen disagreement.
* ``run.run_mandate_across_bundles`` — the routed run's ``project_id``. FILE FIRST, the base's
DECLARED ``bundle_id`` as the fallback (S7a-3). The precedence is load-bearing in the other
direction too: every existing base whose ``project_id`` differs from its ``bundle_id`` must keep
routing on the file.
* ``verdicts.bundle_candidate_features`` — the ExpeL query key, and the ONE place where absence is
not merely tolerable. Two consumers, two different answers: the run's Step-1 fold SKIPS and SAYS
how many prior verdicts did not reach the prompt, while ``seed_store_from_bundle``'s S3.2 fallback
REFUSES BY NAME, because minting a key for a verdict that declares none is precisely the defect
S3.2 closes.
**The visibility carrier is ``RunResult`` alone, and that is a measurement.**
``cost_baseline_anchored`` and ``skipped_links`` are both resolved above the ``live_dry_run`` cut
(``run.py``: the cut returns before generation), so both can be carried on ``DryRunReport``. The
ExpeL fold happens BELOW that cut — a dry run stops before it — so a ``DryRunReport`` field could
only ever report zero. It is not on ``ProvenanceStamp`` either, for ``skipped_links``' own reason:
this is a RUN-level fact settled once, before any candidate exists, whereas the stamp describes the
gate that judged ONE candidate.
**Predictable vacuity, hunted for deliberately.** The shipped fixtures declare no ``bundle_id``
(S7a-3 measured zero ``^bundle_id`` matches under ``tests/``), so on them the declared id and the
mount basename COINCIDE and a routing arm cannot tell the two apart — every routing arm here
therefore runs against a base crafted to declare an id differing from its directory name (M14's
lesson). And an arm that asserts only ``pytest.raises`` cannot discriminate when both
implementations raise (M5's lesson), so the refusal arms assert the NAMED class plus a
distinguishing token.
"""
from __future__ import annotations
import json
import shutil
from pathlib import Path
from typing import Any
import pytest
from portfolio_optimiser import okf, run, verdicts
from portfolio_optimiser.mandate import Approach, Mandate
from portfolio_optimiser.run import RunResult
from portfolio_optimiser.simulation import scripted_factory
from portfolio_optimiser.verdicts import ProposalFeatures, VerdictStore, capture_verdict
_FIXTURES = Path(__file__).parent / "fixtures"
#: MAJOR-4's priced fixture. It ships WITHOUT a ``validator-input.json`` — that absence is the whole
#: subject here, so it is used exactly as it sits on disk unless an arm says otherwise.
_PRICED = str(_FIXTURES / "k2-prisskjema-SYNTETISK")
_PROJECT = "K2"
#: A proposal the deterministic gate validates against the fixture's derived schedule: 21.1 is
#: 1250 x 850 = 1 062 500, and ``energy_efficiency`` caps the claim at 15 % of that (F8's registry).
_PROPOSAL_JSON = 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,
"assumptions": {"21.1": [800.0, 900.0]},
}
)
def _copy(tmp_path: Path, name: str, *, declared: str | None = None) -> Path:
"""A working copy of the priced fixture, optionally DECLARING a bundle id that differs from its
mount name — the only shape under which a routing arm can distinguish declaration from mount."""
root = tmp_path / name
shutil.copytree(_PRICED, root)
if declared is not None:
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 root
def _write_projection(root: Path, project_id: str) -> None:
(root / "validator-input.json").write_text(
json.dumps(
{
"project_id": project_id,
"measure": "energy_efficiency",
"affected_items": [{"code": "21.1", "quantity": 1250.0, "unit_cost": 850.0}],
"claimed_saving_nok": 100_000.0,
}
),
encoding="utf-8",
)
def _factory(sink: list[str] | None = None) -> Any:
return scripted_factory(
{"proposer": _PROPOSAL_JSON, "checker": "VERDICT: APPROVE"},
[] if sink is None else sink,
)
async def _run(bundle_dir: str, *, sink: list[str] | None = None, **kwargs: Any) -> Any:
return await run.run_project(
_PROJECT,
"local",
docs_dir=bundle_dir,
bundle_dir=bundle_dir,
derive_cost_baseline=True,
client_factory=_factory(sink),
max_rounds=2,
**kwargs,
)
# --------------------------------------------------------------------------------------------
# (a) the loader pair itself
# --------------------------------------------------------------------------------------------
def test_the_optional_loader_returns_none_for_a_base_that_has_no_projection() -> None:
assert okf.load_optional_ir_projection(_PRICED) is None
with pytest.raises(FileNotFoundError):
okf.load_ir_projection(_PRICED)
def test_the_optional_loader_returns_the_projection_when_the_base_has_one(tmp_path: Path) -> None:
root = _copy(tmp_path, "with-projection")
_write_projection(root, _PROJECT)
loaded = okf.load_optional_ir_projection(str(root))
assert loaded is not None
assert loaded["project_id"] == _PROJECT
def test_the_tolerance_stops_at_absence_a_malformed_projection_still_raises(
tmp_path: Path,
) -> None:
"""S4.0's rule, transplanted verbatim: reading a corrupt projection as "no projection" would
hand back an unkeyed run under the appearance of a keyed one."""
root = _copy(tmp_path, "malformed")
(root / "validator-input.json").write_text("{not json", encoding="utf-8")
with pytest.raises(json.JSONDecodeError):
okf.load_optional_ir_projection(str(root))
# --------------------------------------------------------------------------------------------
# (b) the run path: a base with no projection runs the WHOLE loop
# --------------------------------------------------------------------------------------------
async def test_a_base_without_a_projection_runs_the_whole_pipeline() -> None:
"""THE HEADLINE. Before this seam the same call raised ``FileNotFoundError`` from
``_project_from_bundle`` — before the first model call, which is why an ingested corpus could be
navigated and never run."""
result = await _run(_PRICED)
assert isinstance(result, RunResult)
assert result.provenance.validator_decision in {"validated", "rejected"}
assert result.provenance.cost_baseline_anchored is True
async def test_a_projection_that_names_another_project_still_refuses(tmp_path: Path) -> None:
"""The divergence guard, which multi-base dispatch rests on. Absence became tolerable;
DISAGREEMENT did not, and a loader swap that loosened both would be green on every other arm
here."""
root = _copy(tmp_path, "wrong-project")
_write_projection(root, "SOMEONE-ELSE")
with pytest.raises(ValueError) as excinfo:
await _run(str(root))
message = str(excinfo.value)
assert "SOMEONE-ELSE" in message
assert _PROJECT in message
async def test_a_base_that_carries_the_projection_runs_exactly_as_before(tmp_path: Path) -> None:
"""The control the order asks for: with the file present nothing about the run changes."""
root = _copy(tmp_path, "runnable")
_write_projection(root, _PROJECT)
result = await _run(str(root))
assert isinstance(result, RunResult)
assert result.provenance.validator_decision in {"validated", "rejected"}
# --------------------------------------------------------------------------------------------
# (c) the dispatcher's routing key: file first, declared bundle id as the fallback
# --------------------------------------------------------------------------------------------
async def test_the_dispatcher_routes_a_projectionless_base_on_its_declared_id(
tmp_path: Path,
) -> None:
"""``run_mandate_across_bundles`` takes no ``project_id`` parameter BY DESIGN — it reads each
base's own. Without a projection the base's DECLARED id is what remains, and S7a-3 already made
that the identity every other door uses."""
declared = "k2-trinn1-20260903"
root = _copy(tmp_path, "mounted-elsewhere", declared=declared)
assert Path(root).name != declared # the two are genuinely distinct here
result = await run.run_mandate_across_bundles(
Mandate(
objective="Finn besparelser",
approaches=(Approach(id="a1", label="Tiltak", bundle_id=declared),),
allow_own_proposals=True,
),
(str(root),),
"local",
client_factory=_factory(),
max_rounds=2,
)
assert [r.bundle_id for r in result.runs] == [declared]
assert [r.project_id for r in result.runs] == [declared]
async def test_the_dispatcher_still_prefers_the_projection_when_the_base_has_one(
tmp_path: Path,
) -> None:
"""PRECEDENCE, and it is load-bearing in the direction the headline arm cannot see: every base
whose ``project_id`` differs from its ``bundle_id`` must keep routing on the FILE. The base here
declares both, and they disagree — an implementation that read the declared id first would send
the run under the wrong project name while every other arm stayed green."""
declared = "k2-trinn1-20260903"
root = _copy(tmp_path, "both-known", declared=declared)
_write_projection(root, _PROJECT)
assert _PROJECT != declared
result = await run.run_mandate_across_bundles(
Mandate(
objective="Finn besparelser",
approaches=(Approach(id="a1", label="Tiltak", bundle_id=declared),),
allow_own_proposals=True,
),
(str(root),),
"local",
client_factory=_factory(),
max_rounds=2,
)
assert [r.project_id for r in result.runs] == [_PROJECT]
# --------------------------------------------------------------------------------------------
# (d) the ExpeL fold: skipped for want of a key, and SAID
# --------------------------------------------------------------------------------------------
#: A rationale that appears nowhere in the fixture, so its presence in a prompt can only come from
#: the ExpeL fold (``simulate_learning_loop``'s marker rule).
_MARKER = "TIDLIGERE-DOM-MARKOER"
def _store() -> VerdictStore:
return VerdictStore(
verdicts=[
capture_verdict(
ProposalFeatures(
affected_codes=frozenset({"21.1"}),
measure_type="energy_efficiency",
claimed_saving_nok=90_000.0,
),
"approved",
_MARKER,
),
capture_verdict(
ProposalFeatures(
affected_codes=frozenset({"36.1"}),
measure_type="ventilation",
claimed_saving_nok=50_000.0,
),
"rejected",
"tidligere dom nummer to",
),
]
)
async def test_prior_verdicts_that_could_not_be_keyed_are_counted_not_silently_dropped() -> None:
"""Without a projection there is no pre-hypothesis candidate to key the fold against, so the
fold cannot run. The run says how MANY prior verdicts therefore never reached the hypothesis
prompt — the ``BudgetExceeded`` kø-(y) rule applied to a fold: "it did not happen" and "you lost
two judgements" are different operative facts."""
store = _store()
sink: list[str] = []
result = await _run(_PRICED, store=store, sink=sink)
assert isinstance(result, RunResult)
assert result.unkeyed_verdicts == 2
# THE DISCRIMINATOR: a counter can be wired to anything, so the arm reads the PROMPT the fold
# would have written into. The marker is a rationale that exists only in this store.
assert not any(_MARKER in prompt for prompt in sink)
async def test_a_run_whose_fold_was_keyed_counts_none(tmp_path: Path) -> None:
"""The control. A constant-count implementation and a constant-zero one are both green without
this pair — and it is what makes the renderer's OMISSION itself gated."""
root = _copy(tmp_path, "keyed")
_write_projection(root, _PROJECT)
sink: list[str] = []
result = await _run(str(root), store=_store(), sink=sink)
assert isinstance(result, RunResult)
assert result.unkeyed_verdicts == 0
# And the fold genuinely RAN — without this the arm above would be green against an
# implementation that never folds at all and merely counts zero here.
assert any(_MARKER in prompt for prompt in sink)
def test_the_renderer_is_silent_when_every_prior_verdict_was_keyed() -> None:
"""Omission, never an empty row (``mandate.announce``'s rule, the one every ``*_notice``
renderer here follows)."""
assert run.unkeyed_verdicts_notice(0) is None
line = run.unkeyed_verdicts_notice(2)
assert line is not None
assert "2" in line
def test_the_cli_prints_the_notice_for_a_projectionless_base_with_prior_verdicts(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""The renderer reaching stdout is its own seam (the ``cost_baseline_notice`` precedent: a
renderer nothing calls is a line nobody sees). Driven through ``main()`` with the verdict inbox,
which is the only route by which the CLI can hand ``run_project`` a non-empty store."""
inbox = tmp_path / "inbox"
inbox.mkdir()
verdicts.write_verdict(str(inbox), _store().verdicts[0])
monkeypatch.setattr(run, "_default_factory", lambda _profile: _factory())
rc = run.main(
[
_PROJECT,
"--docs-dir",
_PRICED,
"--bundle-dir",
_PRICED,
"--derive-cost-baseline",
"--verdict-dir",
str(inbox),
]
)
assert rc == 0
assert "1 prior expert verdict" in capsys.readouterr().out
# --------------------------------------------------------------------------------------------
# (e) the seed fallback: REFUSED by name, never keyed on a guess
# --------------------------------------------------------------------------------------------
def _verdict_concept(root: Path, name: str, *, structural: bool) -> None:
key = (
'affected_codes: "21.1"\nmeasure_type: "energy_efficiency"\nclaimed_saving_nok: "90000"\n'
if structural
else ""
)
(root / name).write_text(
f'---\ntype: verdict\ntitle: "Dom"\n{key}decision: approved\n---\n\nEn tidligere dom.\n',
encoding="utf-8",
)
index = root / "index.md"
index.write_text(
index.read_text(encoding="utf-8") + f"\nOgsaa [dom]({name}).\n", encoding="utf-8"
)
def test_a_verdict_without_its_own_key_in_a_projectionless_base_is_refused_by_name(
tmp_path: Path,
) -> None:
"""S3.2's rule at the one point where absence is NOT tolerable. The pre-S3.2 fallback keys such
a verdict on the bundle's projection candidate; with no projection there is no candidate, and
minting one would attach the verdict to a candidate it is not about — the exact defect S3.2
closes. Validation, never repair (``write_concept_file``'s rule)."""
root = _copy(tmp_path, "unkeyable")
_verdict_concept(root, "dom.md", structural=False)
with pytest.raises(verdicts.VerdictKeyUnavailable) as excinfo:
verdicts.seed_store_from_bundle(str(root))
message = str(excinfo.value)
assert "dom.md" in message
assert "affected_codes" in message
def test_a_verdict_that_declares_its_own_key_seeds_without_any_projection(tmp_path: Path) -> None:
"""The control, and the reason the refusal above is NARROW: S3.2's fields are what
``promote_verdict`` writes, so a base grown by the loop itself seeds unchanged."""
root = _copy(tmp_path, "self-keyed")
_verdict_concept(root, "dom.md", structural=True)
store = verdicts.seed_store_from_bundle(str(root))
assert len(store.verdicts) == 1
assert store.verdicts[0].proposal_features.affected_codes == frozenset({"21.1"})
# --------------------------------------------------------------------------------------------
# (f) DEL C — the report-mode partition (the gap økt 82 measured and reported)
# --------------------------------------------------------------------------------------------
def test_report_mode_refuses_a_mandate_instead_of_dropping_it(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""``--mandate`` was the one flag older than ``report_forbidden`` that never got a row, so
``--report --ledger X --mandate Y`` announced nothing and settled nothing — a SILENT DROP, the
F4 class. The argv is one report mode would otherwise ACCEPT (a valid ``--ledger``, and the
control proves rc 0 without the flag), so rc 1 is the mutant's opposite outcome rather than the
same refusal arriving by another route."""
ledger = tmp_path / "ledger.json"
ledger.write_text(json.dumps([]), encoding="utf-8")
mandate = tmp_path / "mandate.json"
mandate.write_text(
Mandate(objective="x", approaches=(Approach(id="a1", label="t"),)).model_dump_json(),
encoding="utf-8",
)
assert run.main(["--report", "--ledger", str(ledger)]) == 0
capsys.readouterr()
rc = run.main(["--report", "--ledger", str(ledger), "--mandate", str(mandate)])
assert rc == 1
assert "mode-exclusive" in capsys.readouterr().err