docs: multi-base som invariant + den ende-til-ende-sloeyfa som beviser den (ORDRE 20260825T080753Z)

CLAUDE.md-invarianten «Multi-base er en PARTISJON, aldri en videre
run_project-signatur» baerer hele designet: den strukturelle grunnen til at
run_project ikke KAN ta flere bundle_dir (fire enkeltverdier avledet fra DEN
basen), de tre soemmene, hvorfor dispatchen ikke tar project_id, de tolv maalte
mutasjonene, og de fire uttalte aerlighets-grensene - inkludert at CLI-en er
BEVISST uroert (§ C.8 ber om ETT nytt kallsted i run.py, levert i 57; et
repeterbart --bundle-dir er en NY operatoerflate og en egen beslutning).

README faar multi-base-doeren beskrevet der --explore alt er beskrevet, med den
samme nekten uttalt for en leser som ikke leser CLAUDE.md: en hypotese som ikke
navngir noen base blir NEKTET naar flere er konfigurert, aldri rutet til en
gjetning.

T22 er ende-til-ende-vitnet, og det eneste stedet de tre soemmene moetes:
prompt + TO baser -> hypotesiseren former to retninger og navngir hver sin base
-> route_by_bundle partisjonerer -> hver base sin run_project svarer for SIN
hypotese og ingen andres. Assertet paa COVERAGE-radene, ikke paa kall-argumenter,
fordi det er rapporten en fagperson faktisk leser: en approach som havnet i feil
base ville fortsatt sett evaluert ut.

1021 passed / 5 skipped; golden demo-transcript.stdout byte-uendret
(ea8c534773acdbe41ae68f2c55724d69aaf8be4f); mypy/ruff rene.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L3YHobQC3WzYVoxSgZus4d
This commit is contained in:
Kjell Tore Guttormsen 2026-08-25 13:41:31 +02:00
commit 785261f229
3 changed files with 127 additions and 0 deletions

View file

@ -903,6 +903,58 @@ Python ≥3.10. MAF (`agent-framework-core` 1.9.0). Pakkehåndtering: `uv`. To b
formet mandatet** — det er ingen outbox der og intet utforskningsfelt i `_response_payload`, så
ledgeren og de rådgivende dommene når kun CLI-ens artefakt. En bevisst scope-grense, men uttalt,
fordi flatens hele argument er at svaret er etterprøvbart.
- **Multi-base er en PARTISJON, aldri en videre `run_project`-signatur (U4+U13 del 3, § C.7, økt 58):**
planens § C.7 og økt 56s egen ærlighets-grense leste som om leveransen var «`run_project` tar mer
enn én `bundle_dir`». **Den kan ikke det, og nekten er STRUKTURELL:** på bundle-stien avleder
`run_project` FIRE enkeltverdier fra DEN basen — prosjektet (`_project_from_bundle`, som
fail-faster når basens egen `validator-input.json` ikke navngir det forespurte prosjektet),
validatorens stage-0-baseline (S4.0s hele poeng er at gaten er forankret i DETTE prosjektets
kostlinjer), agentenes lesekontekst og ExpeL-nøkkelen — og returnerer ETT stemplet `RunResult`.
En andre katalog på den signaturen ville tvunget et stille velg-en for alle fire, som er den
gjettede-form-klassen repoet nekter. **Planens egen setning sier det samme lest nært:**
«pipelinen kjøres per bundle som i dag (`run_portfolio`-formen)» = N kall, ikke ETT kall med N.
Premisset ble felt FØR bygging; ordren ba selv om nettopp den sjekken. **Konsekvensen er at INGEN
eksisterende kaller endrer signatur** — CLI, hosting og simulation sender fortsatt én base hver,
og kan fortsatt gjøre det. Tre sømmer: (1) `mandate.Approach.bundle_id`, default `""`, så hvert
mandat skrevet før i dag er fortsatt gyldig OG dispatchbart uendret; (2) `mandate.route_by_bundle`
— ren partisjon i `bundle_ids`-rekkefølge (aldri i approach-rekkefølge: spend-ordenen er en
egenskap ved hvordan kjøringen ble konfigurert, ikke ved hvordan en modell tilfeldigvis sekvenserte
hypotesene), **fail-fast på et mandat som ikke kan utføres som skrevet** (`load_mandate`-regelen —
en kjøring skal aldri gå videre på en stille degradert bestilling); (3)
`run.run_mandate_across_bundles` — dispatchen. **Den tar INGEN `project_id`-parameter, og det er
designet:** hver bases prosjekt leses fra DEN basens egen IR-projeksjon, altså nøyaktig verdien
`_project_from_bundle` allerede fail-faster mot, så en kaller-oppgitt konstant kunne uansett bare
vært riktig for én base av N — den eksisterende fail-fasten blir rutingsnøkkelen, og gjetningen
forsvinner. Ett `VerdictStore` trådes på tvers (kryss-base-læring, `run_portfolio`-formen), og
**delt INSTANS er påstanden — ikke lik verdi** (se vakuitets-funnet under). En base ingen approach
navngir kjøres IKKE (en kjøring koster penger, og bestillingen ba om ingenting der); med NØYAKTIG
én base absorberer den alt uten navn, som ikke er en gjetning men det eneste mulige svaret — og
det er dét som holder hvert pre-multi-base-mandat dispatchbart. `explore()` stempler `bundle_id`
på hver MYNTET approach, men **skriver ALDRI om et frø** (§ C.6 dør 1 er en bevaringsregel — å
fylle inn feltet på ekspertens vegne ville satt deres navn på en rutingsbeslutning de ikke tok);
frøene VALIDERES i stedet, **FØR første modellkall** (økt-57-hoisten: ved unntaket alene ser en
nekt etter forbruket identisk ut med en før). En umerket markør med flere baser NEKTES
(`HypothesisParseError`), med én base resolveres den. **Budsjett: de to S3.4-tennene som HAR
mening her** — oppstartsnekt (`BudgetRefused`) og aldri-startet + `budget_stop` +
`not_evaluated`-rader i `MultiBaseResult.unreached`; bølge-reservasjonen har ingen motpart, for
dispatchen er SEKVENSIELL. **Ærlighets-grenser, uttalt:** en base som RAISER propagerer
(collect-and-continue tilhører `run_portfolio`, der kalleren sendte inn en batch uavhengige
prosjekter); uten `portfolio_meter` er taket antall rutede baser × `max_tokens`, hver kjøring
bundet for seg; outboxen er IKKE wiret (N kjøringer trenger N `run_id`-er, og å mynte dem her
ville defaultet en nøkkel repoet krever at en kaller oppgir); og **CLI-en er BEVISST urørt**
§ C.8 ber om ETT nytt kallsted i `run.py` (`--explore`, levert i 57), og et repeterbart
`--bundle-dir` er en NY operatørflate, altså en egen beslutning. Load-bearing MÅLT
(`tests/test_multibase_loadbearing.py`, 22 tester), tolv mutasjoner alle røde mot HELE suiten +
grønn kontroll 1020/5 og golden `demo-transcript.stdout` BYTE-UENDRET
(`ea8c534773acdbe41ae68f2c55724d69aaf8be4f`): detach myntet `bundle_id` (2 røde) · stille
gjennomfall ved >1 base (1) · ukjent id resolvert etter rekkefølge (1) · frø-sjekk etter forbruket
(2 — asserten er på at NULL modellkall skjedde, ikke på unntaket) · ruteren gjetter første base (1)
· uroutbar approach droppet (1) · dispatchen kollapser til én base (5) · `project_id` fra første
base (2) · detach aldri-startet-tannen (1) · `unreached` urapportert (1) · fersk store per base (1)
· detach oppstartsnekten (2). **ÉN MUTASJON FALSIFISERTE TESTEN FØRST (repoets vakuøs-gate-klasse,
åttende gang):** store-testen sammenlignet med `==`, og `VerdictStore` er en pydantic-modell med
VERDI-likhet — tre ulike TOMME stores er alle like, så «fersk store per base» lot HELE suiten stå
grønn. Delt instans er påstanden, så testen asserterer nå på `is`.
- **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.

View file

@ -443,6 +443,17 @@ when the seam is detached, so the loop cannot silently degrade into theater.
The hosted surface takes the same door as `explore_prompt` + `explore_contract` on
`POST /invocations`.
**Several knowledge bases (library API).** An exploration may be given more than one base
(`explore(..., bundle_dirs=[a, b])`). Each approach it shapes records which base it belongs to
(`Approach.bundle_id`), and `run_mandate_across_bundles(mandate, bundle_dirs, ...)` then runs the
pipeline **once per base** — the ordinary `run_project`, with that base's own sub-mandate, and
with each run's project read from that base's own `validator-input.json`. `run_project` itself
still takes one `bundle_dir`, deliberately: it derives the project, the validator's cost
baseline, the agents' read context and the retrieval key from the base it is handed, so a second
directory on that call would mean silently picking one of them. A hypothesis that names no base
is refused when several are configured, rather than routed to a guess. The CLI's `--bundle-dir`
stays single-valued; multi-base is a library door today.
`--semantic-retrieval` (S3.1) is an **opt-in** ranking change, **off by default**. Off, prior
verdicts are ranked exactly as before: a structural score over the affected cost-code set,
measure type and magnitude bucket, with surface text deliberately excluded. On, that score is

View file

@ -667,3 +667,67 @@ async def test_one_store_is_threaded_across_every_base(
assert len(calls) == 2
assert all(c["store"] is store for c in calls)
@pytest.mark.asyncio
async def test_exploration_and_dispatch_close_the_loop_over_two_bases(tmp_path: Path) -> None:
"""T22: the end-to-end witness for § C.7 — prompt + TWO bases -> mandate -> two pipelines.
Every test above pins one seam. This one is the only place the three meet: the hypothesiser
shapes two directions and names a DIFFERENT base for each, ``route_by_bundle`` partitions them,
and each base's own ``run_project`` answers for its own hypothesis and for nobody else's. Every
reply is scripted, so what is shown is that the plumbing closes never that a live model would
shape either direction well (the demo's §1 honesty limit, unchanged).
Assertion is on the COVERAGE rows rather than on call arguments, because that is the report an
expert actually reads: an approach that reached the wrong base would still appear evaluated.
"""
bases = []
for src in (_BYGG, _TUNNEL):
dst = tmp_path / src.name
shutil.copytree(src, dst)
bases.append(str(dst))
exploration = await explore.explore(
_PROMPT,
contract=_CONTRACT,
bundle_dirs=tuple(bases),
client_factory=_factory(
ledgers=[
_ledger_json(satisfied=False, speaker=explore.HYPOTHESISER_ROLE),
_ledger_json(satisfied=True, speaker=explore.HYPOTHESISER_ROLE),
],
hypothesiser=[
_hypothesis_line("Behovsstyrt lys", "fixtures are 1990s", "bygg-energi-mikro")
+ "\n"
+ _hypothesis_line("Nattsenking", "the tunnel runs lit all night", "tunnel-hauglia")
],
),
)
assert [a.bundle_id for a in exploration.mandate.approaches] == [
"bygg-energi-mikro",
"tunnel-hauglia",
]
def factory(role: str) -> BaseChatClient:
return ScriptedChatClient(
reply_selector=lambda _blob, _role: _reply_for("ENERGI-TOTAL-EL", 300000, 1.0, 30_000),
role=role,
)
result = await run_mandate_across_bundles(
exploration.mandate,
tuple(bases),
"local",
verdict_input=_VERDICT_INPUT,
store=VerdictStore(verdicts=[]),
client_factory=factory,
max_rounds=1,
)
assert [r.bundle_id for r in result.runs] == ["bygg-energi-mikro", "tunnel-hauglia"]
assert [{row.id for row in r.result.coverage} for r in result.runs] == [
{"hypothesis-1", "own-proposal"},
{"hypothesis-2", "own-proposal"},
]