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

@ -749,15 +749,32 @@ def load_parked(payload: Mapping[str, Any]) -> ParkedExploration:
def _bundle_index(bundle_dirs: Sequence[str]) -> dict[str, str]:
"""Map each knowledge base's id to its directory. The id is the directory's BASENAME.
"""Map each knowledge base's id to its directory — the DECLARED id where the base declares one,
the directory's basename otherwise (``okf.reconcile_bundle_id``'s rule, S7a-3 pkt. 1).
A duplicate basename is REFUSED rather than resolved by order: the id is what the manager
**Why this door resolves the declaration at all a CONSEQUENCE of the slacken, not scope
creep.** This used to be ``Path(raw).name`` while ``run_mandate_across_bundles`` used
``reconcile_bundle_id(raw).id``. While a declared id that disagreed with its mount was refused
outright the two could not differ. With declared-wins they can, and then ``explore()`` mints
approaches naming the MOUNT while the dispatcher routes by the DECLARATION an exploration
whose own mandate is unroutable.
**Unreadable falls back to the basename rather than raising, and that is measured.**
``test_explore_loadbearing.py`` configures ``/tmp/base-a`` and ``/tmp/one/shared-name``
directories that do not exist and expects an ExplorationError about IDs, not an I/O error.
Nothing is widened by the fallback: this index answers "which ids may be NAMED", and a base
nobody can read is refused a moment later by whichever door actually opens it.
A duplicate id is REFUSED rather than resolved by order: the id is what the manager
names a base by, and two bases answering to one name would let it read A while believing it
read B the S3.2 key-collision class, one layer up.
"""
index: dict[str, str] = {}
for raw in bundle_dirs:
bundle_id = Path(raw).name
try:
bundle_id = okf.reconcile_bundle_id(raw).id
except ValueError:
bundle_id = Path(raw).name
if bundle_id in index:
raise ExplorationError(
f"two knowledge bases share the id {bundle_id!r} ({index[bundle_id]!r} and "
@ -883,11 +900,12 @@ def navigator_tools(bundle_dirs: Sequence[str]) -> list[FunctionTool]:
)
def read_bundle(bundle_id: str) -> list[dict[str, Any]]:
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)
bundle = okf.navigate_bundle(bundle_dir)
# The base is OPENED here, so this is where it must be able to say what it IS (S7a-3
# pkt. 1). A declared id that disagrees with the MOUNT is no longer refused — that is a
# filesystem accident, recorded by the run rather than blocked here — but two concepts
# declaring two different corpora is a base no fallback can settle.
okf.assert_declared_ids_agree(bundle)
# ``context_files``, NEVER ``files``: it is the property that drops the ``type: verdict``
# layer AND nested ``index.md`` at every level. Building from ``files`` would put prior
# verdicts in front of the navigator around the gated ExpeL fold (målbilde §4), and the

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:

View file

@ -18,6 +18,7 @@ from typing import Literal
from agent_framework import Annotation, TextSpanRegion
from pydantic import BaseModel, Field
from portfolio_optimiser.okf import ResolvedBundleId
from portfolio_optimiser.retrieval import TextSpan
@ -71,6 +72,20 @@ class ProvenanceStamp(BaseModel):
#: forgot claim an anchoring that never happened, and ``False`` would under-claim a real one.
#: A binary fact about a falsifier has no honest default.
cost_baseline_anchored: bool
#: WHICH knowledge base this proposal was reasoned from, and how its identity was established —
#: ``okf.ResolvedBundleId`` (declared id / origin / the mount it was read from), or ``None`` on
#: the road path, where no knowledge base was involved at all.
#:
#: REQUIRED WITHOUT DEFAULT, for ``cost_baseline_anchored``'s reason: ``None`` is a VALUE here
#: (a run with no base), so letting an omitted field mean it would make "no base" and "nobody
#: filled this in" the same answer. A stamp that cannot say which corpus it judged cannot be
#: joined back to one, and the artefact leaves the process.
#:
#: Not on ``RunResult`` beside ``skipped_links``, and the distinction is deliberate:
#: ``skipped_links`` is diagnostic about the NAVIGATION, while this is the identity of the
#: artefact that was judged — the same class as ``cost_baseline_anchored``, which also resolves
#: once per run and is stamped per proposal.
bundle_id_source: ResolvedBundleId | None
#: External service calls the run made (B4). EMPTY is a positive statement — "nothing outside
#: this process was contacted" — not an absent field, which is why it is always serialized.
external_calls: list[ExternalCall] = Field(default_factory=list)

View file

@ -224,6 +224,12 @@ class DryRunReport:
#: because a dry run stops before any proposal exists, so there is no stamp to read it off —
#: and this surface is precisely where the un-anchored case was measured to be silent.
cost_baseline_anchored: bool
#: Which knowledge base a real run would judge, and how its identity was established
#: (``ProvenanceStamp.bundle_id_source``). Carried here for the same reason
#: ``cost_baseline_anchored`` is: a dry run stops before any stamp exists, and this surface is
#: precisely where a mount that disagrees with the declaration would otherwise be silent.
#: ``None`` on the road path.
bundle_id_source: okf.ResolvedBundleId | None
#: Every cross-link the bundle navigation could not follow (``okf.Bundle.skipped``). EMPTY is a
#: positive statement — "every cross-link was followed" — which is why it DEFAULTS, unlike
#: ``cost_baseline_anchored`` above: a missing bool would have to claim something about an event
@ -535,6 +541,34 @@ def cost_baseline_notice(anchored: bool) -> str | None:
return None if anchored else _UNANCHORED_NOTICE
def bundle_id_notice(resolved: okf.ResolvedBundleId | None) -> str | None:
"""Render the one line that says a base was mounted under a name it does not answer to, or
``None`` when there is nothing to say.
The warning half of the S7a-3 slacken. A declared id that disagrees with its directory is no
longer refused it is a filesystem accident, and refusing it made the first delivered corpus
that declares its own id unopenable but it must not become SILENT either: every artefact the
run stamps names the declared id, while the operator typed the mount.
ONE renderer with N callsites, never N copies of the wording (-(p)), and it takes the
ALREADY-RESOLVED value rather than a bundle path: a renderer that re-read the base would be a
second resolution of the same rule, free to drift from the run it describes
(``cost_baseline_notice``'s rule, and the reason ``ResolvedBundleId`` carries the mount).
``None`` on agreement AND on ``None`` omission, never an empty row (``mandate.announce``'s
rule). A run with no knowledge base has no identity to disagree about.
Both names are printed. A warning that says only "mismatch" leaves the operator to go and look
for the two values it is warning about."""
if resolved is None or resolved.id == resolved.mount:
return None
return (
f" Knowledge base: declares bundle_id {resolved.id!r} (source: {resolved.origin}) but is "
f"mounted as {resolved.mount!r} — the DECLARED id is the identity, so every artefact this "
f"run stamps names {resolved.id!r}"
)
def collision_notice(collisions: tuple[VerdictCollision, ...]) -> str | None:
"""Render which candidates two bases both described, or ``None`` when none did.
@ -690,11 +724,13 @@ async def run_project(
# 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).
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)
# ONE bundle-id rule (Step 10, slackened S7a-3 pkt. 1): the DECLARED id is the identity and
# the mount is carried alongside, so a base delivered under a directory name of its own is
# opened rather than refused. What is still refused, before a single model call: a base
# whose concepts declare two different corpora.
resolved_bundle_id: okf.ResolvedBundleId | None = okf.reconcile_bundle_id(bundle_dir)
okf.assert_declared_ids_agree(bundle)
project = _project_from_bundle(bundle_dir, project_id, bundle=bundle)
# The THIRD projection into ``CostBaseline`` (MAJOR-4), behind an EXPLICIT commission and
# never silent. The refusal PROPAGATES rather than degrading to the file loader: a caller
@ -722,6 +758,8 @@ async def run_project(
citations = [chunk_dict_to_citation(c) for c in chunks]
context = "\n".join(c["snippet"] for c in chunks)
skipped_links = ()
# No knowledge base, so no bundle identity — said by ABSENCE rather than by minting one.
resolved_bundle_id = None
debate_tools = [make_retrieval_tool(docs_dir, top_k=top_k)]
# Trekk B2 (krav 3): configured MCP servers become tools the AGENTS can call during the debate.
@ -783,6 +821,7 @@ async def run_project(
max_tokens=max_tokens,
top_k=top_k,
cost_baseline_anchored=baseline is not None,
bundle_id_source=resolved_bundle_id,
skipped_links=skipped_links,
)
# The MCP lifecycle (Trekk B2): entered HERE, after the dry-run cut above, so a dry run never
@ -914,6 +953,10 @@ async def run_project(
# anchoring than the one that ran. The road path is anchored by construction (the reference
# project's own cost_items ARE the baseline); a bundle is anchored iff it ships the file.
cost_baseline_anchored=baseline is not None,
# WHICH corpus was judged, and whether the base named itself or the mount named it for it.
# Read off the SAME resolution the run opened the base with (kø-(p)); ``None`` on the road
# path, where no knowledge base exists to name.
bundle_id_source=resolved_bundle_id,
# B4: which external service the debate actually called. Read AFTER the debate, so it is a
# record rather than an intention. The honesty limit lives on ``ExternalCall`` itself: this
# is the call and its source, not a verified rendering of the service's answer.
@ -2842,6 +2885,12 @@ def main(argv: list[str] | None = None) -> int:
nav_notice = skipped_links_notice(report.skipped_links)
if nav_notice is not None:
print(nav_notice)
# The third silence on this surface (S7a-3 pkt. 1): a base delivered under a directory name
# of its own now OPENS, so the disagreement has to be said out loud or nothing ever reports
# that the run's artefacts name something other than the path the operator typed.
id_notice = bundle_id_notice(report.bundle_id_source)
if id_notice is not None:
print(id_notice)
return 0
try:
@ -2890,6 +2939,11 @@ def main(argv: list[str] | None = None) -> int:
nav_notice = skipped_links_notice(result.skipped_links)
if nav_notice is not None:
print(nav_notice)
# Same renderer on the full run, read off the run's OWN stamp — so stdout and the outbox
# artefact cannot disagree about which corpus was judged.
id_notice = bundle_id_notice(result.provenance.bundle_id_source)
if id_notice is not None:
print(id_notice)
# The settlement against the commission (Trekk A4). Empty without a mandate, so an
# un-commissioned run prints exactly what it printed before.
settlement = settle(result.coverage)