feat(okf): den erklaerte bundle_id-en er identiteten - mountet er en filsystem-tilfeldighet
S7a-3 pkt. 1. Til i dag NEKTET reconcile_bundle_id en base som erklaerte en id katalogen ikke bar. Maalt mot K2 - den foerste leverte basen som erklaerer sin egen id (618 av 630 konseptfiler + rot-index, alle "k2-trinn1-20260903", levert som "K2-bundle-20260903") - betydde det at basen ikke kunne aapnes slik den var levert, og at eneste botemiddel var aa montere den paa nytt for haand, en gang per leveranse. PM-beslutning: konsumenten slakker. - Erklaert vinner (B1s rekkefoelge uroert), avviket REGISTRERES: ResolvedBundleId.mount + ProvenanceStamp.bundle_id_source + DryRunReport.bundle_id_source + run.bundle_id_notice (None ved enighet). Stempel-feltet er PAAKREVD uten default: None er en VERDI (veg-stien). - Det som fortsatt nekter er den EKTE kollisjonen: to KONSEPTER i en base som erklaerer ULIKE id-er (okf.assert_declared_ids_agree, kalt ved hver doer som aapner en base). Rot-index er IKKE med i enighets-settet - konsept-slaar-index er en presedens-regel, saa en index i utakt er fallbacken som taper. - KONSEKVENS, ikke scope-krype: explore._bundle_index loeser naa den erklaerte id-en. Den brukte Path(raw).name mens dispatcheren brukte reconcile...id; med erklaert-vinner ville explore() myntet approaches som navngir MOUNTET mens dispatcheren ruter paa ERKLAERINGEN - en utforskning med uruterbart mandat. Load-bearing MAALT: 10 mutasjoner alle roede mot HELE suiten, groenn kontroll 1243 passed / 5 skipped og golden demo-transcript.stdout byte-uendret (shasum -a 1 = ea8c534773acdbe41ae68f2c55724d69aaf8be4f). M1 1 / M2 1 / M3 1 / M4 9 / M5 2 / M6 1 / M7 1 / M8 2 / M9 2 / M11 1. Tre armer i test_bundle_id_reconciliation_loadbearing er SKREVET OM (ikke slettet) - de pinnet nekten beslutningen fjernet. (j) ble skarpere enn den den erstattet: erklaert id ruter, mountet nektes. Kontrakt: docs/okf-konsum-kontrakter.md § 3.1. Invariantrad i CLAUDE.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5adb1cca62
commit
baae7507a9
14 changed files with 708 additions and 73 deletions
50
CLAUDE.md
50
CLAUDE.md
|
|
@ -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
|
||||
`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
|
||||
`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å
|
||||
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
|
||||
|
|
@ -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
|
||||
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.
|
||||
- Prosess: Voyage-plugin (`/trekbrief → /trekplan → /trekexecute → /trekreview`) per større fase.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
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
|
||||
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;
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -749,14 +749,31 @@ 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:
|
||||
try:
|
||||
bundle_id = okf.reconcile_bundle_id(raw).id
|
||||
except ValueError:
|
||||
bundle_id = Path(raw).name
|
||||
if bundle_id in index:
|
||||
raise ExplorationError(
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 (kø-(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)
|
||||
|
|
|
|||
|
|
@ -287,6 +287,7 @@ def _unanchored_run() -> RunResult:
|
|||
validator_decision="rejected",
|
||||
token_usage=1,
|
||||
cost_baseline_anchored=False,
|
||||
bundle_id_source=None,
|
||||
),
|
||||
verdict=Verdict(
|
||||
id="v1",
|
||||
|
|
|
|||
|
|
@ -35,12 +35,25 @@ bundle arm, and the dispatcher.
|
|||
propagates unchanged; reading an unreadable base as "it declares nothing" is the tolerant-read-
|
||||
widens-the-answer defect research topic 2 measured in SPARQL's ``SILENT``.
|
||||
|
||||
**REWRITTEN IN PART, 2026-09-03 (S7a-3 pkt. 1).** Arms (e), (f) and (j) pinned a refusal an
|
||||
operator decision removed: a declared id that disagrees with its MOUNT is now the declaration
|
||||
winning, not an error -- measured against K2, the first delivered corpus that declares its own id,
|
||||
which this refusal made unopenable as delivered. They were rewritten rather than deleted, each onto
|
||||
what replaced it: (e) the declaration wins and the mount survives on the resolution; (f) the CLI's
|
||||
early refusal now guards the collision that IS still refused (two concepts, two corpora); (j) the
|
||||
dispatcher routes by the DECLARED id and refuses the mount -- a sharper discriminator than the one
|
||||
it replaced. The slacken's own gate is ``tests/test_bundle_id_slack_loadbearing.py``.
|
||||
|
||||
**The "DEFENSIVE branch" note above is now historical.** It was true when written and measured
|
||||
again on 2026-09-03: K2 declares the key in 619 files, so ``declared-index`` fires in the wild.
|
||||
Nothing in THIS repo declares it, which is why the arms below still craft their bases.
|
||||
|
||||
Arms: (a) the CONTROL, a shipped base resolving ``mount-derived`` * (b) a declared, agreeing root
|
||||
index * (c) a declaring CONCEPT beating a declaring index -- the ordering discriminator * (d) the
|
||||
three origins are pairwise distinct, read off the three arms above rather than off a literal *
|
||||
(e) disagreement refused BY NAME * (f) disagreement refused through the CLI with ZERO model calls *
|
||||
(g) an unreadable index refuses instead of falling back * (h)-(j) the same helper is reached from
|
||||
all three doors that open a base.
|
||||
(e) a declared id BEATS the mount it disagrees with * (f) a base that cannot say what it is refused
|
||||
through the CLI with ZERO model calls * (g) an unreadable index refuses instead of falling back *
|
||||
(h)-(j) the same helper is reached from all three doors that open a base.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -52,7 +65,7 @@ from typing import Any
|
|||
import pytest
|
||||
|
||||
from portfolio_optimiser import explore, okf, run
|
||||
from portfolio_optimiser.mandate import Approach, Mandate
|
||||
from portfolio_optimiser.mandate import Approach, Mandate, MandateRoutingError
|
||||
from portfolio_optimiser.simulation import ScriptedChatClient
|
||||
from portfolio_optimiser.verdicts import VerdictCollision
|
||||
|
||||
|
|
@ -61,6 +74,11 @@ _BYGG = _EXAMPLES / "bygg-energi-mikro"
|
|||
_PID = "BYGG-KONTOR-NORD"
|
||||
|
||||
|
||||
class _Dispatched(Exception):
|
||||
"""Raised from the stand-in ``run_project`` so an arm can prove a run WAS dispatched without
|
||||
letting it proceed. An ``AssertionError`` would be indistinguishable from a failing assert."""
|
||||
|
||||
|
||||
def _base_copy(tmp_path: Path, *, name: str = "bygg-energi-mikro") -> Path:
|
||||
"""A throwaway copy of a shipped base. Mutations of fixture content NEVER touch the git-tracked
|
||||
tree (the repo's ``shutil.copytree`` discipline)."""
|
||||
|
|
@ -145,30 +163,36 @@ def test_the_three_origins_are_pairwise_distinct(tmp_path: Path) -> None:
|
|||
# --- (e)/(f) disagreement is refused, and refused EARLY -------------------------------------------
|
||||
|
||||
|
||||
def test_a_base_that_disagrees_with_its_mount_is_refused_by_name(tmp_path: Path) -> None:
|
||||
"""(e) The refusal itself. ``BundleIdMismatch`` subclasses ``ValueError`` deliberately, so it
|
||||
lands on the CLI's refusal tuple and hosting's 400 arm rather than the crash channel --
|
||||
``ExplorationError`` is a ``RuntimeError`` and would give a traceback and a 500."""
|
||||
def test_a_base_that_disagrees_with_its_mount_wins_over_it(tmp_path: Path) -> None:
|
||||
"""(e) REWRITTEN 2026-09-03 (S7a-3 pkt. 1), not deleted. This arm used to pin the opposite
|
||||
behaviour: a declared id the mount did not carry was refused by name. The operator decision
|
||||
slackened it -- the declaration IS the identity, the mount is a filesystem accident -- so the
|
||||
arm now pins what replaced it, and the refusal it used to pin lives in
|
||||
``test_bundle_id_slack_loadbearing.py`` in its new form (two CONCEPTS disagreeing)."""
|
||||
base = _base_copy(tmp_path)
|
||||
_declare(base / "index.md", "a-name-the-mount-does-not-carry")
|
||||
|
||||
with pytest.raises(okf.BundleIdMismatch) as excinfo:
|
||||
okf.reconcile_bundle_id(str(base))
|
||||
message = str(excinfo.value)
|
||||
assert "a-name-the-mount-does-not-carry" in message and base.name in message
|
||||
assert issubclass(okf.BundleIdMismatch, ValueError)
|
||||
resolved = okf.reconcile_bundle_id(str(base))
|
||||
|
||||
assert resolved.id == "a-name-the-mount-does-not-carry"
|
||||
assert resolved.origin == "declared-index"
|
||||
assert resolved.mount == base.name, "the overridden mount must survive on the resolution"
|
||||
|
||||
|
||||
def test_the_cli_refuses_a_disagreeing_base_before_it_spends_a_single_model_call(
|
||||
def test_the_cli_refuses_a_base_that_cannot_say_what_it_is_before_it_spends_a_call(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""(f) The assert is on ZERO model calls, never on the exit code alone: a refusal that arrives
|
||||
AFTER the spend looks identical at rc 1 (the M12 signature, and the hoist idiom
|
||||
``test_explore_callsites_loadbearing.py`` already uses twice)."""
|
||||
"""(f) REWRITTEN 2026-09-03 with the refusal it now guards: this used to drive a base whose
|
||||
declared id disagreed with its MOUNT, which no longer refuses. The EARLY half is what the arm
|
||||
was always for and is unchanged -- the assert is on ZERO model calls, never on the exit code
|
||||
alone, because a refusal that arrives AFTER the spend looks identical at rc 1 (the M12
|
||||
signature, and the hoist idiom ``test_explore_callsites_loadbearing.py`` already uses twice)."""
|
||||
monkeypatch.delenv("PORTFOLIO_MODEL_MAP", raising=False)
|
||||
monkeypatch.delenv("PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT", raising=False)
|
||||
base = _base_copy(tmp_path)
|
||||
_declare(base / "index.md", "not-the-mount")
|
||||
concepts = [p for p in sorted(base.rglob("*.md")) if p.name != "index.md"]
|
||||
_declare(concepts[0], "corpus-alfa")
|
||||
_declare(concepts[1], "corpus-beta")
|
||||
|
||||
sink: list[str] = []
|
||||
|
||||
|
|
@ -180,7 +204,7 @@ def test_the_cli_refuses_a_disagreeing_base_before_it_spends_a_single_model_call
|
|||
rc = run.main([_PID, "--docs-dir", str(base), "--bundle-dir", str(base)])
|
||||
assert rc == 1
|
||||
assert sink == [], (
|
||||
"the run reached the model before the mount was reconciled -- the base was paid for "
|
||||
"the run reached the model before the base was asked what it is -- the base was paid for "
|
||||
f"before it was refused ({len(sink)} calls)"
|
||||
)
|
||||
|
||||
|
|
@ -243,18 +267,20 @@ async def test_run_project_reconciles_the_base_its_bundle_arm_opens(_spy: list[s
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_dispatcher_refuses_a_disagreeing_base_before_it_starts_any_run(
|
||||
async def test_the_dispatcher_routes_by_the_declared_id_not_the_mount(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""(j) The dispatcher door, and it needs its OWN discriminator.
|
||||
"""(j) REWRITTEN 2026-09-03 (S7a-3 pkt. 1) and now SHARPER than what it replaced.
|
||||
|
||||
A declared id that AGREES with the mount is by construction the basename, so
|
||||
``reconcile_bundle_id(raw).id`` and ``Path(raw).name`` return the same string on every base
|
||||
that resolves at all -- an assert on the routed id could not tell the two implementations
|
||||
apart. What CAN: a disagreeing base must be refused while assembling the routing table, i.e.
|
||||
BEFORE the first ``run_project`` is dispatched. Left as a private basename copy, routing
|
||||
succeeds and the first base is started; ``run_project``'s own reconciliation would refuse it,
|
||||
but only after the run had begun -- the M11/M12 pairing, one level up.
|
||||
It used to pin that a base disagreeing with its mount was refused while assembling the routing
|
||||
table. That refusal is gone; what took its place is a discriminator the old arm could not have:
|
||||
on a base whose declaration disagrees with its directory, the DECLARED id must route and the
|
||||
MOUNT must not. A dispatcher left on ``Path(raw).name`` answers exactly the other way round, so
|
||||
the two implementations differ in both halves rather than in an exit code.
|
||||
|
||||
The mount half is also the arm that keeps ``explore()`` and this door on ONE naming scheme: an
|
||||
exploration mints approaches from ``_bundle_index``, and if the two resolved ids differently an
|
||||
exploration's own mandate would be unroutable here.
|
||||
"""
|
||||
base = _base_copy(tmp_path)
|
||||
_declare(base / "index.md", "a-name-the-mount-does-not-carry")
|
||||
|
|
@ -263,20 +289,26 @@ async def test_the_dispatcher_refuses_a_disagreeing_base_before_it_starts_any_ru
|
|||
|
||||
async def counting_run_project(*args: Any, **kwargs: Any) -> Any:
|
||||
started.append(str(kwargs.get("bundle_dir")))
|
||||
raise AssertionError("a run was dispatched against an unreconciled base")
|
||||
raise _Dispatched
|
||||
|
||||
monkeypatch.setattr(run, "run_project", counting_run_project)
|
||||
|
||||
with pytest.raises(okf.BundleIdMismatch):
|
||||
await run.run_mandate_across_bundles(
|
||||
Mandate(
|
||||
def _mandate(named: str) -> Mandate:
|
||||
return Mandate(
|
||||
objective="o",
|
||||
approaches=(Approach(id="a", label="A", bundle_id=base.name),),
|
||||
approaches=(Approach(id="a", label="A", bundle_id=named),),
|
||||
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"
|
||||
|
||||
|
||||
# ==================================================================================================
|
||||
|
|
|
|||
369
tests/test_bundle_id_slack_loadbearing.py
Normal file
369
tests/test_bundle_id_slack_loadbearing.py
Normal 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,
|
||||
)
|
||||
|
|
@ -36,6 +36,7 @@ _PROVENANCE = ProvenanceStamp(
|
|||
# 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.
|
||||
cost_baseline_anchored=True,
|
||||
bundle_id_source=None,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ _PROVENANCE = ProvenanceStamp(
|
|||
# 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.
|
||||
cost_baseline_anchored=True,
|
||||
bundle_id_source=None,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ _PROVENANCE = ProvenanceStamp(
|
|||
# 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.
|
||||
cost_baseline_anchored=True,
|
||||
bundle_id_source=None,
|
||||
)
|
||||
_VALIDATED = ValidatedProposal(
|
||||
proposal=_PROPOSAL, p10=100.0, p50=150.0, p90=200.0, nominal_feasible=180.0
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ _PROVENANCE = ProvenanceStamp(
|
|||
# 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.
|
||||
cost_baseline_anchored=True,
|
||||
bundle_id_source=None,
|
||||
)
|
||||
_VALIDATED = ValidatedProposal(
|
||||
proposal=_PROPOSAL, p10=100.0, p50=150.0, p90=200.0, nominal_feasible=180.0
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ def _stamp() -> ProvenanceStamp:
|
|||
validator_decision="validated",
|
||||
token_usage=42,
|
||||
cost_baseline_anchored=True,
|
||||
bundle_id_source=None,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -32,6 +33,7 @@ def test_zero_citations_is_rejected() -> None:
|
|||
validator_decision="validated",
|
||||
token_usage=1,
|
||||
cost_baseline_anchored=True,
|
||||
bundle_id_source=None,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue