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

@ -1361,7 +1361,9 @@ Python ≥3.10. MAF (`agent-framework-core` 1.16.0, `-orchestrations` 1.1.1 —
stempel den ikke kan revidere (D-i → 3 røde). **MÅLT: null `^bundle_id`-treff** noe sted under stempel den ikke kan revidere (D-i → 3 røde). **MÅLT: null `^bundle_id`-treff** noe sted under
`shared/`, `src/`, `tests/` mot en kjent-positiv kontroll på 31 filer med `^type:`, så begge `shared/`, `src/`, `tests/` mot en kjent-positiv kontroll på 31 filer med `^type:`, så begge
erklærte grener er DEFENSIVE (`budget_stop`-presedensen) og drives fra CRAFTED baser; alle 27 erklærte grener er DEFENSIVE (`budget_stop`-presedensen) og drives fra CRAFTED baser; alle 27
`bundle_dirs=`-kallsteder står grønne. **`explore._bundle_index` forblir REN** — to av dens egne `bundle_dirs=`-kallsteder står grønne. **⚠️ TO SETNINGER I DENNE RADEN ER ENDRET 03.09 — se
slakke-raden nederst (S7a-3 pkt. 1):** mount-avviket NEKTER ikke lenger (erklært vinner), og
«begge erklærte grener er DEFENSIVE» gjelder kun dette repoet — K2 erklærer nøkkelen i 619 filer. **`explore._bundle_index` forblir REN** — to av dens egne
armer konfigurerer kataloger som ikke finnes og forventer en id-feil, ikke en I/O-feil — så armer konfigurerer kataloger som ikke finnes og forventer en id-feil, ikke en I/O-feil — så
avstemmingen skjer der en base faktisk ÅPNES: `explore.read_bundle` (M11a → 1 rød), avstemmingen skjer der en base faktisk ÅPNES: `explore.read_bundle` (M11a → 1 rød),
`run_project`s bundle-arm (M11b → 2 røde) og dispatcheren (M11c → 1 rød, som erstattet en privat `run_project`s bundle-arm (M11b → 2 røde) og dispatcheren (M11c → 1 rød, som erstattet en privat
@ -1481,6 +1483,52 @@ Python ≥3.10. MAF (`agent-framework-core` 1.16.0, `-orchestrations` 1.1.1 —
4e-halvdelene står uendret); og ingen LEVENDE K2-fil er lest — fixturen er syntetisk, og at 4e-halvdelene står uendret); og ingen LEVENDE K2-fil er lest — fixturen er syntetisk, og at
produsenten faktisk emitterer denne formen for K2s Prisskjema er MÅLT på pandoc-stien, ikke på K2. produsenten faktisk emitterer denne formen for K2s Prisskjema er MÅLT på pandoc-stien, ikke på K2.
- **Den ERKLÆRTE `bundle_id`-en er identiteten; monteringsnavnet er en filsystem-tilfeldighet
(S7a-3 pkt. 1, økt 81):** til 03.09 NEKTET `reconcile_bundle_id` en base som erklærte en id
katalogen ikke bar. **Målt mot den første leverte basen som erklærer sin egen id** (K2: 618 av
630 konseptfiler + rot-`index.md`, alle `k2-trinn1-20260903`, levert som `K2-bundle-20260903`)
var følgen at basen **ikke kunne åpnes slik den var levert** — botemiddelet var å montere den på
nytt for hånd, én gang per leveranse, for alltid. Det er en konsument som avviser en produsents
legitime output over et katalognavn. **PM-beslutning: konsumenten slakker.** Erklært vinner
(konsept → rot-index → mount, B1s rekkefølge URØRT), avviket REGISTRERES —
`ResolvedBundleId.mount` bærer den overkjørte katalogen, `ProvenanceStamp.bundle_id_source` og
`DryRunReport.bundle_id_source` bærer hele oppløsningen inn i artefaktene, og `bundle_id_notice`
er ENESTE renderer (returnerer `None` ved enighet — omisjon, aldri tom rad; omisjonen er selv
gatet, M5 → 2 røde inkludert et UAVHENGIG eksisterende vitne i
`test_navigation_visibility_loadbearing`). **Stempel-feltet er PÅKREVD uten default** av
`cost_baseline_anchored`s grunn, og her skjerpet: `None` er en VERDI (veg-stien har ingen base),
så et utelatt felt måtte bety det samme som «ingen base» — nøyaktig stillheten kravet lukker.
**Det som FORTSATT nekter er den ekte kollisjonen:** to KONSEPTER i ÉN base som erklærer ULIKE
id-er (`assert_declared_ids_agree`, kalt ved hver dør som ÅPNER en base). **Rot-`index.md` er
IKKE med i enighets-settet** — B1 anvendt en gang til: konsept-slår-index er en PRESEDENS-regel,
så en index i utakt med sine konsepter er fallbacken som taper, ikke to konsepter som kolliderer;
å folde indeksen inn ville nektet nøyaktig de K2-formede basene slakken finnes for (M3 → 1 rød).
Sjekken er en EGEN funksjon, ikke en gren i `reconcile_bundle_id`: den trenger en navigert
bundle, resolveren må forbli REN (`_bundle_index` løser id-er for kataloger som kanskje ikke
finnes), og separat gir den sin egen mutasjon per dør (M1/M2 → 1 rød hver).
**`explore._bundle_index` løser nå den erklærte id-en, og det er en KONSEKVENS — ikke
scope-krype:** den brukte `Path(raw).name` mens dispatcheren brukte `reconcile_bundle_id(raw).id`;
så lenge avviket ble nektet KUNNE de ikke divergere, men med erklært-vinner mynter `explore()`
approaches som navngir MOUNTET mens dispatcheren ruter på ERKLÆRINGEN — en utforskning hvis eget
mandat er uruterbart (M9 → 2 røde, M11 → 1 rød). Uleselig base faller tilbake til basenavnet
i stedet for å reise, fordi to av `_bundle_index`s egne armer konfigurerer kataloger som ikke
finnes og forventer en ID-feil, ikke en I/O-feil — ingenting utvides: den indeksen svarer på
hvilke id-er som kan NAVNGIS, og en base ingen kan lese nektes et øyeblikk senere av den døra som
faktisk åpner den. Load-bearing MÅLT (`tests/test_bundle_id_slack_loadbearing.py`, 13 armer),
**ti mutasjoner alle røde mot HELE suiten** + grønn kontroll **1243/5** og golden
`demo-transcript.stdout` BYTE-UENDRET (`shasum -a 1` =
`ea8c534773acdbe41ae68f2c55724d69aaf8be4f`): M1 detach gaten i `run_project` (1) · M2 detach den i
`read_bundle` (1) · M3 fold indeksen inn i enighets-settet (1) · M4 gjeninnfør mount-nekten (9) ·
M5 rendereren skriver alltid linja (2) · M6 detach CLI-utskriften (1) · M7 konstant stempel-wiring
(1) · M8 konstant dry-run-wiring (2) · M9 `_bundle_index` tilbake til mountet (2) · M11
dispatcheren tilbake til mountet (1). **TRE ARMER I `test_bundle_id_reconciliation_loadbearing`
ble SKREVET OM, ikke slettet** — (e)/(f)/(j) pinnet nekten beslutningen fjernet; (j) ble SKARPERE
enn den den erstattet (erklært id ruter, mountet nektes — to implementasjoner som skiller seg i
BEGGE halvdeler, ikke i en exit-kode). **Ærlighets-grenser, uttalt:** uten `concept_name` svarer
rot-`index.md`, så en base hvis index erklærer X mens konseptene erklærer Y løser til X (B1s
rekkefølge, urørt); og dispatcherens «to baser deler id»-nekt er NYLIG NÅBAR — den krevde før to
monteringer med samme basenavn, nå kolliderer to ULIKT navngitte kataloger som erklærer samme id.
Kontrakt: `docs/okf-konsum-kontrakter.md § 3.1`.
- **STATE.md er local-only** (gitignored). Voyage session-state er efemert; STATE.md er kanonisk kontinuitet. - **STATE.md er local-only** (gitignored). Voyage session-state er efemert; STATE.md er kanonisk kontinuitet.
- Prosess: Voyage-plugin (`/trekbrief → /trekplan → /trekexecute → /trekreview`) per større fase. - Prosess: Voyage-plugin (`/trekbrief → /trekplan → /trekexecute → /trekreview`) per større fase.

View file

@ -97,7 +97,8 @@ propagates unchanged; reading an unreadable base as *"no declaration"* is the to
that widens an answer instead of refusing it. Only a base whose index *is* readable and declares that widens an answer instead of refusing it. Only a base whose index *is* readable and declares
nothing falls through to the next row. nothing falls through to the next row.
**Measured today: every base resolves `mount-derived`.** No shipped `index.md` declares the key: **Measured today: every base *in this repo* resolves `mount-derived`.** No shipped `index.md`
declares the key:
```sh ```sh
find shared -name index.md | wc -l # -> 7 find shared -name index.md | wc -l # -> 7
@ -107,6 +108,42 @@ grep -l 'bundle_id' $(find shared -name index.md) | wc -l # -> 0
So this contract changes which sources are **consulted**, not what happens when none of them So this contract changes which sources are **consulted**, not what happens when none of them
declares anything — which is why adopting it moves no existing call site. declares anything — which is why adopting it moves no existing call site.
### § 3.1. A declared id BEATS the mount; the mount is a filesystem accident
**Amended 2026-09-03 (S7a-3 pkt. 1), and the amendment is a consumer slacken.** Until then, a base
that declared an id its mount did not carry was **refused** (`BundleIdMismatch`) at every door. The
first delivered corpus that declares its own id — K2, measured 2026-09-03: 618 of 630 concept files
plus the root `index.md`, all saying `k2-trinn1-20260903`, delivered in a directory named
`K2-bundle-20260903` — could therefore **not be opened as delivered**. The only remedy was to
re-mount it under the declared name by hand, once per delivery, forever.
That is a consumer refusing a producer's legitimate output over a directory name. The rule is now:
- **The declared id is the identity.** The mount's basename answers only when nothing is declared.
- **A disagreement is RECORDED, never refused:** `ResolvedBundleId.mount` carries the overridden
directory, `ProvenanceStamp.bundle_id_source` / `DryRunReport.bundle_id_source` carry the whole
resolution into the artefacts, and the run prints **one warning line** naming both values.
- **A base is addressed by its declared id at BOTH doors.** The exploration catalogue
(`list_bundles` / `read_bundle` / `read_file` / `quick_validate`) and the mandate dispatcher
resolve the same id. This is a consequence of the slacken rather than a separate decision: while
a disagreeing declaration was refused the two could not differ, and if they differed now, an
exploration's own minted mandate would be unroutable.
**What still refuses, because no mount name can settle it:** two **concepts inside one base**
declaring **different** ids. Such a base cannot say what it is, and an artefact stamped from it
would name one of two corpora at random. The check runs at every door that OPENS a base.
**The root `index.md` is NOT part of that agreement check** — 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 this amendment exists to admit.
**Honesty limits, stated.** (1) Without a named concept 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. (2) Two *differently named* directories that both declare the same id now collide in the
dispatcher's "two knowledge bases share the id" refusal; that branch used to require two mounts with
the same basename and is no longer defensive.
**There is no cross-bundle link form.** A consumer holding several bundles partitions per bundle; **There is no cross-bundle link form.** A consumer holding several bundles partitions per bundle;
the producer emits no syntax for pointing from inside one bundle into another, and adding one is out the producer emits no syntax for pointing from inside one bundle into another, and adding one is out
of scope until a consumer states the need. of scope until a consumer states the need.

View file

@ -749,15 +749,32 @@ def load_parked(payload: Mapping[str, Any]) -> ParkedExploration:
def _bundle_index(bundle_dirs: Sequence[str]) -> dict[str, str]: 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 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. read B the S3.2 key-collision class, one layer up.
""" """
index: dict[str, str] = {} index: dict[str, str] = {}
for raw in bundle_dirs: 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: if bundle_id in index:
raise ExplorationError( raise ExplorationError(
f"two knowledge bases share the id {bundle_id!r} ({index[bundle_id]!r} and " 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]]: def read_bundle(bundle_id: str) -> list[dict[str, Any]]:
bundle_dir = _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)
bundle = okf.navigate_bundle(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`` # ``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 # 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 # 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): 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 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 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) @dataclass(frozen=True)
class ResolvedBundleId: 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`` ``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 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 resolver that never read the base claim it had; defaulting to a declared value would claim a
declaration that never happened. 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 id: str
origin: BundleIdOrigin origin: BundleIdOrigin
mount: str
def _declared_bundle_id(frontmatter: dict[str, str]) -> 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() 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( def reconcile_bundle_id(
bundle_dir: str | Path, *, concept_name: str | None = None bundle_dir: str | Path, *, concept_name: str | None = None
) -> ResolvedBundleId: ) -> ResolvedBundleId:
@ -930,16 +937,26 @@ def reconcile_bundle_id(
2. the root ``index.md``'s frontmatter → ``"declared-index"``; 2. the root ``index.md``'s frontmatter → ``"declared-index"``;
3. the mount's basename → ``"mount-derived"``. 3. the mount's basename → ``"mount-derived"``.
A declared id that DISAGREES with the mount raises ``BundleIdMismatch`` at whichever source A declared id that DISAGREES with the mount is NOT an error (operator decision, S7a-3 pkt. 1):
declared it. A base whose root ``index.md`` cannot be read is **unknown, not undeclared**: 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 ``navigate_bundle``'s fail-fast propagates unchanged, because reading an unreadable base as
"it declares nothing" would widen the answer on missing evidence. "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 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 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. :raises ValueError: the root ``index.md`` is missing or unreadable.
""" """
root = str(bundle_dir) 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}") raise ValueError(f"OKF bundle has no readable concept {concept_name!r}: {root!r}")
declared = _declared_bundle_id(concept.frontmatter) declared = _declared_bundle_id(concept.frontmatter)
if declared: if declared:
return _reconciled(declared, mount, "declared-concept", root) return ResolvedBundleId(id=declared, origin="declared-concept", mount=mount)
index = _load_file(root, _INDEX_NAME) index = _load_file(root, _INDEX_NAME)
if index is None: if index is None:
raise ValueError(f"OKF bundle has no readable {_INDEX_NAME}: {root!r}") raise ValueError(f"OKF bundle has no readable {_INDEX_NAME}: {root!r}")
declared = _declared_bundle_id(index.frontmatter) declared = _declared_bundle_id(index.frontmatter)
if declared: if declared:
return _reconciled(declared, mount, "declared-index", root) return ResolvedBundleId(id=declared, origin="declared-index", mount=mount)
return ResolvedBundleId(id=mount, origin="mount-derived") 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: 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 agent_framework import Annotation, TextSpanRegion
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from portfolio_optimiser.okf import ResolvedBundleId
from portfolio_optimiser.retrieval import TextSpan 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. #: 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. #: A binary fact about a falsifier has no honest default.
cost_baseline_anchored: bool 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 #: 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. #: this process was contacted" — not an absent field, which is why it is always serialized.
external_calls: list[ExternalCall] = Field(default_factory=list) 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 — #: 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. #: and this surface is precisely where the un-anchored case was measured to be silent.
cost_baseline_anchored: bool 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 #: 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 #: 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 #: ``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 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: def collision_notice(collisions: tuple[VerdictCollision, ...]) -> str | None:
"""Render which candidates two bases both described, or ``None`` when none did. """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 = # 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)
# 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) project = _project_from_bundle(bundle_dir, project_id, bundle=bundle)
# The THIRD projection into ``CostBaseline`` (MAJOR-4), behind an EXPLICIT commission and # 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 # 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] citations = [chunk_dict_to_citation(c) for c in chunks]
context = "\n".join(c["snippet"] for c in chunks) context = "\n".join(c["snippet"] for c in chunks)
skipped_links = () 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)] 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. # 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, max_tokens=max_tokens,
top_k=top_k, top_k=top_k,
cost_baseline_anchored=baseline is not None, cost_baseline_anchored=baseline is not None,
bundle_id_source=resolved_bundle_id,
skipped_links=skipped_links, skipped_links=skipped_links,
) )
# The MCP lifecycle (Trekk B2): entered HERE, after the dry-run cut above, so a dry run never # 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 # 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. # project's own cost_items ARE the baseline); a bundle is anchored iff it ships the file.
cost_baseline_anchored=baseline is not None, 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 # 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 # 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. # 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) nav_notice = skipped_links_notice(report.skipped_links)
if nav_notice is not None: if nav_notice is not None:
print(nav_notice) 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 return 0
try: try:
@ -2890,6 +2939,11 @@ def main(argv: list[str] | None = None) -> int:
nav_notice = skipped_links_notice(result.skipped_links) nav_notice = skipped_links_notice(result.skipped_links)
if nav_notice is not None: if nav_notice is not None:
print(nav_notice) 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 # The settlement against the commission (Trekk A4). Empty without a mandate, so an
# un-commissioned run prints exactly what it printed before. # un-commissioned run prints exactly what it printed before.
settlement = settle(result.coverage) settlement = settle(result.coverage)

View file

@ -287,6 +287,7 @@ def _unanchored_run() -> RunResult:
validator_decision="rejected", validator_decision="rejected",
token_usage=1, token_usage=1,
cost_baseline_anchored=False, cost_baseline_anchored=False,
bundle_id_source=None,
), ),
verdict=Verdict( verdict=Verdict(
id="v1", id="v1",

View file

@ -35,12 +35,25 @@ bundle arm, and the dispatcher.
propagates unchanged; reading an unreadable base as "it declares nothing" is the tolerant-read- 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``. 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 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 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 * 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 * (e) a declared id BEATS the mount it disagrees with * (f) a base that cannot say what it is refused
(g) an unreadable index refuses instead of falling back * (h)-(j) the same helper is reached from through the CLI with ZERO model calls * (g) an unreadable index refuses instead of falling back *
all three doors that open a base. (h)-(j) the same helper is reached from all three doors that open a base.
""" """
from __future__ import annotations from __future__ import annotations
@ -52,7 +65,7 @@ from typing import Any
import pytest import pytest
from portfolio_optimiser import explore, okf, run 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.simulation import ScriptedChatClient
from portfolio_optimiser.verdicts import VerdictCollision from portfolio_optimiser.verdicts import VerdictCollision
@ -61,6 +74,11 @@ _BYGG = _EXAMPLES / "bygg-energi-mikro"
_PID = "BYGG-KONTOR-NORD" _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: 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 """A throwaway copy of a shipped base. Mutations of fixture content NEVER touch the git-tracked
tree (the repo's ``shutil.copytree`` discipline).""" 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 ------------------------------------------- # --- (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: def test_a_base_that_disagrees_with_its_mount_wins_over_it(tmp_path: Path) -> None:
"""(e) The refusal itself. ``BundleIdMismatch`` subclasses ``ValueError`` deliberately, so it """(e) REWRITTEN 2026-09-03 (S7a-3 pkt. 1), not deleted. This arm used to pin the opposite
lands on the CLI's refusal tuple and hosting's 400 arm rather than the crash channel -- behaviour: a declared id the mount did not carry was refused by name. The operator decision
``ExplorationError`` is a ``RuntimeError`` and would give a traceback and a 500.""" 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) base = _base_copy(tmp_path)
_declare(base / "index.md", "a-name-the-mount-does-not-carry") _declare(base / "index.md", "a-name-the-mount-does-not-carry")
with pytest.raises(okf.BundleIdMismatch) as excinfo: resolved = okf.reconcile_bundle_id(str(base))
okf.reconcile_bundle_id(str(base))
message = str(excinfo.value) assert resolved.id == "a-name-the-mount-does-not-carry"
assert "a-name-the-mount-does-not-carry" in message and base.name in message assert resolved.origin == "declared-index"
assert issubclass(okf.BundleIdMismatch, ValueError) 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 tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
"""(f) The assert is on ZERO model calls, never on the exit code alone: a refusal that arrives """(f) REWRITTEN 2026-09-03 with the refusal it now guards: this used to drive a base whose
AFTER the spend looks identical at rc 1 (the M12 signature, and the hoist idiom declared id disagreed with its MOUNT, which no longer refuses. The EARLY half is what the arm
``test_explore_callsites_loadbearing.py`` already uses twice).""" 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_MODEL_MAP", raising=False)
monkeypatch.delenv("PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT", raising=False) monkeypatch.delenv("PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT", raising=False)
base = _base_copy(tmp_path) 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] = [] 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)]) rc = run.main([_PID, "--docs-dir", str(base), "--bundle-dir", str(base)])
assert rc == 1 assert rc == 1
assert sink == [], ( 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)" 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 @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 tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> 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 It used to pin that a base disagreeing with its mount was refused while assembling the routing
``reconcile_bundle_id(raw).id`` and ``Path(raw).name`` return the same string on every base table. That refusal is gone; what took its place is a discriminator the old arm could not have:
that resolves at all -- an assert on the routed id could not tell the two implementations on a base whose declaration disagrees with its directory, the DECLARED id must route and the
apart. What CAN: a disagreeing base must be refused while assembling the routing table, i.e. MOUNT must not. A dispatcher left on ``Path(raw).name`` answers exactly the other way round, so
BEFORE the first ``run_project`` is dispatched. Left as a private basename copy, routing the two implementations differ in both halves rather than in an exit code.
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. 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) base = _base_copy(tmp_path)
_declare(base / "index.md", "a-name-the-mount-does-not-carry") _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: async def counting_run_project(*args: Any, **kwargs: Any) -> Any:
started.append(str(kwargs.get("bundle_dir"))) 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) monkeypatch.setattr(run, "run_project", counting_run_project)
with pytest.raises(okf.BundleIdMismatch): def _mandate(named: str) -> Mandate:
await run.run_mandate_across_bundles( return Mandate(
Mandate( objective="o",
objective="o", approaches=(Approach(id="a", label="A", bundle_id=named),),
approaches=(Approach(id="a", label="A", bundle_id=base.name),), allow_own_proposals=False,
allow_own_proposals=False,
),
(str(base),),
) )
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"
# ================================================================================================== # ==================================================================================================

View file

@ -0,0 +1,369 @@
"""S7a-3 pkt. 1 - the DECLARED id is the identity; the mount is a filesystem accident.
**The measurement that forced it** (``docs/2026-09-03-syretest-s7a2-k2.md`` § 1). The first
delivered corpus that declares its own ``bundle_id`` -- K2, 618 of 630 concept files plus the root
index, all saying ``k2-trinn1-20260903`` -- arrived mounted as ``K2-bundle-20260903``. Step 10's
reconciliation refused it with ``BundleIdMismatch`` at EVERY door: the base could not be opened as
delivered, and the only remedy was to re-mount it under the declared name by hand, once per
delivery, forever. That is a consumer refusing a producer's legitimate output over a directory name.
**Operator decision (PM, 2026-09-03).** The consumer slackens. The declared id (concept frontmatter
first, root ``index.md`` as fallback -- B1's order, unchanged) IS the identity; the mount's basename
answers only when nothing is declared. A disagreement between declared and mount is RECORDED
(``ProvenanceStamp.bundle_id_source`` / ``DryRunReport.bundle_id_source`` + one warning line),
never refused.
**What still refuses, and it is the REAL collision.** Two CONCEPTS inside one base declaring
DIFFERENT ids is a base that cannot say what it is -- no mount name can settle that, and every
artefact stamped from it would name one of two corpora at random. ``okf.assert_declared_ids_agree``
raises there, and it is called at every door that OPENS a base.
**The root index NEVER participates in that check, and that is decision B1 applied twice.** Concept
beats index is a PRECEDENCE rule; an index that disagrees with its concepts is the fallback losing,
not two concepts colliding. Folding the index into the agreement set would newly refuse exactly the
K2-shaped bases this file exists to admit.
**One naming scheme, or the pipeline breaks -- a CONSEQUENCE, not scope creep.**
``explore._bundle_index`` keyed bases on ``Path(raw).name`` and ``run_mandate_across_bundles`` on
``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 DECLARED id -- so an exploration's
own mandate becomes unroutable. ``_bundle_index`` therefore resolves the declared id too. It stays
usable on a base that cannot be read (falling back to the basename) because two of its own arms in
``test_explore_loadbearing.py`` configure directories that do not exist and expect an id error, not
an I/O one -- and because a base nobody can read is refused a moment later by whichever door
actually opens it.
**Newly reachable, stated:** ``run_mandate_across_bundles``'s "two knowledge bases share the id"
refusal used to require two mounts with the same basename. Two differently-named mounts declaring
the same id now collide there -- same code, no longer a defensive branch (arm (k)).
**Honesty limit, measured and left alone.** ``reconcile_bundle_id`` without a named concept answers
from the root ``index.md``, so a base whose index declares X while its concepts declare Y resolves
to X. Changing that would change B1's resolution ORDER, which this order did not ask for; the
agreement check above is about concepts colliding with each other, not with their index.
Arms: (a) a K2-shaped base opens, declared wins * (b) the mount answers only in silence * (c) two
disagreeing concepts refused BY NAME * (d) the index is not in the collision set * (e) the run
stamps which source answered, and COMPLETES * (f) the dry run carries it too * (g) the renderer
fires on disagreement and is silent on agreement * (h) the CLI prints it * (i) explore addresses a
base by its DECLARED id * (j) ``read_bundle`` refuses an intra-base collision * (k) the dispatcher's
shared-id refusal is newly reachable * (l) the field has no default.
"""
from __future__ import annotations
import json
import shutil
from pathlib import Path
from typing import Any
import pytest
from conftest import SyntheticUsageChatClient
from portfolio_optimiser import explore, okf, run
from portfolio_optimiser.mandate import Approach, Mandate
from portfolio_optimiser.provenance import Citation, ProvenanceStamp
from portfolio_optimiser.retrieval import TextSpan
from portfolio_optimiser.run import MandateRoutingError, bundle_id_notice, run_project
_EXAMPLES = Path(__file__).resolve().parents[1] / "shared" / "examples"
_BYGG = _EXAMPLES / "bygg-energi-mikro"
_DATA = Path(__file__).resolve().parents[1] / "src" / "portfolio_optimiser" / "data" / "bundles"
_RUNNABLE = _DATA / "bygg-energi-mikro-a"
_RUNNABLE_PID = "BYGG-ENERGI-MIKRO-A"
#: The K2 shape, in one line: a base that calls itself one thing and sits in a directory called
#: another. Deliberately NOT a valid directory name pattern of its own -- the point is that the two
#: differ, not that either is prettier.
_DECLARED = "k2-shaped-corpus"
_REPLY = json.dumps(
{
"measure": "LED-retrofit",
"affected_items": [{"code": "ENERGI-TOTAL-EL", "quantity": 180000, "unit_cost": 1.0}],
"claimed_saving_nok": 30000,
}
)
_VERDICT_INPUT = {"decision": "approved", "rationale": "expert reviewed (sim)"}
@pytest.fixture(autouse=True)
def _isolate_model_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("PORTFOLIO_MODEL_MAP", raising=False)
monkeypatch.delenv("PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT", raising=False)
def _copy(src: Path, tmp_path: Path, name: str) -> Path:
dst = tmp_path / name
shutil.copytree(src, 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")
def _concepts(base: Path) -> list[Path]:
return [p for p in sorted(base.rglob("*.md")) if p.name != "index.md"]
def _factory(reply: str = _REPLY) -> Any:
def factory(role: str) -> Any:
return SyntheticUsageChatClient(default_reply=reply)
return factory
# --- (a)/(b) the slacken itself -------------------------------------------------------------------
def test_a_base_whose_declared_id_disagrees_with_its_mount_opens(tmp_path: Path) -> None:
"""(a) The headline. This exact shape raised ``BundleIdMismatch`` before today, at every door."""
base = _copy(_BYGG, tmp_path, "a-mount-name-nobody-declared")
_declare(base / "index.md", _DECLARED)
resolved = okf.reconcile_bundle_id(str(base))
assert resolved.id == _DECLARED, "the declared id is the identity; the mount is an accident"
assert resolved.origin == "declared-index"
assert resolved.mount == "a-mount-name-nobody-declared", (
"the mount must be CARRIED, not discarded: the warning line names both, and a resolver "
"that kept only the winner could not say what it overrode"
)
def test_the_mount_answers_only_when_nothing_is_declared() -> None:
"""(b) The control. Every base shipped in this repo takes this branch, so without it arm (a)
could pass against a resolver that had simply stopped consulting the mount at all."""
resolved = okf.reconcile_bundle_id(str(_BYGG))
assert (resolved.id, resolved.origin, resolved.mount) == (
_BYGG.name,
"mount-derived",
_BYGG.name,
)
# --- (c)/(d) what still refuses, and what deliberately does not -----------------------------------
def test_two_concepts_declaring_different_ids_are_refused_by_name(tmp_path: Path) -> None:
"""(c) The REAL collision, and the one no mount name can settle. Both ids must be named: an
operator holding a base that cannot say what it is needs to know which two answers it gave."""
base = _copy(_BYGG, tmp_path, "two-minds")
first, second = _concepts(base)[:2]
_declare(first, "corpus-alfa")
_declare(second, "corpus-beta")
with pytest.raises(okf.BundleIdMismatch) as excinfo:
okf.assert_declared_ids_agree(okf.navigate_bundle(str(base)))
message = str(excinfo.value)
assert "corpus-alfa" in message and "corpus-beta" in message
assert issubclass(okf.BundleIdMismatch, ValueError), (
"it must land on the CLI's refusal tuple and hosting's 400 arm, never the crash channel"
)
def test_concepts_agreeing_with_each_other_but_not_with_the_index_is_not_a_collision(
tmp_path: Path,
) -> None:
"""(d) B1's precedence, applied to the agreement set: an index that disagrees with its concepts
is the FALLBACK LOSING, not two concepts colliding. Fold the index into the set and every
K2-shaped base -- the whole point of the slacken -- newly refuses."""
base = _copy(_BYGG, tmp_path, "index-out-of-step")
_declare(base / "index.md", "what-the-index-thinks")
for concept in _concepts(base):
_declare(concept, "what-the-concepts-think")
okf.assert_declared_ids_agree(okf.navigate_bundle(str(base))) # must not raise
# The control that proves the fixture COULD have collided: the two values really are different,
# so a green pass above is the rule working rather than a fixture with nothing to disagree about.
assert okf.reconcile_bundle_id(str(base)).id == "what-the-index-thinks"
# --- (e)/(f) the run records which source answered ------------------------------------------------
async def test_the_run_stamps_which_source_answered_and_completes(
tmp_path: Path, fresh_store: Any
) -> None:
"""(e) End-to-end: the base that used to be refused now RUNS, and the stamp says the id was
declared rather than guessed from the mount. Both halves matter -- a run that refused would
fail on the first assert, and a run that stamped nothing on the second."""
base = _copy(_RUNNABLE, tmp_path, "mounted-under-another-name")
_declare(base / "index.md", _DECLARED)
result = await run_project(
_RUNNABLE_PID,
"local",
docs_dir=str(base),
bundle_dir=str(base),
verdict_input=_VERDICT_INPUT,
client_factory=_factory(),
store=fresh_store,
)
source = result.provenance.bundle_id_source
assert source is not None
assert (source.id, source.origin, source.mount) == (
_DECLARED,
"declared-index",
"mounted-under-another-name",
)
async def test_the_road_path_stamps_no_bundle_identity_at_all(
docs_dir: Any, fresh_store: Any
) -> None:
"""(e, control) A run with no knowledge base has no bundle identity, and says so by ABSENCE
rather than by inventing one. Without this arm a constant ``declared-index`` would pass above."""
result = await run_project(
"FV42-GSV-E1",
"local",
docs_dir=docs_dir,
verdict_input=_VERDICT_INPUT,
client_factory=_factory(
json.dumps(
{
"measure": "Reduce scope",
"affected_items": [{"code": "05.2", "quantity": 4300.0, "unit_cost": 215.0}],
"claimed_saving_nok": 200000.0,
}
)
),
store=fresh_store,
)
assert result.provenance.bundle_id_source is None
async def test_the_dry_run_carries_the_same_fact(tmp_path: Path) -> None:
"""(f) ``cost_baseline_anchored``'s precedent: a dry run stops before any stamp exists, so the
fact has to leave on the report or that surface goes silent again."""
base = _copy(_RUNNABLE, tmp_path, "dry-run-mount")
_declare(base / "index.md", _DECLARED)
report = await run_project(
_RUNNABLE_PID,
"local",
docs_dir=str(base),
bundle_dir=str(base),
client_factory=_factory(),
live_dry_run=True,
)
source = getattr(report, "bundle_id_source", None)
assert source is not None and source.id == _DECLARED and source.mount == "dry-run-mount"
# --- (g)/(h) the warning line ---------------------------------------------------------------------
def test_the_notice_fires_on_disagreement_and_is_silent_on_agreement() -> None:
"""(g) ONE renderer over the ALREADY-RESOLVED value, and ``None`` when there is nothing to warn
about -- omission, never an empty row (``cost_baseline_notice``'s rule). Both names must appear:
a warning that says only "mismatch" leaves the operator to go and look."""
disagreeing = okf.ResolvedBundleId(
id="declared-name", origin="declared-index", mount="mount-name"
)
agreeing = okf.ResolvedBundleId(id="same", origin="declared-index", mount="same")
line = bundle_id_notice(disagreeing)
assert line is not None and "declared-name" in line and "mount-name" in line
assert bundle_id_notice(agreeing) is None
assert bundle_id_notice(None) is None, "a run with no knowledge base has nothing to warn about"
def test_the_cli_warns_about_a_disagreeing_mount(tmp_path: Path, capsys: Any) -> None:
"""(h) The behavioural half: a renderer nothing calls is a line nobody reads. Driven through
``--live-dry-run`` so the arm costs one navigation and no model call."""
base = _copy(_RUNNABLE, tmp_path, "cli-mount-name")
_declare(base / "index.md", _DECLARED)
rc = run.main(
[_RUNNABLE_PID, "--docs-dir", str(base), "--bundle-dir", str(base), "--live-dry-run"]
)
out = capsys.readouterr().out
assert rc == 0, "a declared id that disagrees with the mount must no longer refuse"
assert _DECLARED in out and "cli-mount-name" in out
# --- (i)-(k) one naming scheme across the two doors -----------------------------------------------
def test_explore_addresses_a_base_by_its_declared_id(tmp_path: Path) -> None:
"""(i) The consequence that keeps ``explore()`` -> ``run_mandate_across_bundles`` connected: a
minted approach names the base by the id the catalogue offered, and the dispatcher routes by the
declared one. Two schemes here means an exploration's own mandate is unroutable."""
base = _copy(_BYGG, tmp_path, "explore-mount-name")
_declare(base / "index.md", _DECLARED)
tools = {t.name: t for t in explore.navigator_tools((str(base),))}
catalogue = tools["list_bundles"].func()
assert [entry["id"] for entry in catalogue] == [_DECLARED]
assert tools["read_bundle"].func(bundle_id=_DECLARED), "the declared id must OPEN the base"
with pytest.raises(explore.ExplorationError):
tools["read_bundle"].func(bundle_id="explore-mount-name")
def test_read_bundle_refuses_a_base_that_cannot_say_what_it_is(tmp_path: Path) -> None:
"""(j) The refusal reaches the door that OPENS a base, not just the helper. Detach the call and
an exploration reads a base whose concepts name two different corpora."""
base = _copy(_BYGG, tmp_path, "two-minds-explored")
first, second = _concepts(base)[:2]
_declare(first, "corpus-alfa")
_declare(second, "corpus-beta")
tools = {t.name: t for t in explore.navigator_tools((str(base),))}
with pytest.raises(okf.BundleIdMismatch):
tools["read_bundle"].func(bundle_id="two-minds-explored")
async def test_two_mounts_declaring_one_id_collide_in_the_dispatcher(tmp_path: Path) -> None:
"""(k) NEWLY REACHABLE, and said out loud: this refusal used to need two mounts with the same
basename. With declared-wins, two differently-named directories claiming one id collide -- the
same code, no longer a defensive branch."""
first = _copy(_BYGG, tmp_path, "mount-one")
second = _copy(_BYGG, tmp_path, "mount-two")
_declare(first / "index.md", _DECLARED)
_declare(second / "index.md", _DECLARED)
with pytest.raises(MandateRoutingError, match=_DECLARED):
await run.run_mandate_across_bundles(
Mandate(
objective="o",
approaches=(Approach(id="a", label="A", bundle_id=_DECLARED),),
allow_own_proposals=False,
),
(str(first), str(second)),
)
# --- (l) the field has no default -----------------------------------------------------------------
def test_the_stamp_field_has_no_default() -> None:
"""(l) ``cost_baseline_anchored``'s rule: a stamp that forgot to say which corpus it judged must
not construct at all. ``None`` is a VALUE here (the road path), which is precisely why the
absence of the field cannot be allowed to mean it."""
with pytest.raises(Exception):
ProvenanceStamp( # type: ignore[call-arg]
citations=[
Citation(file="f", snippet="s", locator=TextSpan(start_index=0, end_index=1))
],
model="m",
role="proposer",
validator_decision="validated",
token_usage=0,
cost_baseline_anchored=True,
)

View file

@ -36,6 +36,7 @@ _PROVENANCE = ProvenanceStamp(
# These fixtures stand in for an ordinary complete run; the road path is anchored by # These fixtures stand in for an ordinary complete run; the road path is anchored by
# construction, so ``True`` is the honest value here. The un-anchored case has its own file. # construction, so ``True`` is the honest value here. The un-anchored case has its own file.
cost_baseline_anchored=True, cost_baseline_anchored=True,
bundle_id_source=None,
) )

View file

@ -34,6 +34,7 @@ _PROVENANCE = ProvenanceStamp(
# These fixtures stand in for an ordinary complete run; the road path is anchored by # These fixtures stand in for an ordinary complete run; the road path is anchored by
# construction, so ``True`` is the honest value here. The un-anchored case has its own file. # construction, so ``True`` is the honest value here. The un-anchored case has its own file.
cost_baseline_anchored=True, cost_baseline_anchored=True,
bundle_id_source=None,
) )

View file

@ -101,6 +101,7 @@ _PROVENANCE = ProvenanceStamp(
# These fixtures stand in for an ordinary complete run; the road path is anchored by # These fixtures stand in for an ordinary complete run; the road path is anchored by
# construction, so ``True`` is the honest value here. The un-anchored case has its own file. # construction, so ``True`` is the honest value here. The un-anchored case has its own file.
cost_baseline_anchored=True, cost_baseline_anchored=True,
bundle_id_source=None,
) )
_VALIDATED = ValidatedProposal( _VALIDATED = ValidatedProposal(
proposal=_PROPOSAL, p10=100.0, p50=150.0, p90=200.0, nominal_feasible=180.0 proposal=_PROPOSAL, p10=100.0, p50=150.0, p90=200.0, nominal_feasible=180.0

View file

@ -65,6 +65,7 @@ _PROVENANCE = ProvenanceStamp(
# These fixtures stand in for an ordinary complete run; the road path is anchored by # These fixtures stand in for an ordinary complete run; the road path is anchored by
# construction, so ``True`` is the honest value here. The un-anchored case has its own file. # construction, so ``True`` is the honest value here. The un-anchored case has its own file.
cost_baseline_anchored=True, cost_baseline_anchored=True,
bundle_id_source=None,
) )
_VALIDATED = ValidatedProposal( _VALIDATED = ValidatedProposal(
proposal=_PROPOSAL, p10=100.0, p50=150.0, p90=200.0, nominal_feasible=180.0 proposal=_PROPOSAL, p10=100.0, p50=150.0, p90=200.0, nominal_feasible=180.0

View file

@ -20,6 +20,7 @@ def _stamp() -> ProvenanceStamp:
validator_decision="validated", validator_decision="validated",
token_usage=42, token_usage=42,
cost_baseline_anchored=True, cost_baseline_anchored=True,
bundle_id_source=None,
) )
@ -32,6 +33,7 @@ def test_zero_citations_is_rejected() -> None:
validator_decision="validated", validator_decision="validated",
token_usage=1, token_usage=1,
cost_baseline_anchored=True, cost_baseline_anchored=True,
bundle_id_source=None,
) )