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:
parent
e47be68b57
commit
38df79126f
8 changed files with 532 additions and 37 deletions
|
|
@ -130,11 +130,14 @@
|
|||
"scope": "run_project"
|
||||
},
|
||||
"entry": {
|
||||
"kind": "console-script",
|
||||
"module": "run.py",
|
||||
"scope": "main"
|
||||
"kind": "subcommand",
|
||||
"module": "toolbox.py",
|
||||
"scope": "main",
|
||||
"command": "navigate-bundle"
|
||||
},
|
||||
"probe": []
|
||||
"probe": [
|
||||
"tests/test_toolbox_doors.py::test_navigate_bundle_from_outside_reports_every_file_the_base_holds"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "kostnadsgrunnlag",
|
||||
|
|
@ -146,11 +149,14 @@
|
|||
"scope": "run_project"
|
||||
},
|
||||
"entry": {
|
||||
"kind": "console-script",
|
||||
"module": "run.py",
|
||||
"scope": "main"
|
||||
"kind": "subcommand",
|
||||
"module": "toolbox.py",
|
||||
"scope": "main",
|
||||
"command": "cost-baseline"
|
||||
},
|
||||
"probe": []
|
||||
"probe": [
|
||||
"tests/test_toolbox_doors.py::test_cost_baseline_from_outside_derives_exactly_what_the_priced_table_says"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "kontekst",
|
||||
|
|
@ -162,11 +168,14 @@
|
|||
"scope": "run_project"
|
||||
},
|
||||
"entry": {
|
||||
"kind": "console-script",
|
||||
"module": "run.py",
|
||||
"scope": "main"
|
||||
"kind": "subcommand",
|
||||
"module": "toolbox.py",
|
||||
"scope": "main",
|
||||
"command": "retrieve-chunks"
|
||||
},
|
||||
"probe": []
|
||||
"probe": [
|
||||
"tests/test_toolbox_doors.py::test_retrieve_chunks_from_outside_matches_the_in_process_seam_exactly"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "prepass",
|
||||
|
|
@ -178,11 +187,14 @@
|
|||
"scope": "run_project"
|
||||
},
|
||||
"entry": {
|
||||
"kind": "console-script",
|
||||
"module": "run.py",
|
||||
"scope": "main"
|
||||
"kind": "subcommand",
|
||||
"module": "toolbox.py",
|
||||
"scope": "main",
|
||||
"command": "prepass-admit"
|
||||
},
|
||||
"probe": []
|
||||
"probe": [
|
||||
"tests/test_toolbox_doors.py::test_prepass_admit_from_outside_admits_the_producers_own_payload"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "validering",
|
||||
|
|
|
|||
190
src/portfolio_optimiser/toolbox.py
Normal file
190
src/portfolio_optimiser/toolbox.py
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
"""The toolbox door: po's run-path steps as ONE command line, without a chat client on the way.
|
||||
|
||||
This is B's premise made callable. The framework's own CLI (``portfolio_optimiser.run``) drives a
|
||||
MAF debate and therefore constructs a chat client on every path through it; an outside caller —
|
||||
a human at a terminal, or an agent that is NOT po — cannot reach ``navigate_bundle`` or
|
||||
``retrieve_chunks`` through that door without paying for a model. The steps themselves need no
|
||||
model at all. This module exposes exactly those steps, and nothing else.
|
||||
|
||||
**One CLI, four subcommands, one core call each.** Each subcommand parses arguments, calls the
|
||||
SAME function the run path calls, writes the result as JSON on stdout, and returns an exit code
|
||||
that says what happened: ``0`` the step ran, ``2`` the call was malformed (argparse), ``3`` the
|
||||
step refused and the refusal is named in the JSON. There is no fourth code and no silent zero —
|
||||
the exit code is the only signal a calling agent has before it reads a byte.
|
||||
|
||||
**No re-implementation, and that is the load-bearing part.** Every handler below is a thin
|
||||
adapter: it converts strings to the types the core function already takes and converts what came
|
||||
back to JSON. A handler that computed anything of its own would be a second implementation of a
|
||||
run-path step, and the two would drift — which is the one thing a toolbox door must not do,
|
||||
because the whole claim is that the outside caller gets what the debate gets.
|
||||
|
||||
**Deliberately NOT here:** anything that needs a chat client, anything that writes into the
|
||||
run's outbox, and any path back into ``portfolio_optimiser.run``. The import list is part of the
|
||||
contract: B-gate row 1 refuses a door whose entry reaches a chat-client name, and row 3 refuses
|
||||
a repository with a written path to Claude. Both read this file.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any
|
||||
|
||||
from portfolio_optimiser import okf, prepass
|
||||
from portfolio_optimiser.datasource import retrieve_chunks
|
||||
|
||||
__all__ = ["main"]
|
||||
|
||||
#: Exit code for a step that REFUSED. Separate from 2 (malformed call) because they are two
|
||||
#: different facts about the caller's request: 2 means "you asked wrong", 3 means "you asked
|
||||
#: right and the answer is no". An agent retries the first and reports the second.
|
||||
REFUSED = 3
|
||||
|
||||
#: What a refusal is allowed to be. Everything here is a REFUSAL the core functions document —
|
||||
#: a bad bundle, an unpriced schedule, a payload that is not this base's cut. A bug in po is not
|
||||
#: in this tuple and must keep crashing with its traceback.
|
||||
_REFUSALS = (ValueError, FileNotFoundError, OSError)
|
||||
|
||||
|
||||
def navigate_bundle_command(args: argparse.Namespace) -> Mapping[str, Any]:
|
||||
"""``navigate-bundle`` — open a knowledge base and report what navigation reached.
|
||||
|
||||
``skipped`` is carried out of the door rather than summarised away: "this document was never
|
||||
written" and "the link to it was wrong" are two different repairs, and a caller that only
|
||||
saw a file count could act on neither."""
|
||||
bundle = okf.navigate_bundle(args.bundle_dir)
|
||||
return {
|
||||
"bundle_dir": bundle.dir,
|
||||
"files": [
|
||||
{"name": f.name, "type": f.type, "characters": len(f.body)} for f in bundle.files
|
||||
],
|
||||
"skipped": [
|
||||
{"from_file": s.from_file, "target": s.target, "reason": s.reason}
|
||||
for s in bundle.skipped
|
||||
],
|
||||
"counts": {"files": len(bundle.files), "skipped": len(bundle.skipped)},
|
||||
}
|
||||
|
||||
|
||||
def cost_baseline_command(args: argparse.Namespace) -> Mapping[str, Any]:
|
||||
"""``cost-baseline`` — derive the anchor from a priced schedule already in the base.
|
||||
|
||||
``project_id`` is required for the reason ``okf.derive_cost_baseline`` requires it: the base
|
||||
carries one too, and reading it here would make this a second reader of a fact that has an
|
||||
owner."""
|
||||
bundle = okf.navigate_bundle(args.bundle_dir)
|
||||
baseline = okf.derive_cost_baseline(bundle, project_id=args.project_id)
|
||||
return baseline.model_dump()
|
||||
|
||||
|
||||
def retrieve_chunks_command(args: argparse.Namespace) -> list[dict[str, Any]]:
|
||||
"""``retrieve-chunks`` — the shared data-source call, byte for byte.
|
||||
|
||||
The SAME function the in-process ``FunctionTool`` and the MCP path call. An outside caller
|
||||
gets the citation-ready shape the agents get, not a variant of it."""
|
||||
return retrieve_chunks(args.query, args.docs_dir, args.top_k)
|
||||
|
||||
|
||||
def prepass_admit_command(args: argparse.Namespace) -> Mapping[str, Any]:
|
||||
"""``prepass-admit`` — everything that must hold before a declared cut may shape a run.
|
||||
|
||||
The identity check needs the base's RESOLVED id, so the door resolves it the one way the
|
||||
repository resolves it (``okf.reconcile_bundle_id``) rather than reading the mount name."""
|
||||
payload = prepass.load_prepass_payload(args.payload)
|
||||
resolved = okf.reconcile_bundle_id(args.bundle_dir)
|
||||
prepass.admit_payload(
|
||||
payload,
|
||||
bundle_dir=args.bundle_dir,
|
||||
resolved_id=resolved,
|
||||
dimension=args.dimension,
|
||||
)
|
||||
return {
|
||||
"admitted": True,
|
||||
"bundle_id": resolved.id,
|
||||
"bundle_id_origin": resolved.origin,
|
||||
"mount": resolved.mount,
|
||||
"considered": payload.denominators.considered,
|
||||
"withheld": payload.denominators.withheld,
|
||||
"delivered": payload.denominators.delivered,
|
||||
}
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
"""The four doors, each registered by name.
|
||||
|
||||
``required=True`` on the subparsers: a toolbox invoked with no command must be a usage error,
|
||||
never a zero. Measured as a class in this repository — an exit 0 for a call that did nothing
|
||||
is indistinguishable, to an automated caller, from a call that did everything."""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="portfolio-optimiser-toolbox",
|
||||
description="po's run-path steps as plain commands — no model call, no network. "
|
||||
"Exit 0 when the step ran, 2 on a malformed call, 3 when the step refused.",
|
||||
)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
naviger = sub.add_parser("navigate-bundle", help="open a knowledge base and report its files")
|
||||
naviger.add_argument("--bundle-dir", required=True, help="the base's root directory")
|
||||
|
||||
kostnad = sub.add_parser("cost-baseline", help="derive the cost baseline from a priced table")
|
||||
kostnad.add_argument("--bundle-dir", required=True, help="the base's root directory")
|
||||
kostnad.add_argument("--project-id", required=True, help="the project the baseline anchors")
|
||||
|
||||
hent = sub.add_parser("retrieve-chunks", help="retrieve citation-ready chunks")
|
||||
hent.add_argument("--query", required=True, help="what to retrieve for")
|
||||
hent.add_argument("--docs-dir", required=True, help="the folder to retrieve from")
|
||||
hent.add_argument("--top-k", type=int, default=3, help="how many chunks (default: 3)")
|
||||
|
||||
slipp = sub.add_parser("prepass-admit", help="admit a declared pre-pass cut, or refuse it")
|
||||
slipp.add_argument("--payload", required=True, help="the producer's payload JSON")
|
||||
slipp.add_argument("--bundle-dir", required=True, help="the base the cut claims to be of")
|
||||
slipp.add_argument("--dimension", default=None, help="restrict admission to one dimension")
|
||||
return parser
|
||||
|
||||
|
||||
def dispatch(args: argparse.Namespace) -> Any:
|
||||
"""Name the four handlers, one branch each — deliberately not a ``set_defaults(handler=…)``.
|
||||
|
||||
The dispatch table argparse offers is one line shorter and hides the only thing a reader of
|
||||
this module wants to see: which command reaches which run-path step. B-gate row 1 asks the
|
||||
same question of the source (it walks the call graph from ``main`` down to the step's symbol)
|
||||
and a callable stored in a Namespace is a hop neither a reader nor the gate can follow."""
|
||||
if args.command == "navigate-bundle":
|
||||
return navigate_bundle_command(args)
|
||||
if args.command == "cost-baseline":
|
||||
return cost_baseline_command(args)
|
||||
if args.command == "retrieve-chunks":
|
||||
return retrieve_chunks_command(args)
|
||||
if args.command == "prepass-admit":
|
||||
return prepass_admit_command(args)
|
||||
raise RuntimeError(f"unregistered command {args.command!r}") # pragma: no cover - argparse
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
result = dispatch(args)
|
||||
except _REFUSALS as refusal:
|
||||
json.dump(
|
||||
{
|
||||
"error": {
|
||||
"kind": type(refusal).__name__,
|
||||
"message": str(refusal),
|
||||
"command": args.command,
|
||||
}
|
||||
},
|
||||
sys.stdout,
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
print()
|
||||
return REFUSED
|
||||
json.dump(result, sys.stdout, ensure_ascii=False, indent=2)
|
||||
print()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover - dekket av subprosess-probene
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue