feat(okf): one bundle-id rule, reconciled against the mount and origin-marked
[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>
This commit is contained in:
parent
60e51ab76c
commit
842c51401d
5 changed files with 388 additions and 4 deletions
|
|
@ -859,7 +859,12 @@ def navigator_tools(bundle_dirs: Sequence[str]) -> list[FunctionTool]:
|
||||||
description="Open ONE knowledge base by id and read its navigated context.",
|
description="Open ONE knowledge base by id and read its navigated context.",
|
||||||
)
|
)
|
||||||
def read_bundle(bundle_id: str) -> str:
|
def read_bundle(bundle_id: str) -> str:
|
||||||
return okf.bundle_context(okf.navigate_bundle(_resolve_bundle(index, bundle_id)))
|
bundle_dir = _resolve_bundle(index, bundle_id)
|
||||||
|
# The base is OPENED here, so this is where it is reconciled against its mount (Step 10).
|
||||||
|
# ``_bundle_index`` above stays PURE — it does no file I/O, and must not: two of its own
|
||||||
|
# arms configure directories that do not exist and expect an id error, not an I/O one.
|
||||||
|
okf.reconcile_bundle_id(bundle_dir)
|
||||||
|
return okf.bundle_context(okf.navigate_bundle(bundle_dir))
|
||||||
|
|
||||||
@tool(
|
@tool(
|
||||||
name="read_file",
|
name="read_file",
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,9 @@ class Approach(BaseModel):
|
||||||
id: str = Field(min_length=1)
|
id: str = Field(min_length=1)
|
||||||
label: str = Field(min_length=1)
|
label: str = Field(min_length=1)
|
||||||
description: str = ""
|
description: str = ""
|
||||||
#: Which knowledge base this approach belongs to (§ C.7), by the base's id — the directory's
|
#: Which knowledge base this approach belongs to (§ C.7), by the base's id — exactly as
|
||||||
#: BASENAME, exactly as ``explore._bundle_index`` names it. DEFAULTS to empty, meaning "no base
|
#: ``okf.reconcile_bundle_id`` resolves it (the base's own declaration where it makes one, the
|
||||||
|
#: mount's basename otherwise). DEFAULTS to empty, meaning "no base
|
||||||
#: named": a legitimate statement when the run has only one base to name, and what keeps every
|
#: named": a legitimate statement when the run has only one base to name, and what keeps every
|
||||||
#: mandate written before multi-base existed valid and dispatchable unchanged.
|
#: mandate written before multi-base existed valid and dispatchable unchanged.
|
||||||
#:
|
#:
|
||||||
|
|
|
||||||
|
|
@ -846,6 +846,101 @@ def navigate_bundle(bundle_dir: str) -> Bundle:
|
||||||
return Bundle(dir=bundle_dir, files=tuple(files), skipped=tuple(skipped))
|
return Bundle(dir=bundle_dir, files=tuple(files), skipped=tuple(skipped))
|
||||||
|
|
||||||
|
|
||||||
|
class BundleIdMismatch(ValueError):
|
||||||
|
"""A base declares a ``bundle_id`` its mount does not carry.
|
||||||
|
|
||||||
|
A ``ValueError`` DELIBERATELY: it must land on ``run.main``'s refusal tuple and on hosting's
|
||||||
|
400 arm rather than on the crash channel. ``ExplorationError`` is a ``RuntimeError`` and would
|
||||||
|
give an operator a traceback and an automated caller a 500 — a configuration mistake dressed
|
||||||
|
as a server fault.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
#: Which source answered. THREE values, not a boolean (operator decision B1): a caller that cannot
|
||||||
|
#: tell "the concept said so" from "we fell back twice" has been handed a stamp it cannot audit.
|
||||||
|
BundleIdOrigin = Literal["declared-concept", "declared-index", "mount-derived"]
|
||||||
|
|
||||||
|
_BUNDLE_ID_KEY = "bundle_id"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ResolvedBundleId:
|
||||||
|
"""A base's id together with WHERE it came from.
|
||||||
|
|
||||||
|
``origin`` is REQUIRED WITHOUT DEFAULT, for the reason ``ProvenanceStamp.cost_baseline_anchored``
|
||||||
|
is: both defaults would lie about an event. Defaulting to ``"mount-derived"`` would let a
|
||||||
|
resolver that never read the base claim it had; defaulting to a declared value would claim a
|
||||||
|
declaration that never happened.
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
origin: BundleIdOrigin
|
||||||
|
|
||||||
|
|
||||||
|
def _declared_bundle_id(frontmatter: dict[str, str]) -> str:
|
||||||
|
"""The declared id, de-quoted through ``unquote_scalar`` — this repo's ONE de-quoting rule."""
|
||||||
|
return unquote_scalar(frontmatter.get(_BUNDLE_ID_KEY, "")).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _reconciled(
|
||||||
|
declared: str, mount: str, origin: BundleIdOrigin, bundle_dir: str
|
||||||
|
) -> ResolvedBundleId:
|
||||||
|
if declared != mount:
|
||||||
|
raise BundleIdMismatch(
|
||||||
|
f"knowledge base {bundle_dir!r} declares bundle_id {declared!r} but is mounted as "
|
||||||
|
f"{mount!r}; an approach names a base by that id, so a run would evaluate against one "
|
||||||
|
f"and report the other (source: {origin})"
|
||||||
|
)
|
||||||
|
return ResolvedBundleId(id=declared, origin=origin)
|
||||||
|
|
||||||
|
|
||||||
|
def reconcile_bundle_id(
|
||||||
|
bundle_dir: str | Path, *, concept_name: str | None = None
|
||||||
|
) -> ResolvedBundleId:
|
||||||
|
"""Resolve a base's ``bundle_id`` against the mount it was opened from — the ONE rule.
|
||||||
|
|
||||||
|
``bundle_id`` denotes the corpus **as a mounted artefact**, never the production event that made
|
||||||
|
it (operator decision D6). That denotation cannot be changed once artefacts bear the stamp,
|
||||||
|
which is why it is written down here rather than left to each call site.
|
||||||
|
|
||||||
|
Resolution order is operator decision B1 — identity is the pair ``(bundle_id, concept_id)``, and
|
||||||
|
the CONCEPT's own frontmatter is consulted first:
|
||||||
|
|
||||||
|
1. ``concept_name``'s frontmatter, when a concept is named and declares the key →
|
||||||
|
``"declared-concept"``;
|
||||||
|
2. the root ``index.md``'s frontmatter → ``"declared-index"``;
|
||||||
|
3. the mount's basename → ``"mount-derived"``.
|
||||||
|
|
||||||
|
A declared id that DISAGREES with the mount raises ``BundleIdMismatch`` at whichever source
|
||||||
|
declared it. A base whose root ``index.md`` cannot be read is **unknown, not undeclared**:
|
||||||
|
``navigate_bundle``'s fail-fast propagates unchanged, because reading an unreadable base as
|
||||||
|
"it declares nothing" would widen the answer on missing evidence.
|
||||||
|
|
||||||
|
MEASURED 2026-09-02: no file under ``shared/`` declares the key (zero ``^bundle_id`` hits
|
||||||
|
against a known-positive control of 31 files carrying ``^type:``), so every base in this repo
|
||||||
|
resolves ``mount-derived`` today and both declared branches are DEFENSIVE.
|
||||||
|
|
||||||
|
:raises BundleIdMismatch: a declared id the mount does not carry.
|
||||||
|
:raises ValueError: the root ``index.md`` is missing or unreadable.
|
||||||
|
"""
|
||||||
|
root = str(bundle_dir)
|
||||||
|
mount = Path(root).name
|
||||||
|
if concept_name is not None:
|
||||||
|
concept = _load_file(root, concept_name)
|
||||||
|
if concept is None:
|
||||||
|
raise ValueError(f"OKF bundle has no readable concept {concept_name!r}: {root!r}")
|
||||||
|
declared = _declared_bundle_id(concept.frontmatter)
|
||||||
|
if declared:
|
||||||
|
return _reconciled(declared, mount, "declared-concept", root)
|
||||||
|
index = _load_file(root, _INDEX_NAME)
|
||||||
|
if index is None:
|
||||||
|
raise ValueError(f"OKF bundle has no readable {_INDEX_NAME}: {root!r}")
|
||||||
|
declared = _declared_bundle_id(index.frontmatter)
|
||||||
|
if declared:
|
||||||
|
return _reconciled(declared, mount, "declared-index", root)
|
||||||
|
return ResolvedBundleId(id=mount, origin="mount-derived")
|
||||||
|
|
||||||
|
|
||||||
def bundle_context(bundle: Bundle, *, dimension: str | None = None) -> str:
|
def bundle_context(bundle: Bundle, *, dimension: str | None = None) -> str:
|
||||||
"""Render a navigated bundle as agent read-context via progressive disclosure: the ``index.md``
|
"""Render a navigated bundle as agent read-context via progressive disclosure: the ``index.md``
|
||||||
summary, then each concept file as ``## {type}: {title}\\n{body}``. ``type: verdict`` files are
|
summary, then each concept file as ``## {type}: {title}\\n{body}``. ``type: verdict`` files are
|
||||||
|
|
|
||||||
|
|
@ -658,6 +658,10 @@ async def run_project(
|
||||||
# before the amendment (every commons-owned golden) is legitimately un-anchored -> None =
|
# before the amendment (every commons-owned golden) is legitimately un-anchored -> None =
|
||||||
# pre-S4.0 behaviour. A baseline that exists but is malformed still raises (fail-closed).
|
# pre-S4.0 behaviour. A baseline that exists but is malformed still raises (fail-closed).
|
||||||
if bundle_dir is not None:
|
if bundle_dir is not None:
|
||||||
|
# ONE bundle-id rule (Step 10): the base is reconciled against the mount it was opened
|
||||||
|
# from, HERE rather than in ``explore._bundle_index``, which stays pure and does no I/O.
|
||||||
|
# A base that declares an id its mount does not carry is refused before anything is spent.
|
||||||
|
okf.reconcile_bundle_id(bundle_dir)
|
||||||
bundle = okf.navigate_bundle(bundle_dir)
|
bundle = okf.navigate_bundle(bundle_dir)
|
||||||
project = _project_from_bundle(bundle_dir, project_id, bundle=bundle)
|
project = _project_from_bundle(bundle_dir, project_id, bundle=bundle)
|
||||||
baseline = okf.load_optional_cost_baseline(bundle_dir)
|
baseline = okf.load_optional_cost_baseline(bundle_dir)
|
||||||
|
|
@ -1511,7 +1515,11 @@ async def run_mandate_across_bundles(
|
||||||
"""
|
"""
|
||||||
by_id: dict[str, str] = {}
|
by_id: dict[str, str] = {}
|
||||||
for raw in bundle_dirs:
|
for raw in bundle_dirs:
|
||||||
bundle_id = Path(raw).name
|
# The ONE derivation rule (Step 10) — this used to be a second private copy of
|
||||||
|
# ``Path(raw).name``, free to drift from ``explore``'s. The REFUSAL below stays local:
|
||||||
|
# ``MandateRoutingError`` is this door's class, ``ExplorationError`` is explore's, and
|
||||||
|
# unifying the derivation is not the same as unifying the two doors' error vocabularies.
|
||||||
|
bundle_id = okf.reconcile_bundle_id(raw).id
|
||||||
if bundle_id in by_id:
|
if bundle_id in by_id:
|
||||||
# The same refusal ``explore._bundle_index`` makes, for the same reason: the id is how
|
# The same refusal ``explore._bundle_index`` makes, for the same reason: the id is how
|
||||||
# the mandate names a base, so two bases answering to one name would let an approach be
|
# the mandate names a base, so two bases answering to one name would let an approach be
|
||||||
|
|
|
||||||
275
tests/test_bundle_id_reconciliation_loadbearing.py
Normal file
275
tests/test_bundle_id_reconciliation_loadbearing.py
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
"""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"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue