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

@ -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"},
]