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:
Kjell Tore Guttormsen 2026-09-03 06:47:58 +02:00
commit baae7507a9
14 changed files with 708 additions and 73 deletions

View file

@ -866,7 +866,20 @@ def navigate_bundle(bundle_dir: str) -> Bundle:
class BundleIdMismatch(ValueError):
"""A base declares a ``bundle_id`` its mount does not carry.
"""A base cannot say what it is: two of its CONCEPTS declare different ``bundle_id`` values.
**This used to mean something else, and the change is an operator decision (S7a-3 pkt. 1).**
Until 2026-09-03 it also covered a declared id that disagreed with the directory the base was
mounted under. That refusal was measured against the first delivered corpus that declares its
own id (K2: 618 of 630 concept files, mounted under a different name) and it made the base
unopenable at every door, with re-mounting by hand as the only remedy. The mount is a filesystem
accident; the declaration is the artefact speaking. A disagreement between them is now RECORDED
(``ResolvedBundleId.mount`` + ``ProvenanceStamp.bundle_id_source`` + one warning line), never
refused.
What is left is the disagreement no mount name could ever settle: within ONE base, two concepts
naming two different corpora. There is no fallback that makes that base coherent, and an
artefact stamped from it would name one of the two at random.
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
@ -884,16 +897,22 @@ _BUNDLE_ID_KEY = "bundle_id"
@dataclass(frozen=True)
class ResolvedBundleId:
"""A base's id together with WHERE it came from.
"""A base's id, WHERE it came from, and the mount it was read 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.
``mount`` is CARRIED rather than discarded once the declaration wins (S7a-3 pkt. 1): the
warning a run prints names both, and a caller reading an artefact months later needs to know
which directory the corpus was read from. A resolver that kept only the winner could report
that something was overridden without being able to say what.
"""
id: str
origin: BundleIdOrigin
mount: str
def _declared_bundle_id(frontmatter: dict[str, str]) -> str:
@ -901,18 +920,6 @@ def _declared_bundle_id(frontmatter: dict[str, str]) -> str:
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:
@ -930,16 +937,26 @@ def reconcile_bundle_id(
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**:
A declared id that DISAGREES with the mount is NOT an error (operator decision, S7a-3 pkt. 1):
the declaration wins, the mount is carried alongside on ``ResolvedBundleId.mount``, and the
caller that opened the base is the one that reports it. What still refuses is a base whose
CONCEPTS disagree with each other see ``assert_declared_ids_agree``, which is a separate,
unconditionally called check rather than a branch here, so a door that forgot it fails a test
of its own instead of quietly skipping a refusal.
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.
resolves ``mount-derived`` today and both declared branches are DEFENSIVE the first corpus in
the wild to take a declared branch was K2 (measured 2026-09-03, 619 declaring files).
**Honesty limit, stated:** without ``concept_name`` the root ``index.md`` answers, so a base
whose index declares X while its concepts declare Y resolves to X. That is B1's resolution
ORDER, unchanged here; the agreement check below is about concepts colliding with each other.
:raises BundleIdMismatch: a declared id the mount does not carry.
:raises ValueError: the root ``index.md`` is missing or unreadable.
"""
root = str(bundle_dir)
@ -950,14 +967,52 @@ def reconcile_bundle_id(
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)
return ResolvedBundleId(id=declared, origin="declared-concept", mount=mount)
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")
return ResolvedBundleId(id=declared, origin="declared-index", mount=mount)
return ResolvedBundleId(id=mount, origin="mount-derived", mount=mount)
def assert_declared_ids_agree(bundle: Bundle) -> None:
"""Refuse a navigated base whose CONCEPTS declare two different ``bundle_id`` values.
This is the collision the slacken (S7a-3 pkt. 1) deliberately keeps: a declared id that
disagrees with the MOUNT is a filesystem accident and is now recorded rather than refused, but
two concepts inside one base naming two different corpora is a base that cannot say what it is.
No fallback settles it, and every artefact stamped from it would name one of the two at random.
**Concepts only the root ``index.md`` is NOT in the set, and that is decision B1 applied a
second time.** Concept-beats-index is a PRECEDENCE rule, so an index out of step with its
concepts is the fallback losing, not a collision. Folding the index in would newly refuse
exactly the K2-shaped bases the slacken exists to admit.
Reads ``context_files``, which is also what drops the ``type: verdict`` layer and nested
``index.md`` at every level the same property ``read_bundle`` is built from, so the two
cannot disagree about which files count as concepts.
A SEPARATE function rather than a branch inside ``reconcile_bundle_id``: it needs a navigated
bundle, and ``reconcile_bundle_id`` must stay pure (``explore._bundle_index`` resolves ids for
directories that may not exist). Being separate also gives it its own mutation detach the call
at a door and that door's own arm goes red, instead of a refusal quietly not happening.
:raises BundleIdMismatch: two concepts declare different ids.
"""
declared: dict[str, str] = {}
for f in bundle.context_files:
value = _declared_bundle_id(f.frontmatter)
if value:
declared.setdefault(value, f.name)
if len(declared) > 1:
named = ", ".join(f"{value!r} (in {name!r})" for value, name in sorted(declared.items()))
raise BundleIdMismatch(
f"knowledge base {bundle.dir!r} cannot say what it is: its concepts declare "
f"{len(declared)} different bundle_id values — {named}; an approach names a base by "
"that id, so a run would evaluate against one corpus and report the other"
)
def bundle_context(bundle: Bundle, *, dimension: str | None = None) -> str: