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>
This commit is contained in:
parent
5adb1cca62
commit
baae7507a9
14 changed files with 708 additions and 73 deletions
|
|
@ -35,12 +35,25 @@ bundle arm, and the dispatcher.
|
|||
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) 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.
|
||||
(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
|
||||
|
|
@ -52,7 +65,7 @@ from typing import Any
|
|||
import pytest
|
||||
|
||||
from portfolio_optimiser import explore, okf, run
|
||||
from portfolio_optimiser.mandate import Approach, Mandate
|
||||
from portfolio_optimiser.mandate import Approach, Mandate, MandateRoutingError
|
||||
from portfolio_optimiser.simulation import ScriptedChatClient
|
||||
from portfolio_optimiser.verdicts import VerdictCollision
|
||||
|
||||
|
|
@ -61,6 +74,11 @@ _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)."""
|
||||
|
|
@ -145,30 +163,36 @@ def test_the_three_origins_are_pairwise_distinct(tmp_path: Path) -> None:
|
|||
# --- (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."""
|
||||
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")
|
||||
|
||||
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)
|
||||
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_disagreeing_base_before_it_spends_a_single_model_call(
|
||||
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) 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)."""
|
||||
"""(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)
|
||||
_declare(base / "index.md", "not-the-mount")
|
||||
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] = []
|
||||
|
||||
|
|
@ -180,7 +204,7 @@ def test_the_cli_refuses_a_disagreeing_base_before_it_spends_a_single_model_call
|
|||
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 "
|
||||
"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)"
|
||||
)
|
||||
|
||||
|
|
@ -243,18 +267,20 @@ async def test_run_project_reconciles_the_base_its_bundle_arm_opens(_spy: list[s
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_dispatcher_refuses_a_disagreeing_base_before_it_starts_any_run(
|
||||
async def test_the_dispatcher_routes_by_the_declared_id_not_the_mount(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""(j) The dispatcher door, and it needs its OWN discriminator.
|
||||
"""(j) REWRITTEN 2026-09-03 (S7a-3 pkt. 1) and now SHARPER than what it replaced.
|
||||
|
||||
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.
|
||||
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")
|
||||
|
|
@ -263,20 +289,26 @@ async def test_the_dispatcher_refuses_a_disagreeing_base_before_it_starts_any_ru
|
|||
|
||||
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")
|
||||
raise _Dispatched
|
||||
|
||||
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),),
|
||||
def _mandate(named: str) -> Mandate:
|
||||
return Mandate(
|
||||
objective="o",
|
||||
approaches=(Approach(id="a", label="A", bundle_id=named),),
|
||||
allow_own_proposals=False,
|
||||
)
|
||||
assert started == [], "the dispatcher started a run before reconciling the base it routed to"
|
||||
|
||||
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"
|
||||
|
||||
|
||||
# ==================================================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue