portfolio-optimiser/tests/test_cost_baseline_flag_loadbearing.py
Kjell Tore Guttormsen 37547fe292
refactor(examples): replace sector-specific example material with generic, fictitious examples
The context sets, the packaged knowledge bases and the example bundles are
replaced by one fictitious example set about IT operations in an invented
organisation: three context sets (serverrom-2027, driftsavtale-2027 and the
two-base drift-og-avtale-2027), two synthetic knowledge bases under
src/portfolio_optimiser/data/kunnskapsbaser and two example bundles under
src/portfolio_optimiser/data/bundles. Numbers, codes and structural values in
tests and fixtures are kept; names, ids and wording change. Dated measurement
documents that only recorded runs on the replaced material are deleted.

Gate figures measured on the new set are not comparable with earlier ones.
The exclusion gate from the previous commit is green: 0 tracked files hit
outside the shared/ subtree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 15:04:21 +02:00

397 lines
16 KiB
Python

"""P21 DEL A - the PROJECT carries the price, so a run on a requirements base can be anchored.
**The measurement this closes.** Four paid stress rounds (P16/P18/P19/P17b/P20) ran ENTIRELY
un-anchored. The cause is one line: the only file loader reads ``cost-baseline.json`` out of the
BUNDLE directory (``okf.load_optional_cost_baseline``), and no requirements base or process
catalogue ships one — they are knowledge, and knowledge carries requirements, never amounts. The
validator's stage 0 — the one stage that can tell an invented cost line from a line this project
actually buys — was therefore skipped in every single one, and ``validated`` could not mean what it
says: P20 G1/G2 measured real process-catalogue numbers (``12.11`` three times on one set,
``1.1.1`` on another) validating with amounts nobody had anywhere.
``--cost-baseline FILE`` is PM decision (e), taken over three alternatives P20 wrote down: (a)
refusing every requirement-shaped code un-anchored would make the one realistic context set
unmeasurable, (b) ``--require-cost-baseline`` as a default would leave no stress test at all, and
(c) K2's priced schedule is refused by MAJOR-4's own stated limit. (e) puts the price where it
belongs — with the project — and stage 0 judges again.
Arms: (a) the library seam anchors * (b) control: without it the same base is un-anchored *
(c) it is the SAME baseline the validator is handed, so stage 0 really judges * (d) two sources for
one baseline are refused at the library seam * (e) the free trip anchors too * (f) a missing file
and (g) a malformed one are refused with ``load_cost_baseline``'s own error classes * (h)(i)(j)(k)
four CLI refusals BY NAME, each with an rc-0 control on an argv that would otherwise be accepted *
(l) the CLI wiring, measured on the stamp * (m) the notice * (n) EVERY base of an ``--across-bundle``
pass gets the SAME schedule.
"""
from __future__ import annotations
import json
import shutil
from pathlib import Path
from typing import Any
import pytest
from pydantic import ValidationError
from portfolio_optimiser import okf, run
from portfolio_optimiser.ir import CostBaseline, CostBaselineLine
from portfolio_optimiser.simulation import ScriptedChatClient
from portfolio_optimiser.validator import Rejection, validate_proposal
_EXAMPLES = Path(__file__).resolve().parents[1] / "shared" / "examples"
#: A base that ships NO ``cost-baseline.json`` — the whole class this flag exists for.
_UNPRICED_SOURCE = _EXAMPLES / "bygg-energi-mikro"
_IR_PROJECTION = {
"project_id": "P-KNOWLEDGE",
"measure": "PLACEHOLDER - authored by this test to satisfy the bundle contract",
"affected_items": [{"code": "KNOW-1", "quantity": 10, "unit_cost": 100.0}],
"claimed_saving_nok": 500.0,
}
def _runnable(tmp_path: Path, *, name: str = "base") -> str:
root = tmp_path / name
shutil.copytree(_UNPRICED_SOURCE, root)
(root / "validator-input.json").write_text(json.dumps(_IR_PROJECTION), encoding="utf-8")
assert not (root / "cost-baseline.json").exists(), "the fixture must be UNPRICED"
return str(root)
def _schedule(tmp_path: Path, *, name: str = "cost-baseline.json", **codes: float) -> str:
"""The PROJECT's own price schedule, written OUTSIDE every knowledge base — which is the whole
point: ``safe_resolve`` guards the bundle door, and a project's schedule is legitimately not in
a bundle."""
path = tmp_path / name
path.write_text(
json.dumps(
{
"project_id": "P-KNOWLEDGE",
"items": {
code: {"quantity": 10.0, "unit_cost": unit} for code, unit in codes.items()
},
}
),
encoding="utf-8",
)
return str(path)
def _scripted(sink: list[str] | None = None) -> Any:
return lambda role: ScriptedChatClient(sink=sink, role=role, default_reply="ok")
# --- (a)/(b)/(c)/(d)/(e) the library seam ---------------------------------------------------------
async def test_a_project_schedule_anchors_a_base_that_ships_none(tmp_path: Path) -> None:
bundle_dir = _runnable(tmp_path)
baseline = okf.load_cost_baseline_file(_schedule(tmp_path, RIGG=1000.0))
report = await run.run_project(
"P-KNOWLEDGE",
"local",
docs_dir=bundle_dir,
bundle_dir=bundle_dir,
cost_baseline=baseline,
live_dry_run=True,
)
assert isinstance(report, run.DryRunReport)
assert report.cost_baseline_anchored is True
async def test_control_without_the_schedule_the_same_base_is_unanchored(tmp_path: Path) -> None:
"""The discriminator for the arm above: the SAME base, the flag removed. Without this, "it is
anchored" could be a property of the fixture rather than of the file."""
bundle_dir = _runnable(tmp_path)
report = await run.run_project(
"P-KNOWLEDGE", "local", docs_dir=bundle_dir, bundle_dir=bundle_dir, live_dry_run=True
)
assert isinstance(report, run.DryRunReport)
assert report.cost_baseline_anchored is False
def test_the_supplied_schedule_is_what_stage_0_judges_against() -> None:
"""The stamp says anchored; this says the anchoring DOES something.
An implementation that read the file, stamped ``True`` and handed the validator ``None`` would
pass every other arm here — the mutation the order names as (i). Stage 0 either refuses a code
the project does not buy or it does not, and that is the only thing worth having.
"""
baseline = CostBaseline(
project_id="P-KNOWLEDGE",
items={"RIGG": CostBaselineLine(quantity=10.0, unit_cost=1000.0)},
)
from portfolio_optimiser.ir import AffectedItem, SavingsProposal
invented = SavingsProposal(
project_id="P-KNOWLEDGE",
measure="m",
affected_items=[AffectedItem(code="INDEKS-01", quantity=10.0, unit_cost=1000.0)],
claimed_saving_nok=100.0,
)
outcome = validate_proposal(invented, baseline=baseline)
assert isinstance(outcome, Rejection)
assert "cost baseline" in outcome.reason
# The control: the SAME magnitudes on a code the project DOES buy are not refused by stage 0.
real = SavingsProposal(
project_id="P-KNOWLEDGE",
measure="m",
affected_items=[AffectedItem(code="RIGG", quantity=10.0, unit_cost=1000.0)],
claimed_saving_nok=100.0,
)
second = validate_proposal(real, baseline=baseline)
assert not (isinstance(second, Rejection) and "cost baseline" in second.reason)
async def test_two_sources_for_one_baseline_are_refused_at_the_library_seam(
tmp_path: Path,
) -> None:
"""(d) Checked in ``run_project`` and not only in the CLI: the library takes the same two
arguments, and a library caller must not reach a state the CLI refuses by name."""
bundle_dir = _runnable(tmp_path)
baseline = okf.load_cost_baseline_file(_schedule(tmp_path, RIGG=1000.0))
with pytest.raises(ValueError, match="two sources for one baseline"):
await run.run_project(
"P-KNOWLEDGE",
"local",
docs_dir=bundle_dir,
bundle_dir=bundle_dir,
cost_baseline=baseline,
derive_cost_baseline=True,
live_dry_run=True,
)
async def test_it_satisfies_the_anchoring_requirement(tmp_path: Path) -> None:
"""(e) ``--require-cost-baseline`` is the guarantee; this is one of the two ways to meet it."""
bundle_dir = _runnable(tmp_path)
baseline = okf.load_cost_baseline_file(_schedule(tmp_path, RIGG=1000.0))
report = await run.run_project(
"P-KNOWLEDGE",
"local",
docs_dir=bundle_dir,
bundle_dir=bundle_dir,
cost_baseline=baseline,
require_cost_baseline=True,
live_dry_run=True,
)
assert isinstance(report, run.DryRunReport)
assert report.cost_baseline_anchored is True
# --- (f)/(g) the loader's error classes -----------------------------------------------------------
def test_a_missing_schedule_is_refused(tmp_path: Path) -> None:
with pytest.raises(FileNotFoundError):
okf.load_cost_baseline_file(str(tmp_path / "nope.json"))
def test_a_malformed_schedule_is_refused(tmp_path: Path) -> None:
"""``load_cost_baseline``'s own classes, and there is no tolerant twin: this path exists only
because an operator NAMED a file, so degrading its absence would answer an explicit order with
a silently un-anchored run."""
bad = tmp_path / "bad.json"
bad.write_text('{"project_id": "P", "items": {"X": {"quantity": -1, "unit_cost": 0}}}', "utf-8")
with pytest.raises(ValidationError):
okf.load_cost_baseline_file(str(bad))
def test_the_bundle_loader_still_parses_through_the_same_seam(tmp_path: Path) -> None:
"""ONE parse, two doors (kø-(p)). What differs is the RESOLUTION: ``safe_resolve`` stays on the
bundle door alone, because a project's own schedule is legitimately outside every base."""
base = tmp_path / "b"
base.mkdir()
(base / "cost-baseline.json").write_text(
json.dumps({"project_id": "P", "items": {"X": {"quantity": 1, "unit_cost": 2}}}), "utf-8"
)
assert okf.load_cost_baseline(str(base)).items["X"].unit_cost == 2.0
# --- (h)/(i)/(j)/(k) four CLI refusals, each with an rc-0 control ----------------------------------
def test_cli_requires_a_knowledge_base(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
"""(h) On the reference-project path the baseline IS ``Project.cost_items``, so a file there is
a second source for one fact with nothing to break the tie (``--require-cost-baseline``'s
reason)."""
rc = run.main(["P1", "--docs-dir", "docs", "--cost-baseline", _schedule(tmp_path, RIGG=1000.0)])
assert rc == 1
assert "--cost-baseline" in capsys.readouterr().err
def test_cli_refuses_two_sources_by_name(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""(i) BY NAME so the operator hears WHICH two flags conflict, rather than a traceback."""
bundle_dir = _runnable(tmp_path)
schedule = _schedule(tmp_path, RIGG=1000.0)
assert (
run.main(
[
"P-KNOWLEDGE",
"--bundle-dir",
bundle_dir,
"--cost-baseline",
schedule,
"--live-dry-run",
]
)
== 0
), "the control argv must be ACCEPTED"
capsys.readouterr()
rc = run.main(
[
"P-KNOWLEDGE",
"--bundle-dir",
bundle_dir,
"--cost-baseline",
schedule,
"--derive-cost-baseline",
"--live-dry-run",
]
)
assert rc == 1
err = capsys.readouterr().err
assert "--cost-baseline" in err and "--derive-cost-baseline" in err
def test_cli_is_refused_in_portfolio_mode(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""(j) A portfolio pass keys on PROJECTS, each already anchored by its own ``cost_items``, so
ONE file could be right for at most one row out of N. BY NAME, its neighbours' reason."""
rc = run.main(["--portfolio", "--cost-baseline", _schedule(tmp_path, RIGG=1000.0)])
assert rc == 1
assert "--portfolio" in capsys.readouterr().err
def test_cli_is_refused_in_report_mode(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
"""(k) Report mode returns ABOVE every dispatch, so an omission from the allowlist is a SILENT
DROP — the file would be accepted, nothing anchored, exit 0 (the F4 gap)."""
ledger = tmp_path / "ledger.json"
ledger.write_text("[]", encoding="utf-8")
assert run.main(["--report", "--ledger", str(ledger)]) == 0, "the control argv must be ACCEPTED"
capsys.readouterr()
rc = run.main(
["--report", "--ledger", str(ledger), "--cost-baseline", _schedule(tmp_path, RIGG=1000.0)]
)
assert rc == 1
assert "mode-exclusive" in capsys.readouterr().err
# --- (l)/(m) the CLI wiring and the notice --------------------------------------------------------
def test_cli_wiring_anchors_the_dry_run_and_says_so(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""(l)+(m) The flag must REACH ``run_project``, and the operator must be able to see that it
did on the FREE trip. rc 0 alone proves neither, so the discriminators are the two lines: the
un-anchored warning is GONE and the positive line names the file and the count."""
monkeypatch.delenv("PORTFOLIO_MODEL_MAP", raising=False)
bundle_dir = _runnable(tmp_path)
argv = ["P-KNOWLEDGE", "--bundle-dir", bundle_dir, "--live-dry-run"]
assert run.main(argv) == 0
before = capsys.readouterr().out
assert "Cost baseline: NONE in the bundle" in before
assert "Cost baseline: 2 lines from" not in before
schedule = _schedule(tmp_path, RIGG=1000.0, LISENS=250.0)
assert run.main([*argv, "--cost-baseline", schedule]) == 0
after = capsys.readouterr().out
assert "Cost baseline: NONE in the bundle" not in after
assert f"Cost baseline: 2 lines from {schedule}" in after
def test_the_notice_is_omitted_when_nobody_named_a_file() -> None:
"""Omission where it is unambiguous — there is exactly one way to supply a schedule, so
silence means nobody did (``cost_baseline_notice``'s rule, kept)."""
assert run.cost_baseline_source_notice(None, 0) is None
assert run.cost_baseline_source_notice("x.json", 1) == (
" Cost baseline: 1 line from x.json — the validator's stage 0 reconciles every proposed "
"cost line against this project's own schedule"
)
# --- (n) every base of a multi-base pass ----------------------------------------------------------
async def test_every_base_of_an_across_bundle_pass_gets_the_same_schedule(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""(n) ONE project has ONE price schedule, so it anchors EVERY base.
The order's mutation (ii) is "only the first base gets it", and the assert is therefore per
BASE: a recorder that stopped at the first call would pass on exactly that mutation — the
vacuous-gate class this repo keeps measuring. Both calls are recorded and both must carry the
SAME object, because two reads of one file is already one resolution too many (kø-(p)).
"""
from portfolio_optimiser.mandate import Approach, Mandate
first = _runnable(tmp_path, name="one")
second = _runnable(tmp_path, name="two")
baseline = okf.load_cost_baseline_file(_schedule(tmp_path, RIGG=1000.0))
calls: list[dict[str, Any]] = []
async def _recorder(project_id: str, profile: Any = "local", **kwargs: Any) -> Any:
calls.append({"project_id": project_id, **kwargs})
class _Stub:
coverage: tuple[Any, ...] = ()
provenance = None
return _Stub()
monkeypatch.setattr(run, "run_project", _recorder)
mandate = Mandate(
objective="o",
success_criteria="s",
approaches=[
Approach(
id="a1",
label="one",
affected_codes=["RIGG"],
claimed_saving_nok=1.0,
bundle_id="one",
),
Approach(
id="a2",
label="two",
affected_codes=["RIGG"],
claimed_saving_nok=1.0,
bundle_id="two",
),
],
)
await run.run_mandate_across_bundles(mandate, [first, second], "local", cost_baseline=baseline)
assert len(calls) == 2, f"the dispatch ran {len(calls)} base(s), not two"
assert [c["bundle_dir"] for c in calls] == [first, second]
assert [c.get("cost_baseline") for c in calls] == [baseline, baseline], (
"a base was dispatched without the project's own schedule"
)
# The control: without the flag, no base is handed one — so the arm above measures the flag
# rather than a default.
calls.clear()
await run.run_mandate_across_bundles(mandate, [first, second], "local")
assert [c.get("cost_baseline") for c in calls] == [None, None]