fix(explore): utforskningen skal ikke brenne budsjettet paa en argv som ikke kan fullfoere (ORDRE 20260823T204216Z)

Tre review-funn fra oerkt 57, alle handtert.

1. DEFEKT, funnet i review og REPRODUSERT foer fiks: --explore --outbox-dir uten
   --run-id kjoerte HELE utforskningen og ble deretter nektet av run_project, som
   eier outbox-kontrakten og nekter paa sin FOERSTE setning - tidlig nok for enhver
   sti som fantes foer U4, men utforskningen kjoerer FORAN det kallet. Artefakt-
   skrivingen hoppet ogsaa over (den krever begge), saa ikke engang regnskapet over
   hva som ble brukt overlevde. Fiksen er en HOIST i utforskningsblokka, ikke en
   andre kopi av regelen - samme hoist main() alt gjoer for de paakrevde argumentene,
   av samme grunn. Testen asserterer at INGEN modellkall skjedde, ikke bare at rc er
   1: ved exit-koden ser en nekt etter forbruket identisk ut.

2. VAKUOES ASSERT FJERNET (repoets egen klasse, snudd innover): scenarioets
   label_in_bundle ble beregnet av den SAMME variabelen vakten raiser paa, saa feltet
   kunne strukturelt kun vaere False og assertet kunne ikke feile mot noen
   implementasjon - mens docstringen kalte det en kausalitetskontroll «akkurat som
   marker_in_run_a_prompt». Feltet og assertet er borte; vakten ER kontrollen, og en
   alltid-sann gjentakelse av den ville bare gjort den ekte lettere aa avfeie.

3. MAALINGS-PAASTANDEN PRESISERT: M10 (fjern --explore fra portefoelje-partisjonen)
   ble re-maalt mot HELE suiten etter test-fiksen - foer sto den kun maalt med -k.
   Invarianten sa «seksten mutasjoner mot HELE suiten + groenn kontroll 998/5» og
   slo dermed sammen tre ulike kontroller; den oppgir naa alle fire (990/5, 996/5,
   998/5, 999/5). En paastand om egen maaling som er upresis om sin egen nevner er
   premiss-vs-faktum-regelen vendt innover.

Pluss en uttalt aerlighets-grense i invarianten: den HOSTEDE flaten gir ingen innsyn
i hva som formet mandatet (ingen outbox, intet utforskningsfelt i _response_payload).
Bevisst scope-grense, men uttalt, fordi flatens hele argument er at svaret er
etterproevbart.

Sytten mutasjoner totalt, alle roede mot HELE suiten. 999 passed / 5 skipped.
Golden-transkriptet byte-uendret (ea8c534773acdbe41ae68f2c55724d69aaf8be4f).
mypy + ruff rene.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRZhBJcxqTcqWyMW6hBttx
This commit is contained in:
Kjell Tore Guttormsen 2026-08-25 09:58:28 +02:00
commit 3cbea91a72
4 changed files with 69 additions and 14 deletions

View file

@ -130,16 +130,16 @@ def _factory(
def factory(role: str) -> BaseChatClient:
if role == explore.MANAGER_ROLE:
return ScriptedChatClient(reply_selector=_manager_script(ledgers), role=role)
return ScriptedChatClient(sink=sink, reply_selector=_manager_script(ledgers), role=role)
if role == explore.HYPOTHESISER_ROLE:
replies = list(hypothesiser)
def _hyp(_blob: str, _role: str) -> str:
return replies.pop(0) if replies else "nothing further."
return ScriptedChatClient(reply_selector=_hyp, role=role)
return ScriptedChatClient(sink=sink, reply_selector=_hyp, role=role)
if role == explore.NAVIGATOR_ROLE:
return ScriptedChatClient("NAVIGATOR: index read.", role=role)
return ScriptedChatClient("NAVIGATOR: index read.", sink, role=role)
return ScriptedChatClient(fallback, sink, role=role)
return factory
@ -403,19 +403,22 @@ def test_explore_belongs_to_single_project_mode(tmp_path, capsys) -> None:
@pytest.fixture()
def _explored_main(monkeypatch: pytest.MonkeyPatch) -> None:
def _explored_main(monkeypatch: pytest.MonkeyPatch) -> list[str]:
"""Inject the role-dispatching scripted factory into the seam ``main()`` resolves through.
``main()`` passes no ``client_factory``, and ``explore()`` imports ``run._default_factory``
lazily at call time, so this ONE patch covers both the exploration and the pipeline it feeds
which is what makes the arm below end-to-end rather than a wiring spy.
"""
sink: list[str] = []
factory = _factory(
ledgers=[_ledger_json(satisfied=False), _ledger_json(satisfied=True)],
hypothesiser=[_hypothesis_line(_LABEL, "the index says the fittings are old")],
fallback=_ENERGY_REPLY,
sink=sink,
)
monkeypatch.setattr("portfolio_optimiser.run._default_factory", lambda profile: factory)
return sink
def test_the_shaped_mandate_reaches_the_pipeline(tmp_path, capsys, _explored_main) -> None:
@ -705,7 +708,6 @@ async def test_the_demo_scenario_lets_a_shaped_direction_reach_the_hypothesis(tm
"""
result = await simulation.simulate_exploration(str(_BUNDLE_DIR), str(tmp_path), max_rounds=3)
assert result.label_in_bundle is False, "the control the scenario refuses on"
assert [a.label for a in result.exploration.mandate.approaches] == [result.label]
assert result.label_in_generation_prompt, (
"the shaped direction never reached the hypothesis prompt — the demo would show a mandate "
@ -714,6 +716,31 @@ async def test_the_demo_scenario_lets_a_shaped_direction_reach_the_hypothesis(tm
assert result.trace.ledger, "the exploration recorded no rounds"
def test_an_outbox_without_a_run_id_is_refused_before_the_exploration_spends_anything(
tmp_path, _explored_main
) -> None:
"""T16: an argv that cannot finish is refused BEFORE the loop costs anything.
``run_project`` refuses ``outbox_dir`` without ``run_id`` at its very first statement, which is
early enough for every path that existed before U4. The exploration runs AHEAD of that call, so
without this guard the run spends its whole exploration budget on model calls and only then
refuses and the artefact write is skipped too, so not even the evidence of what was spent
survives. Exactly the hoist ``main()`` already performs twice ("an incomplete argv is refused
BEFORE the honesty banner could claim a scripted run happened").
The assertion is that NO model call happened, not merely that rc is 1: a refusal that arrives
after the spend looks identical at the exit code.
Detach point: drop the guard from the exploration block RED.
"""
rc = run.main(_base_argv(tmp_path) + ["--outbox-dir", str(tmp_path / "outbox")])
assert rc == 1
assert not _explored_main, (
"the exploration made model calls before the run was refused — the budget was spent on an "
"argv that could never finish"
)
@pytest.mark.asyncio
async def test_a_direction_the_base_already_states_is_refused_as_vacuous(tmp_path) -> None:
"""S2: a label the knowledge base ALREADY contains is refused, not demonstrated.