[skip-docs] — the invariant row for this plan lands in Step 13, where the mutations that force it have been measured. Documenting a seam before its measurement is the claim-without-evidence class this repo writes rows against. Co-Authored-By: Claude <claude-opus-5>
275 lines
13 KiB
Python
275 lines
13 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``.
|
|
|
|
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
|
|
|
|
_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),))}
|
|
assert 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"
|