feat(toolbox): the first four doors out of the toolbox, without a chat client on the way

B-gate row 1's premise, made callable. Every path through the framework CLI constructs a chat
client, so an outside caller -- a human at a terminal, or an agent that is NOT po -- could not
reach a single run-path step without paying for a model. These four steps need no model at all.

One CLI, four subcommands, one core call each:

  navigate-bundle  --bundle-dir                         -> okf.navigate_bundle
  cost-baseline    --bundle-dir --project-id            -> okf.derive_cost_baseline
  retrieve-chunks  --query --docs-dir [--top-k]         -> datasource.retrieve_chunks
  prepass-admit    --payload --bundle-dir [--dimension] -> prepass.admit_payload

Each handler is a thin adapter: strings in, the SAME function the run path calls, JSON on stdout,
and an exit code that says what happened (0 ran, 2 malformed call, 3 the step refused, named).
A handler that computed anything of its own would be a second implementation of a run-path step,
and the outside caller would stop getting what the debate gets.

Dispatch is an explicit branch per command, not argparse's `set_defaults(handler=...)`: the table
hides the one thing a reader wants to see, and B-gate row 1 asks the same question of the source
(it walks the call graph from `main` down to the step's symbol), where a callable in a Namespace
is a hop neither can follow.

Probes (`tests/test_toolbox_doors.py`, 10 arms): each starts the door as a SUBPROCESS with the
subcommand in argv and asserts on what it wrote -- never by importing the core function, which is
the whole difference the gate exists to measure. The yardstick is outside the door in every arm:
the filesystem (navigate-bundle, including the one deliberate outside-bundle link), a table
transcribed from the priced fixture (cost-baseline), the in-process seam it must equal byte for
byte (retrieve-chunks), and the producer's own checked-in payload (prepass-admit). Every refusal
arm has an rc-0 control beside it.

`portfolio-optimiser-toolbox` is the THIRD console script, and the pin test now says why: it is
the door the other two cannot be used for. README and CLAUDE.md updated with the command and the
reason it exists; every documented invocation was run.

Row 1: 1 -> 5 of 17 (four subcommands + `gate`, which the class fix in e47be68 stopped rejecting
on a name technicality). No other row moved; exit 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-20 08:11:58 +02:00
commit 38df79126f
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
8 changed files with 532 additions and 37 deletions

View file

@ -44,7 +44,7 @@ _NOW = datetime(2026, 9, 19, 12, 0, tzinfo=timezone.utc)
#: Repoets egne tall ved skriving (19.09.2026), talt av armene under mot KILDEN. De står her for
#: at en stille endring i kjørestien skal vise seg som en rød test og ikke som et nytt tall.
_STEPS_TODAY = 17
_EXTERNAL_TODAY = 1
_EXTERNAL_TODAY = 5
_ROLES_TODAY = ("proposer", "checker")
_PATTERNS_TODAY = 15
@ -106,16 +106,25 @@ def test_the_step_denominator_equals_an_independent_count_of_the_source() -> Non
assert row.n == _STEPS_TODAY
def test_row1_today_is_red_with_the_steps_that_have_no_door_named() -> None:
"""Var «3 av 17» til 19.09, så 0 av 17 da proben ble BUNDET til steget. Fra 20.09 teller
``gate`` igjen: den proben kjører v1-gaten som subprosess og leser JSON-en ut av den, og
steget er alene bak den døren den ble avvist en navneteknikalitet, ikke atferd."""
#: Stegene som HAR en dør ut 20.09.2026, skrevet ut. Fire av dem er verktøykassens under-
#: kommandoer (steg 2 i byggerekkefølgen); ``gate`` er v1-gatens egen kommando, som alltid var
#: en ekte dørprobe og ble avvist på en navneteknikalitet til klasse-rettingen samme dag.
_WITH_A_DOOR_TODAY = ("gate", "kontekst", "kostnadsgrunnlag", "pakke", "prepass")
def test_row1_today_is_red_and_names_exactly_the_steps_that_have_a_door() -> None:
"""Var «3 av 17» til 19.09, så 0 av 17 da proben ble BUNDET til steget, og 5 av 17 fra 20.09.
Armen teller ikke bare k den navngir HVILKE, en dør som flytter seg uten å bli bygd
(eller en probe som slutter å drive sin dør) blir rød her og ikke bare et annet tall."""
passing = {n: "passed" for step in _CONFIG["steps"] for n in step.get("probe", ())}
row = gate.score_toolbox(_CONFIG["steps"], _SRC, _CONFIG["run_path"], passing, _REPO)
assert (row.k, row.n, row.status) == (_EXTERNAL_TODAY, _STEPS_TODAY, gate.RED)
named = {x.split(":")[0] for x in row.exceptions}
assert "validering" in named and "utboks" in named
assert "prepass-artefakt" in named and "parse-feil" in named
assert len(_WITH_A_DOOR_TODAY) == _EXTERNAL_TODAY
uten_doer = {x.split(":")[0] for x in row.exceptions}
alle = {str(s["id"]) for s in _CONFIG["steps"]}
assert sorted(alle - uten_doer) == sorted(_WITH_A_DOOR_TODAY), row.exceptions
assert "validering" in uten_doer and "utboks" in uten_doer
assert "prepass-artefakt" in uten_doer and "parse-feil" in uten_doer
bound = {x.split(":")[0] for x in row.exceptions if "atferdsprobe" in x}
assert bound == {"rundebinding", "rapport"}, row.exceptions
@ -1862,3 +1871,38 @@ def test_the_row_limit_is_a_class_description_and_no_longer_an_enumeration() ->
assert "tekstmønstre" in limit or "tekstvakt" in limit
assert "uttømmende" in limit
assert "git-manifestet" in limit
def test_a_subcommand_door_is_only_driven_when_the_command_stands_in_argv(
tmp_path: Path,
) -> None:
"""For en underkommando ER kommandonavnet døren. En probe som starter CLI-en uten det har
kjørt en annen dør eller ingen. Uten denne armen ville kravet kunne fjernes uten at noe
ble rødt: verktøykassens egne prober har alle navnet i argv."""
src = _fake_src(tmp_path, chat_client=False)
_write(
src / "verktoey.py",
"import argparse\n\nfrom fakesrc.steg import gjoer_noe\n\n\n"
"def main(argv=None):\n"
" parser = argparse.ArgumentParser()\n"
' sub = parser.add_subparsers(dest="kommando", required=True)\n'
' sub.add_parser("naviger-pakke")\n'
" parser.parse_args(argv)\n"
" return gjoer_noe()\n\n\n"
'if __name__ == "__main__":\n raise SystemExit(main())\n',
)
steg = _one_step()
steg[0]["entry"] = {
"kind": "subcommand",
"module": "verktoey.py",
"scope": "main",
"command": "naviger-pakke",
}
_write_probe(tmp_path, door="fakesrc.verktoey", needle="naviger-pakke")
row = gate.score_toolbox(steg, src, _fake_run_path(), _FAKE_PASSED, tmp_path)
assert (row.k, row.n) == (1, 1), row.exceptions
_write_probe(tmp_path, door="fakesrc.verktoey", needle="gjoer_noe")
row = gate.score_toolbox(steg, src, _fake_run_path(), _FAKE_PASSED, tmp_path)
assert (row.k, row.n) == (0, 1), row.exceptions
assert any("underkommandoen" in x for x in row.exceptions), row.exceptions