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

View file

@ -11,11 +11,13 @@ clone materializes after ``uv sync``, so it is the only reading that can fail wh
merely *declared*. The declaration is checked too (the TOML is the source the metadata is built
from), but the metadata assert is the load-bearing one.
Scope, stated so it is a decision and not an oversight: exactly TWO commands are exposed. ``run``
is the framework CLI (three documented modes) and ``simulation`` is the offline end-to-end proof
the README points a newcomer at. ``costsim`` / ``hitl`` / ``preflight`` keep the ``-m`` form they
are operator utilities, not the product's front door, and every name added here is a name the
freeze has to carry.
Scope, stated so it is a decision and not an oversight: exactly THREE commands are exposed.
``run`` is the framework CLI (three documented modes), ``simulation`` is the offline end-to-end
proof the README points a newcomer at, and ``toolbox`` (added 2026-09-20) is the door an outside
caller reaches the run-path steps through WITHOUT a chat client B-gate row 1's premise, and the
one thing the other two cannot be used for, since every path through ``run`` constructs a client.
``costsim`` / ``hitl`` / ``preflight`` keep the ``-m`` form they are operator utilities, not the
product's front door, and every name added here is a name the freeze has to carry.
"""
from __future__ import annotations
@ -32,6 +34,7 @@ _DIST = "portfolio-optimiser"
_EXPECTED: dict[str, str] = {
"portfolio-optimiser": "portfolio_optimiser.run:main",
"portfolio-optimiser-demo": "portfolio_optimiser.simulation:main",
"portfolio-optimiser-toolbox": "portfolio_optimiser.toolbox:main",
}

214
tests/test_toolbox_doors.py Normal file
View file

@ -0,0 +1,214 @@
"""B-gatens rad 1, steg 2: de fire verktøykasse-dørene, drevet UTENFRA som en kommando.
Hver arm her er en **atferdsprobe** i B-gatens forstand: den starter `portfolio_optimiser.toolbox`
som en subprosess med underkommandoens navn i argv, og asserterer det kommandoen SKREV. Ingen
av dem importerer kjernefunksjonen og kaller den in-prosess det er nettopp den forskjellen
gaten finnes for å måle, og en probe som gjør begge deler beviser ingenting om døren.
Nevneren for «hva kommandoen skal ha skrevet» er hentet UTENFOR døren i hver arm: filsystemet
(navigate-bundle), en tabell transkribert fra prisskjema-fixturen (cost-baseline), det in-prosess
API-et den deler søm med (retrieve-chunks), og produsentens egen innsjekkede nyttelast
(prepass-admit). En arm som sammenlignet døren med seg selv ville vært grønn uansett hva døren
gjorde.
INGEN modellkall, intet nett: hele poenget med verktøykassen er at disse stegene ikke har en
chatklient veien, og gaten feller døren om de får en.
"""
from __future__ import annotations
import ast
import json
import shutil
import subprocess
import sys
from pathlib import Path
from portfolio_optimiser import datasource
_REPO = Path(__file__).resolve().parents[1]
_MIKRO = _REPO / "shared" / "examples" / "bygg-energi-mikro"
_PRISSKJEMA = _REPO / "tests" / "fixtures" / "k2-prisskjema-SYNTETISK"
_PREPASS_PAYLOAD = (
_REPO / "tests" / "fixtures" / "prepass" / "bygg-energi-mikro-fixture.payload.json"
)
_DECLARED_ID = "bygg-energi-mikro-fixture"
#: Prisskjema-fixturens tabell, transkribert fra fixturen — ikke fra derivasjonen som leser den.
_PRICED_ROWS = {
"21.1": (1250.0, 850.0),
"24.2": (48000.0, 42.5),
"36.1": (4.0, 187500.0),
}
def _toolbox(*args: str) -> subprocess.CompletedProcess[str]:
"""Døren, som en subprosess. ``-m``-formen med vilje: den virker i en ren klone uten sync."""
return subprocess.run(
[sys.executable, "-m", "portfolio_optimiser.toolbox", *args],
cwd=_REPO,
capture_output=True,
text=True,
)
# --- navigate-bundle --------------------------------------------------------------------------
def test_navigate_bundle_from_outside_reports_every_file_the_base_holds() -> None:
"""Fasiten er FILSYSTEMET: mikro-basens `.md`-filer, talt her. Basen er bygget så hver av dem
nås fra indeksen, «navigert» og «finnes» skal falle sammen og gjør de det ikke, er det
dét armen skal si."""
proc = _toolbox("navigate-bundle", "--bundle-dir", str(_MIKRO))
assert proc.returncode == 0, proc.stderr
payload = json.loads(proc.stdout)
paa_disk = sorted(p.name for p in _MIKRO.glob("*.md"))
assert sorted(f["name"] for f in payload["files"]) == paa_disk
assert payload["counts"]["files"] == len(paa_disk)
assert payload["bundle_dir"] == str(_MIKRO)
# Sporet bæres UT av døren, ikke summert bort: mikro-basens indeks lenker med vilje til
# `shared/README.md`, som ligger utenfor basen. En dør som bare ga et filtall ville latt
# «dokumentet finnes ikke» og «lenken var feil» se like ut for den som skal rette det.
assert payload["skipped"] == [
{"from_file": "index.md", "target": "../../README.md", "reason": "outside-bundle"}
]
assert payload["counts"]["skipped"] == 1
def test_navigate_bundle_refuses_a_directory_that_is_no_bundle_with_a_named_code() -> None:
"""rc-0-kontrollens motstykke: en avvisning må IKKE være 0. Exit-koden er det eneste
signalet en agent som kaller døren har."""
proc = _toolbox("navigate-bundle", "--bundle-dir", str(_REPO / "docs"))
assert proc.returncode == 3, proc.stdout
assert json.loads(proc.stdout)["error"]["kind"]
# --- cost-baseline ----------------------------------------------------------------------------
def test_cost_baseline_from_outside_derives_exactly_what_the_priced_table_says() -> None:
proc = _toolbox(
"cost-baseline", "--bundle-dir", str(_PRISSKJEMA), "--project-id", "K2"
)
assert proc.returncode == 0, proc.stderr
payload = json.loads(proc.stdout)
assert payload["project_id"] == "K2"
assert set(payload["items"]) == set(_PRICED_ROWS)
for code, (quantity, unit_cost) in _PRICED_ROWS.items():
assert payload["items"][code]["quantity"] == quantity, code
assert payload["items"][code]["unit_cost"] == unit_cost, code
def test_cost_baseline_refuses_an_unpriced_schedule_and_names_the_row() -> None:
proc = _toolbox(
"cost-baseline", "--bundle-dir", str(_MIKRO), "--project-id", "bygg-kontor-nord"
)
assert proc.returncode == 3, proc.stdout
assert json.loads(proc.stdout)["error"]["message"]
# --- retrieve-chunks --------------------------------------------------------------------------
def test_retrieve_chunks_from_outside_matches_the_in_process_seam_exactly() -> None:
"""Den ene armen der fasiten ER det in-prosess API-et: verktøykassen skal gi en ekstern
kaller NØYAKTIG det MAF-agentene får gjennom sin FunctionTool, ikke en egen variant."""
proc = _toolbox(
"retrieve-chunks", "--query", "LED retrofit", "--docs-dir", str(_MIKRO), "--top-k", "2"
)
assert proc.returncode == 0, proc.stderr
payload = json.loads(proc.stdout)
assert payload == datasource.retrieve_chunks("LED retrofit", str(_MIKRO), 2)
assert len(payload) == 2
assert payload[0]["snippet"]
def test_retrieve_chunks_refuses_a_top_k_that_asks_for_nothing() -> None:
proc = _toolbox(
"retrieve-chunks", "--query", "x", "--docs-dir", str(_MIKRO), "--top-k", "0"
)
assert proc.returncode == 3, proc.stdout
assert "top_k" in json.loads(proc.stdout)["error"]["message"]
# --- prepass-admit ----------------------------------------------------------------------------
def _base_declaring_its_id(tmp_path: Path) -> str:
"""Mikro-basen kopiert med `bundle_id` erklært — `shared/` er et pull-only subtree."""
root = tmp_path / "en-annen-mount"
shutil.copytree(_MIKRO, root)
index = root / "index.md"
lines = index.read_text(encoding="utf-8").split("\n")
assert lines[0].strip() == "---", "basen åpner ikke lenger med frontmatter"
lines.insert(1, f"bundle_id: {_DECLARED_ID}")
index.write_text("\n".join(lines), encoding="utf-8")
return str(root)
def test_prepass_admit_from_outside_admits_the_producers_own_payload(tmp_path: Path) -> None:
"""Produsentens innsjekkede output, uendret. Uten denne rc-0-kontrollen måler avvisnings-
armen under ingenting den ville vært grønn mot en dør som avviste alt."""
proc = _toolbox(
"prepass-admit",
"--payload",
str(_PREPASS_PAYLOAD),
"--bundle-dir",
_base_declaring_its_id(tmp_path),
)
assert proc.returncode == 0, proc.stderr
payload = json.loads(proc.stdout)
assert payload["admitted"] is True
assert payload["bundle_id"] == _DECLARED_ID
assert payload["delivered"] == 4
def test_prepass_admit_refuses_a_payload_that_names_another_base(tmp_path: Path) -> None:
raw = json.loads(_PREPASS_PAYLOAD.read_text(encoding="utf-8"))
raw["bundle"]["bundle_id"] = "en-helt-annen-base"
endret = tmp_path / "endret.payload.json"
endret.write_text(json.dumps(raw, ensure_ascii=False), encoding="utf-8")
proc = _toolbox(
"prepass-admit",
"--payload",
str(endret),
"--bundle-dir",
_base_declaring_its_id(tmp_path),
)
assert proc.returncode == 3, proc.stdout
assert "en-helt-annen-base" in json.loads(proc.stdout)["error"]["message"]
# --- døren selv ---------------------------------------------------------------------------------
def test_the_toolbox_has_no_chat_client_on_any_of_its_paths() -> None:
"""Verktøykassens ene forutsetning, målt i kilden: ingen av de fire kommandoene drar en
modell med seg. Gaten måler det samme rad 1; her står det som en egen påstand, fordi det
er DENNE modulens grunn til å finnes."""
modul = _REPO / "src" / "portfolio_optimiser" / "toolbox.py"
tre = ast.parse(modul.read_text(encoding="utf-8"))
kalt = {
node.func.attr if isinstance(node.func, ast.Attribute) else getattr(node.func, "id", "")
for node in ast.walk(tre)
if isinstance(node, ast.Call)
}
for navn in ("create_chat_client", "client_factory", "BaseChatClient", "get_backend"):
assert navn not in kalt, navn
importert: set[str] = set()
for node in ast.walk(tre):
if isinstance(node, ast.ImportFrom):
importert |= {f"{node.module}.{a.name}" for a in node.names}
elif isinstance(node, ast.Import):
importert |= {a.name for a in node.names}
assert not any(navn.startswith("portfolio_optimiser.run") for navn in importert), importert
assert "portfolio_optimiser.run" not in importert
# Kontroll: armen KAN se en import — uten den ville den vært grønn mot et tomt sett.
assert "portfolio_optimiser.okf" in importert or "portfolio_optimiser.prepass" in importert
def test_wrong_usage_is_exit_two_and_never_a_silent_zero() -> None:
assert _toolbox().returncode == 2
assert _toolbox("ingen-slik-kommando").returncode == 2
assert _toolbox("navigate-bundle").returncode == 2