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>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-04 08:09:03 +02:00
commit b75387ca25
4 changed files with 593 additions and 14 deletions

View file

@ -0,0 +1,439 @@
"""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`` -(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