portfolio-optimiser/tests/test_bundle_id_reconciliation_loadbearing.py
Kjell Tore Guttormsen baae7507a9 feat(okf): den erklaerte bundle_id-en er identiteten - mountet er en filsystem-tilfeldighet
S7a-3 pkt. 1. Til i dag NEKTET reconcile_bundle_id en base som erklaerte en id
katalogen ikke bar. Maalt mot K2 - den foerste leverte basen som erklaerer sin
egen id (618 av 630 konseptfiler + rot-index, alle "k2-trinn1-20260903", levert
som "K2-bundle-20260903") - betydde det at basen ikke kunne aapnes slik den var
levert, og at eneste botemiddel var aa montere den paa nytt for haand, en gang
per leveranse. PM-beslutning: konsumenten slakker.

- Erklaert vinner (B1s rekkefoelge uroert), avviket REGISTRERES:
  ResolvedBundleId.mount + ProvenanceStamp.bundle_id_source +
  DryRunReport.bundle_id_source + run.bundle_id_notice (None ved enighet).
  Stempel-feltet er PAAKREVD uten default: None er en VERDI (veg-stien).
- Det som fortsatt nekter er den EKTE kollisjonen: to KONSEPTER i en base som
  erklaerer ULIKE id-er (okf.assert_declared_ids_agree, kalt ved hver doer som
  aapner en base). Rot-index er IKKE med i enighets-settet - konsept-slaar-index
  er en presedens-regel, saa en index i utakt er fallbacken som taper.
- KONSEKVENS, ikke scope-krype: explore._bundle_index loeser naa den erklaerte
  id-en. Den brukte Path(raw).name mens dispatcheren brukte reconcile...id; med
  erklaert-vinner ville explore() myntet approaches som navngir MOUNTET mens
  dispatcheren ruter paa ERKLAERINGEN - en utforskning med uruterbart mandat.

Load-bearing MAALT: 10 mutasjoner alle roede mot HELE suiten, groenn kontroll
1243 passed / 5 skipped og golden demo-transcript.stdout byte-uendret
(shasum -a 1 = ea8c534773acdbe41ae68f2c55724d69aaf8be4f).
M1 1 / M2 1 / M3 1 / M4 9 / M5 2 / M6 1 / M7 1 / M8 2 / M9 2 / M11 1.

Tre armer i test_bundle_id_reconciliation_loadbearing er SKREVET OM (ikke
slettet) - de pinnet nekten beslutningen fjernet. (j) ble skarpere enn den den
erstattet: erklaert id ruter, mountet nektes.

Kontrakt: docs/okf-konsum-kontrakter.md § 3.1. Invariantrad i CLAUDE.md.

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

434 lines
21 KiB
Python

"""Step 10-11 (Session 4) - ONE bundle-id rule, and a cross-base collision that stops being silent.
**The defect, measured 2026-09-02.** Two modules derived a base's id by hand and identically:
``explore._bundle_index`` (``Path(raw).name``) and ``run_mandate_across_bundles`` (``run.py:1514``,
a second copy of the same line with its own refusal beside it). Neither consulted what the base
itself says. A corpus that declares its own id in ``index.md`` -- the form OKF SPEC leaves open --
would have been mounted under a directory name that disagreed with it, and every artefact the run
stamped would have named the mount while the base named itself something else. Two copies of one
derivation rule is the ko-(p) drift class; a derivation that ignores the artefact is worse, because
nothing ever disagrees out loud.
**Operator decision B1 (D6 ratified), and why ``origin`` has THREE values.** Identity is the pair
``(bundle_id, concept_id)``, and ``bundle_id`` is read from the CONCEPT's own frontmatter first,
with the root ``index.md`` as fallback, and the mount's basename as the last resort. ``origin`` is
REQUIRED WITHOUT DEFAULT for ``cost_baseline_anchored``'s reason -- both defaults would lie about an
event -- and it is not a boolean, because a caller that cannot tell "the concept said so" from "we
fell back twice" has been handed a stamp it cannot audit.
**MEASURED before building, and it decides the shape of this file (nevner stated).** No file
anywhere under ``shared/``, ``src/`` or ``tests/`` declares ``bundle_id`` in frontmatter: zero hits
for ``^bundle_id`` against a known-positive control of 31 files carrying ``^type:`` under
``shared/examples/``. Every base in the repo therefore resolves ``mount-derived`` today, and the 27
``bundle_dirs=`` call sites stay green. That makes ``declared-concept`` and ``declared-index``
DEFENSIVE branches (the ``budget_stop`` precedent): they are driven from CRAFTED bases here,
because nothing else in the suite would keep them alive.
**``explore._bundle_index`` stays PURE, and that is a correction from review, measured.**
``tests/test_explore_loadbearing.py:750`` passes ``/tmp/base-a`` and ``:764`` passes
``/tmp/one/shared-name`` -- directories that do not exist -- and both expect an ``ExplorationError``
about ids. Reading ``index.md`` there would raise on I/O before any id logic ran. Reconciliation
therefore happens where a base is actually OPENED: ``explore.read_bundle``, ``run_project``'s
bundle arm, and the dispatcher.
**A base with no readable ``index.md`` is NOT "undeclared".** ``navigate_bundle``'s fail-fast
propagates unchanged; reading an unreadable base as "it declares nothing" is the tolerant-read-
widens-the-answer defect research topic 2 measured in SPARQL's ``SILENT``.
**REWRITTEN IN PART, 2026-09-03 (S7a-3 pkt. 1).** Arms (e), (f) and (j) pinned a refusal an
operator decision removed: a declared id that disagrees with its MOUNT is now the declaration
winning, not an error -- measured against K2, the first delivered corpus that declares its own id,
which this refusal made unopenable as delivered. They were rewritten rather than deleted, each onto
what replaced it: (e) the declaration wins and the mount survives on the resolution; (f) the CLI's
early refusal now guards the collision that IS still refused (two concepts, two corpora); (j) the
dispatcher routes by the DECLARED id and refuses the mount -- a sharper discriminator than the one
it replaced. The slacken's own gate is ``tests/test_bundle_id_slack_loadbearing.py``.
**The "DEFENSIVE branch" note above is now historical.** It was true when written and measured
again on 2026-09-03: K2 declares the key in 619 files, so ``declared-index`` fires in the wild.
Nothing in THIS repo declares it, which is why the arms below still craft their bases.
Arms: (a) the CONTROL, a shipped base resolving ``mount-derived`` * (b) a declared, agreeing root
index * (c) a declaring CONCEPT beating a declaring index -- the ordering discriminator * (d) the
three origins are pairwise distinct, read off the three arms above rather than off a literal *
(e) a declared id BEATS the mount it disagrees with * (f) a base that cannot say what it is refused
through the CLI with ZERO model calls * (g) an unreadable index refuses instead of falling back *
(h)-(j) the same helper is reached from all three doors that open a base.
"""
from __future__ import annotations
import shutil
from pathlib import Path
from typing import Any
import pytest
from portfolio_optimiser import explore, okf, run
from portfolio_optimiser.mandate import Approach, Mandate, MandateRoutingError
from portfolio_optimiser.simulation import ScriptedChatClient
from portfolio_optimiser.verdicts import VerdictCollision
_EXAMPLES = Path(__file__).resolve().parents[1] / "shared" / "examples"
_BYGG = _EXAMPLES / "bygg-energi-mikro"
_PID = "BYGG-KONTOR-NORD"
class _Dispatched(Exception):
"""Raised from the stand-in ``run_project`` so an arm can prove a run WAS dispatched without
letting it proceed. An ``AssertionError`` would be indistinguishable from a failing assert."""
def _base_copy(tmp_path: Path, *, name: str = "bygg-energi-mikro") -> Path:
"""A throwaway copy of a shipped base. Mutations of fixture content NEVER touch the git-tracked
tree (the repo's ``shutil.copytree`` discipline)."""
dst = tmp_path / name
shutil.copytree(_BYGG, dst)
return dst
def _declare(path: Path, value: str) -> None:
"""Insert a ``bundle_id`` line into an existing frontmatter block, after the opening ``---``."""
lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
assert lines[0].startswith("---"), f"{path} has no frontmatter block to declare into"
lines.insert(1, f"bundle_id: {value}\n")
path.write_text("".join(lines), encoding="utf-8")
# --- (a) the CONTROL: nothing declares, so the mount answers ------------------------------------
def test_a_base_that_declares_nothing_resolves_from_the_mount() -> None:
"""(a) CONTROL. Every base shipped today takes this branch -- measured: zero ``^bundle_id``
declarations against a known-positive control of 31 files carrying ``^type:``. Without this arm
the two declared arms below could not be read as the exceptions they are."""
resolved = okf.reconcile_bundle_id(str(_BYGG))
assert resolved.id == "bygg-energi-mikro"
assert resolved.origin == "mount-derived"
# --- (b)/(c) the two declared origins ------------------------------------------------------------
def test_a_declaring_root_index_is_reconciled_and_marked_declared_index(tmp_path: Path) -> None:
"""(b) DEFENSIVE (``budget_stop`` precedent): no shipped index declares the key, so this branch
is crafted or it is untested."""
base = _base_copy(tmp_path)
_declare(base / "index.md", base.name)
resolved = okf.reconcile_bundle_id(str(base))
assert resolved.id == base.name
assert resolved.origin == "declared-index"
def test_a_declaring_concept_beats_a_declaring_index(tmp_path: Path) -> None:
"""(c) The ORDERING discriminator, and the reason both files declare the SAME (agreeing) value:
with only the concept declaring, an index-first implementation would fall through to
``mount-derived`` and could be mistaken for a bug elsewhere. With BOTH declaring, index-first
answers ``declared-index`` and concept-first answers ``declared-concept`` -- two implementations
that differ in exactly the field under test, which is what makes the arm falsifiable."""
base = _base_copy(tmp_path)
concept = next(p for p in sorted(base.rglob("*.md")) if p.name != "index.md")
_declare(base / "index.md", base.name)
_declare(concept, base.name)
resolved = okf.reconcile_bundle_id(str(base), concept_name=concept.relative_to(base).as_posix())
assert resolved.id == base.name
assert resolved.origin == "declared-concept"
def test_the_three_origins_are_pairwise_distinct(tmp_path: Path) -> None:
"""(d) B1's own reason for three values rather than a boolean: a caller must be able to tell
"the concept said so" from "the index said so" from "we fell back twice". Read off the three
RESOLUTIONS, never off a literal list -- an assert on a ``Literal``'s members is a static
property of the type that no runtime mutation can redden (the M8 correction)."""
declared_index = _base_copy(tmp_path, name="only-index")
_declare(declared_index / "index.md", "only-index")
declared_concept = _base_copy(tmp_path, name="with-concept")
target = next(p for p in sorted(declared_concept.rglob("*.md")) if p.name != "index.md")
_declare(target, "with-concept")
origins = {
okf.reconcile_bundle_id(str(_BYGG)).origin,
okf.reconcile_bundle_id(str(declared_index)).origin,
okf.reconcile_bundle_id(
str(declared_concept),
concept_name=target.relative_to(declared_concept).as_posix(),
).origin,
}
assert len(origins) == 3, f"two sources collapsed onto one origin: {sorted(origins)}"
# --- (e)/(f) disagreement is refused, and refused EARLY -------------------------------------------
def test_a_base_that_disagrees_with_its_mount_wins_over_it(tmp_path: Path) -> None:
"""(e) REWRITTEN 2026-09-03 (S7a-3 pkt. 1), not deleted. This arm used to pin the opposite
behaviour: a declared id the mount did not carry was refused by name. The operator decision
slackened it -- the declaration IS the identity, the mount is a filesystem accident -- so the
arm now pins what replaced it, and the refusal it used to pin lives in
``test_bundle_id_slack_loadbearing.py`` in its new form (two CONCEPTS disagreeing)."""
base = _base_copy(tmp_path)
_declare(base / "index.md", "a-name-the-mount-does-not-carry")
resolved = okf.reconcile_bundle_id(str(base))
assert resolved.id == "a-name-the-mount-does-not-carry"
assert resolved.origin == "declared-index"
assert resolved.mount == base.name, "the overridden mount must survive on the resolution"
def test_the_cli_refuses_a_base_that_cannot_say_what_it_is_before_it_spends_a_call(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""(f) REWRITTEN 2026-09-03 with the refusal it now guards: this used to drive a base whose
declared id disagreed with its MOUNT, which no longer refuses. The EARLY half is what the arm
was always for and is unchanged -- the assert is on ZERO model calls, never on the exit code
alone, because a refusal that arrives AFTER the spend looks identical at rc 1 (the M12
signature, and the hoist idiom ``test_explore_callsites_loadbearing.py`` already uses twice)."""
monkeypatch.delenv("PORTFOLIO_MODEL_MAP", raising=False)
monkeypatch.delenv("PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT", raising=False)
base = _base_copy(tmp_path)
concepts = [p for p in sorted(base.rglob("*.md")) if p.name != "index.md"]
_declare(concepts[0], "corpus-alfa")
_declare(concepts[1], "corpus-beta")
sink: list[str] = []
def counting_factory(profile: Any) -> Any:
return lambda role: ScriptedChatClient(sink=sink, role=role, default_reply="ok")
monkeypatch.setattr("portfolio_optimiser.run._default_factory", counting_factory)
rc = run.main([_PID, "--docs-dir", str(base), "--bundle-dir", str(base)])
assert rc == 1
assert sink == [], (
"the run reached the model before the base was asked what it is -- the base was paid for "
f"before it was refused ({len(sink)} calls)"
)
# --- (g) absence is not a declaration ------------------------------------------------------------
def test_an_unreadable_index_refuses_instead_of_falling_back_to_the_mount(tmp_path: Path) -> None:
"""(g) The tolerant-read trap, refused. A base whose index cannot be read is UNKNOWN, not
undeclared; answering ``mount-derived`` there would widen the answer on missing evidence."""
empty = tmp_path / "no-index-here"
empty.mkdir()
with pytest.raises(ValueError, match="index.md"):
okf.reconcile_bundle_id(str(empty))
# --- (h)-(j) one helper, reached from all three doors ---------------------------------------------
@pytest.fixture
def _spy(monkeypatch: pytest.MonkeyPatch) -> list[str]:
"""Record every reconciliation while delegating to the real one. A private basename copy at a
call site leaves this list empty, which is the only witness that distinguishes "reconciled" from
"still derived by hand" (mutation M11)."""
seen: list[str] = []
real = okf.reconcile_bundle_id
def spy(bundle_dir: Any, **kwargs: Any) -> Any:
seen.append(str(bundle_dir))
return real(bundle_dir, **kwargs)
monkeypatch.setattr(okf, "reconcile_bundle_id", spy)
return seen
def test_explore_read_bundle_reconciles_the_base_it_opens(_spy: list[str]) -> None:
"""(h) The tool is driven DIRECTLY: measured in okt 56, a scripted client returns TEXT and never
emits a tool call, so a gate that only drove ``explore()`` would never enter this body."""
tools = {t.name: t for t in explore.navigator_tools((str(_BYGG),))}
# See test_read_bundle_cost_loadbearing: the tool returns a LISTING since S2c, and ``!= ""``
# holds for every list ever built — this arm is about the reconciliation, so the call must
# still be a call that DID something.
assert [e["name"] for e in tools["read_bundle"].func(bundle_id=_BYGG.name)]
assert str(_BYGG) in _spy
@pytest.mark.asyncio
async def test_run_project_reconciles_the_base_its_bundle_arm_opens(_spy: list[str]) -> None:
"""(i) The pipeline door. ``run_project``'s bundle arm is where the project, the stage-0
baseline and the read context are all derived from one base -- the id must come from the same
reconciliation, not from a fourth private copy."""
await run.run_project(
_PID,
docs_dir=str(_BYGG),
bundle_dir=str(_BYGG),
client_factory=lambda role: ScriptedChatClient(role=role, default_reply="ok"),
live_dry_run=True,
)
assert str(_BYGG) in _spy
@pytest.mark.asyncio
async def test_the_dispatcher_routes_by_the_declared_id_not_the_mount(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""(j) REWRITTEN 2026-09-03 (S7a-3 pkt. 1) and now SHARPER than what it replaced.
It used to pin that a base disagreeing with its mount was refused while assembling the routing
table. That refusal is gone; what took its place is a discriminator the old arm could not have:
on a base whose declaration disagrees with its directory, the DECLARED id must route and the
MOUNT must not. A dispatcher left on ``Path(raw).name`` answers exactly the other way round, so
the two implementations differ in both halves rather than in an exit code.
The mount half is also the arm that keeps ``explore()`` and this door on ONE naming scheme: an
exploration mints approaches from ``_bundle_index``, and if the two resolved ids differently an
exploration's own mandate would be unroutable here.
"""
base = _base_copy(tmp_path)
_declare(base / "index.md", "a-name-the-mount-does-not-carry")
started: list[str] = []
async def counting_run_project(*args: Any, **kwargs: Any) -> Any:
started.append(str(kwargs.get("bundle_dir")))
raise _Dispatched
monkeypatch.setattr(run, "run_project", counting_run_project)
def _mandate(named: str) -> Mandate:
return Mandate(
objective="o",
approaches=(Approach(id="a", label="A", bundle_id=named),),
allow_own_proposals=False,
)
with pytest.raises(_Dispatched):
await run.run_mandate_across_bundles(
_mandate("a-name-the-mount-does-not-carry"), (str(base),)
)
assert started == [str(base)], "the declared id must route to the base that declared it"
with pytest.raises(MandateRoutingError, match=base.name):
await run.run_mandate_across_bundles(_mandate(base.name), (str(base),))
assert started == [str(base)], "the mount name is not an id once the base declares one"
# ==================================================================================================
# Step 11 - a cross-base candidate collision stops being silent
# ==================================================================================================
#
# **The defect (operator decision D2).** ``VerdictStore.add`` is first-write-wins per id and
# ``_mint_id`` excludes the corpus BY CONSTRUCTION (it hashes ``affected_codes`` / ``measure_type``
# / ``claimed_saving_nok``, and nothing else). Two independently produced bases that describe the
# same candidate therefore collapse onto ONE verdict and the second is dropped in silence. No test
# covered it.
#
# **``_mint_id`` is NOT changed** -- its form is normative in ``shared/method-spec.md`` and
# commons-owned, so altering it is an amendment, not a local edit. **``add`` is not changed either:**
# it is called from inside ``run_project`` and ``run_portfolio``, never from
# ``run_mandate_across_bundles``, so a return value there would reach no code in this step's scope,
# and a drop there can equally be against a Step-7 inbox verdict or a bundle seed rather than a
# second base. The mechanism is dispatcher-side accounting, because the dispatcher is the one place
# that holds the id -> base map the store does not keep.
_COLLIDING_PROPOSAL = (
'{"measure":"LED-retrofit av kontorbelysning","affected_items":'
'[{"code":"ENERGI-TOTAL-EL","quantity":300000,"unit_cost":1.0}],"claimed_saving_nok":30000}'
)
_VERDICT_INPUT = {"decision": "approved", "rationale": "expert reviewed (fixture)"}
def _colliding_factory() -> Any:
"""Both bases answer with the SAME proposal, which is what makes the two ids collide -- the
corrected fixture note: ``_mint_id`` hashes the PROPOSAL's features, not the base's IR
projection, so matching projections alone would mint nothing and the arm would pass for the
wrong reason.
The role names are ``workflow._MAKER_CHECKER_ROLES`` verbatim (``proposer`` / ``checker``); a
typo there would feed the proposal JSON to the checker and the arm would still pass, since F2
mints a verdict on either outcome -- green for a different reason than the docstring claims.
The outcome itself is deliberately NOT asserted: this arm is about candidate IDENTITY, and
``_mint_id`` reads the same three structural fields whether the gate validated or rejected."""
return lambda role: ScriptedChatClient(
role=role,
default_reply=(_COLLIDING_PROPOSAL if role != "checker" else "Holder. VERDICT: APPROVE"),
)
def _two_bases(tmp_path: Path) -> tuple[Path, Path]:
return _base_copy(tmp_path, name="base-one"), _base_copy(tmp_path, name="base-two")
def _mandate_over(*bundle_ids: str) -> Any:
return Mandate(
objective="find the same saving twice",
approaches=tuple(
Approach(id=f"a{i}", label=f"A{i}", bundle_id=bid) for i, bid in enumerate(bundle_ids)
),
allow_own_proposals=False,
)
@pytest.mark.asyncio
async def test_two_bases_describing_one_candidate_are_reported_not_dropped(tmp_path: Path) -> None:
"""(k) The measured silence, closed. Two bases, one candidate: the collision names BOTH bases
and the shared id, in dispatch order."""
one, two = _two_bases(tmp_path)
result = await run.run_mandate_across_bundles(
_mandate_over(one.name, two.name),
(str(one), str(two)),
verdict_input=_VERDICT_INPUT,
client_factory=_colliding_factory(),
)
assert len(result.collisions) == 1, f"expected one collision, got {result.collisions}"
collision = result.collisions[0]
assert collision.first_bundle_id == one.name
assert collision.second_bundle_id == two.name
assert collision.verdict_id == result.runs[0].result.verdict_key
@pytest.mark.asyncio
async def test_a_single_base_reports_no_collision(tmp_path: Path) -> None:
"""(l) CONTROL. An empty trace is an honest POSITIVE statement ("no candidate was described by
two bases") -- the ``skipped_links`` half of the required-vs-default rule, not the
``cost_baseline_anchored`` half. Without this arm, an implementation that always appended would
pass (k)."""
one = _base_copy(tmp_path, name="base-one")
result = await run.run_mandate_across_bundles(
_mandate_over(one.name),
(str(one),),
verdict_input=_VERDICT_INPUT,
client_factory=_colliding_factory(),
)
assert result.collisions == ()
@pytest.mark.asyncio
async def test_the_drop_itself_is_unchanged_only_its_visibility(tmp_path: Path) -> None:
"""(m) This step changes VISIBILITY, never semantics. The store still holds exactly one verdict
for the shared id: ``add`` is untouched and first-write-wins still wins."""
one, two = _two_bases(tmp_path)
result = await run.run_mandate_across_bundles(
_mandate_over(one.name, two.name),
(str(one), str(two)),
verdict_input=_VERDICT_INPUT,
client_factory=_colliding_factory(),
)
shared = result.collisions[0].verdict_id
assert [v.id for v in result.store.verdicts].count(shared) == 1
def test_the_collision_notice_is_omitted_when_nothing_collided() -> None:
"""(n) Omission, never an empty row (``mandate.announce``'s rule, as ``skipped_links_notice``
and ``cost_baseline_notice`` already follow). Both directions are asserted: a renderer that
always returns a line is as wrong as one that never does."""
assert run.collision_notice(()) is None
line = run.collision_notice(
(
VerdictCollision(
verdict_id="abc123", first_bundle_id="base-one", second_bundle_id="base-two"
),
)
)
assert line is not None
assert "abc123" in line and "base-one" in line and "base-two" in line