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

@ -871,15 +871,26 @@ Python ≥3.10. MAF (`agent-framework-core` 1.9.0). Pakkehåndtering: `uv`. To b
utforskningen kjørte eller ei — `simulate_learning_loop`s to-markør-vakt i demo-form. Manager- utforskningen kjørte eller ei — `simulate_learning_loop`s to-markør-vakt i demo-form. Manager-
manuset nøkles på PROMPT-STADIET, ikke prosjekt-ID-en, og det er ikke et unntak fra manuset nøkles på PROMPT-STADIET, ikke prosjekt-ID-en, og det er ikke et unntak fra
`scripted_proposer`-regelen: manageren får FEM ulike spørsmål og prosjekt-ID-en er konstant over `scripted_proposer`-regelen: manageren får FEM ulike spørsmål og prosjekt-ID-en er konstant over
alle fem. Load-bearing MÅLT (`tests/test_explore_callsites_loadbearing.py`, 23 tester), seksten alle fem. **`--outbox-dir` uten `--run-id` NEKTES i utforskningsblokka, og det er en HOIST — ikke
mutasjoner alle røde mot HELE suiten + grønn kontroll 998/5: detach sink-appenden (1) · andre liste en andre kopi av regelen:** `run_project` eier outbox-kontrakten og nekter på sin FØRSTE setning,
tidlig nok for enhver sti som fantes før U4, men utforskningen kjører FORAN det kallet — uten
hoisten brukes hele utforskningsbudsjettet på modellkall før nekten, og artefakt-skrivingen hoppes
over, så ikke engang regnskapet over hva som ble brukt overlever. Funnet i review FØR commit;
testen asserterer at INGEN modellkall skjedde, ikke bare at rc er 1 — ved exit-koden ser en nekt
etter forbruket identisk ut. **Scenarioet har BEVISST intet `label_in_bundle`-felt:** vakten
raiser før et resultat finnes, så feltet kunne kun vært `False`, og en assert på det ville vært
grønn mot enhver implementasjon — vakten ER kontrollen, og en alltid-sann gjentakelse av den ville
bare gjort den ekte lettere å avfeie. Load-bearing MÅLT
(`tests/test_explore_callsites_loadbearing.py`, 24 tester), sytten mutasjoner alle røde mot HELE
suiten, hver mot kontrollen som gjaldt da (990/5 for CLI-en + sporet, 996/5 for hosting, 998/5 for
sim-scenarioet, 999/5 for de to siste): detach sink-appenden (1) · andre liste
for rundene (4) · detach `--mandate`-nekten (1) · detach `--explore-config`-nekten (1) · skriv for rundene (4) · detach `--mandate`-nekten (1) · detach `--explore-config`-nekten (1) · skriv
artefaktet kun ved fullført kjøring (1) · detach CLI-ens `mandate=` (1) · slipp artefaktet kun ved fullført kjøring (1) · detach CLI-ens `mandate=` (1) · slipp
`enable_plan_review` gjennom, CLI (1) · detach `--bundle-dir`-kravet, CLI (1) · detach `enable_plan_review` gjennom, CLI (1) · detach `--bundle-dir`-kravet, CLI (1) · detach
`--live-dry-run`-nekten (1) · fjern `--explore` fra portefølje-partisjonen (1) · videresend de `--live-dry-run`-nekten (1) · fjern `--explore` fra portefølje-partisjonen (1) · videresend de
konsumerte feltene (2) · detach hostings `mandate=` (1) · detach `enable_plan_review`-nekten, konsumerte feltene (2) · detach hostings `mandate=` (1) · detach `enable_plan_review`-nekten,
hosting (1) · detach `bundle_dir`-kravet, hosting (1) · detach sim-scenarioets `mandate=` (1) · hosting (1) · detach `bundle_dir`-kravet, hosting (1) · detach sim-scenarioets `mandate=` (1) ·
detach vakuitets-vakten (1). **ÉN MUTASJON FALSIFISERTE TESTEN FØRST (repoets vakuøs-gate-klasse, detach vakuitets-vakten (1) · detach outbox/run-id-hoisten (1). **ÉN MUTASJON FALSIFISERTE TESTEN FØRST (repoets vakuøs-gate-klasse,
syvende gang):** portefølje-testen asserterte kun at meldingen nevnte `--explore`, og sto GRØNN syvende gang):** portefølje-testen asserterte kun at meldingen nevnte `--explore`, og sto GRØNN
uten partisjonen — kjøringen falt da gjennom til «`--explore` requires `--bundle-dir`», som nevner uten partisjonen — kjøringen falt da gjennom til «`--explore` requires `--bundle-dir`», som nevner
`--explore` også. To nekter som deler en delstreng er «assert aldri på ordlyd to grener deler», `--explore` også. To nekter som deler en delstreng er «assert aldri på ordlyd to grener deler»,
@ -888,7 +899,10 @@ Python ≥3.10. MAF (`agent-framework-core` 1.9.0). Pakkehåndtering: `uv`. To b
`bundle_dir`; utforskningens egne modellkall er UANNONSERTE (annonseringens kontrakt er at en `bundle_dir`; utforskningens egne modellkall er UANNONSERTE (annonseringens kontrakt er at en
KOMMISJON erklæres før arbeidet den bestiller, og før `explore()` returnerer finnes ingen — KOMMISJON erklæres før arbeidet den bestiller, og før `explore()` returnerer finnes ingen —
`exploration_notice` dekker gapet i det sløyfa er ferdig); `BudgetExceeded` ut av `--explore` `exploration_notice` dekker gapet i det sløyfa er ferdig); `BudgetExceeded` ut av `--explore`
tracebacker som den gjør for debatten i dag. tracebacker som den gjør for debatten i dag; og **den HOSTEDE flaten gir ingen innsyn i hva som
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.
- **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

@ -1767,6 +1767,20 @@ def main(argv: list[str] | None = None) -> int:
file=sys.stderr, file=sys.stderr,
) )
return 1 return 1
if args.outbox_dir and not args.run_id:
# A HOIST, not a second copy of the rule: ``run_project`` owns the outbox contract and
# refuses on its first statement, which is early enough for every path that existed
# before U4. The exploration runs AHEAD of that call, so without this the whole
# exploration budget is spent on model calls and only THEN refused — and the artefact
# write is skipped as well, so not even the record of what was spent survives. The same
# hoist ``main()`` performs for the required-args guard, for the same reason.
print(
"run refused: --outbox-dir requires --run-id, and with --explore that has to be "
"settled BEFORE the exploration runs (otherwise the loop spends its whole budget "
"on an argv that cannot finish)",
file=sys.stderr,
)
return 1
exploration_contract: ExplorationContract | None = None exploration_contract: ExplorationContract | None = None
if args.explore_config is not None: if args.explore_config is not None:

View file

@ -738,15 +738,16 @@ def scripted_exploration_factory(
class ExplorationSimulationResult: class ExplorationSimulationResult:
"""The trace of one U4 walkthrough: what the loop shaped, and whether the pipeline used it. """The trace of one U4 walkthrough: what the loop shaped, and whether the pipeline used it.
``label_in_bundle`` is the causality control carried in the result, exactly as There is deliberately NO ``label_in_bundle`` field. The vacuity guard below raises before a
``marker_in_run_a_prompt`` is: without it, a label the base already states would look like a result exists, so such a field could only ever be ``False`` an assertion on it would be green
shaped direction and the whole scenario would prove nothing.""" against every implementation, which is the class this scenario's guard exists to prevent. The
guard IS the control; a second, always-true restatement of it would only make the real one
easier to discount."""
exploration: ExplorationResult exploration: ExplorationResult
trace: ExplorationTrace trace: ExplorationTrace
run: RunResult run: RunResult
label: str label: str
label_in_bundle: bool
label_in_generation_prompt: bool label_in_generation_prompt: bool
generation_prompts: list[str] generation_prompts: list[str]
@ -828,7 +829,6 @@ async def simulate_exploration(
trace=trace, trace=trace,
run=run, run=run,
label=label, label=label,
label_in_bundle=label in context,
label_in_generation_prompt=any(label in p for p in prompts), label_in_generation_prompt=any(label in p for p in prompts),
generation_prompts=prompts, generation_prompts=prompts,
) )

View file

@ -130,16 +130,16 @@ def _factory(
def factory(role: str) -> BaseChatClient: def factory(role: str) -> BaseChatClient:
if role == explore.MANAGER_ROLE: 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: if role == explore.HYPOTHESISER_ROLE:
replies = list(hypothesiser) replies = list(hypothesiser)
def _hyp(_blob: str, _role: str) -> str: def _hyp(_blob: str, _role: str) -> str:
return replies.pop(0) if replies else "nothing further." 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: 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 ScriptedChatClient(fallback, sink, role=role)
return factory return factory
@ -403,19 +403,22 @@ def test_explore_belongs_to_single_project_mode(tmp_path, capsys) -> None:
@pytest.fixture() @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. """Inject the role-dispatching scripted factory into the seam ``main()`` resolves through.
``main()`` passes no ``client_factory``, and ``explore()`` imports ``run._default_factory`` ``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 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. which is what makes the arm below end-to-end rather than a wiring spy.
""" """
sink: list[str] = []
factory = _factory( factory = _factory(
ledgers=[_ledger_json(satisfied=False), _ledger_json(satisfied=True)], ledgers=[_ledger_json(satisfied=False), _ledger_json(satisfied=True)],
hypothesiser=[_hypothesis_line(_LABEL, "the index says the fittings are old")], hypothesiser=[_hypothesis_line(_LABEL, "the index says the fittings are old")],
fallback=_ENERGY_REPLY, fallback=_ENERGY_REPLY,
sink=sink,
) )
monkeypatch.setattr("portfolio_optimiser.run._default_factory", lambda profile: factory) 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: 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) 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 [a.label for a in result.exploration.mandate.approaches] == [result.label]
assert result.label_in_generation_prompt, ( assert result.label_in_generation_prompt, (
"the shaped direction never reached the hypothesis prompt — the demo would show a mandate " "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" 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 @pytest.mark.asyncio
async def test_a_direction_the_base_already_states_is_refused_as_vacuous(tmp_path) -> None: 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. """S2: a label the knowledge base ALREADY contains is refused, not demonstrated.