"""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``. 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) disagreement refused BY NAME * (f) disagreement 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 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" 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_is_refused_by_name(tmp_path: Path) -> None: """(e) The refusal itself. ``BundleIdMismatch`` subclasses ``ValueError`` deliberately, so it lands on the CLI's refusal tuple and hosting's 400 arm rather than the crash channel -- ``ExplorationError`` is a ``RuntimeError`` and would give a traceback and a 500.""" base = _base_copy(tmp_path) _declare(base / "index.md", "a-name-the-mount-does-not-carry") with pytest.raises(okf.BundleIdMismatch) as excinfo: okf.reconcile_bundle_id(str(base)) message = str(excinfo.value) assert "a-name-the-mount-does-not-carry" in message and base.name in message assert issubclass(okf.BundleIdMismatch, ValueError) def test_the_cli_refuses_a_disagreeing_base_before_it_spends_a_single_model_call( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """(f) The assert is on ZERO model calls, never on the exit code alone: 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) _declare(base / "index.md", "not-the-mount") 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 mount was reconciled -- 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_refuses_a_disagreeing_base_before_it_starts_any_run( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """(j) The dispatcher door, and it needs its OWN discriminator. A declared id that AGREES with the mount is by construction the basename, so ``reconcile_bundle_id(raw).id`` and ``Path(raw).name`` return the same string on every base that resolves at all -- an assert on the routed id could not tell the two implementations apart. What CAN: a disagreeing base must be refused while assembling the routing table, i.e. BEFORE the first ``run_project`` is dispatched. Left as a private basename copy, routing succeeds and the first base is started; ``run_project``'s own reconciliation would refuse it, but only after the run had begun -- the M11/M12 pairing, one level up. """ 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 AssertionError("a run was dispatched against an unreconciled base") monkeypatch.setattr(run, "run_project", counting_run_project) with pytest.raises(okf.BundleIdMismatch): await run.run_mandate_across_bundles( Mandate( objective="o", approaches=(Approach(id="a", label="A", bundle_id=base.name),), allow_own_proposals=False, ), (str(base),), ) assert started == [], "the dispatcher started a run before reconciling the base it routed to" # ================================================================================================== # 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