feat(consume): one call over a folder asks every bundle under it

`okf consume <folder>` is the server's `okf_ask` with no bundle named,
byte for byte: no ranking of its own, every excerpt carrying its bundle
id. `--bundle-id` asks one bundle under the folder. A flag that acts on
one bundle's cut is refused by name over a folder rather than dropped,
because the server takes none of them. A bundle path reads as before.

v1.1 order F, part F2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-21 10:29:10 +02:00
commit 718c064279
4 changed files with 194 additions and 4 deletions

View file

@ -1532,6 +1532,14 @@ R761 **8** (S1-S6 + KP + KN), vegnormal **32** questions / **43**
held by a test comparing the printed bytes against the two functions. A
bundle path prints its card exactly as before. Tests over two invented
bundles: `tests/test_folder_of_bundles.py`.
- **`okf consume <folder>` ASKS EVERY BUNDLE UNDER IT IN ONE CALL (v1.1 F2).**
The reply is `mcp_server.call_ask` with no bundle named (or `--bundle-id` as
its `bundle_id`), serialised by the pre-pass's own `serialise` -- no ranking
of its own, held by a test comparing the bytes. `--question` repeats as
before. Every other flag acts on ONE bundle's cut and the server takes none
of them, so over a folder it is REFUSED by name with exit 2
(`consume.FOLDER_FLAGS` is the allowlist), never dropped; `--bundle-id` on a
bundle path is refused the same way. A bundle path reads exactly as before.
- **`okf card <bundle>` and the generic skill are the one-to-many form.** The card is one bundle's identity, concept count,
conditional-field counts and whole-bundle cost as JSON, **DERIVED on every run
and never written into the bundle** -- storing it would move the bytes of all

View file

@ -1412,6 +1412,22 @@ nothing regenerated. Pointed at one bundle, it prints that bundle's card as
before; the command decides which it was given by the same rule discovery uses
(a directory carrying an `index.md` is a bundle).
**And one question -- or several sub-questions -- asks every bundle under the
folder in one call:**
```sh
okf consume ~/okf --question "first sub-question" --question "second sub-question"
okf consume ~/okf --question "..." --bundle-id my-bundle # just one of them
```
The reply is the server's `okf_ask` with no bundle named, byte for byte: the
budget split between the bundles, one payload per bundle, and every excerpt
carrying the id of the bundle it came from. There is no ranking of its own.
The flags that change how ONE bundle is cut (`--ref`, `--ranking`,
`--no-source-quota` and the rest) are refused over a folder, by name, rather
than dropped, because the server takes none of them; point at one bundle to use
them.
`okf skill <bundle> --for-bundle` still writes the per-bundle form, with the
identity and the numbers measured into the text — which is exactly what makes
that file stale the moment the bundle is rebuilt. It refuses out loud when it

View file

@ -3101,11 +3101,27 @@ def serialise(payload: Mapping[str, object]) -> str:
# --- The CLI ------------------------------------------------------------------
def parse_args(argv: list[str] | None) -> argparse.Namespace:
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument("bundle", type=Path, help="the OKF bundle directory to read")
parser.add_argument(
"bundle",
type=Path,
help=(
"the OKF bundle directory to read, or a FOLDER: then every bundle "
"under it is asked in one call, exactly as the server's `okf_ask` "
"asks them, and each answer names its bundle"
),
)
parser.add_argument(
"--bundle-id",
default=None,
help=(
"over a folder, ask only the bundle with this id instead of every "
"one; refused when the path is itself a bundle"
),
)
parser.add_argument(
"--question",
required=True,
@ -3298,7 +3314,70 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace:
"to prevent"
),
)
return parser.parse_args(argv)
return parser
def parse_args(argv: list[str] | None) -> argparse.Namespace:
return _parser().parse_args(argv)
#: The flags the folder door takes. Everything else changes how ONE bundle is
#: cut, and the server that asks a folder takes none of it -- so over a folder
#: such a flag is refused by name, never dropped: a flag silently ignored makes
#: its caller believe in a cut that never happened.
FOLDER_FLAGS = ("--question", "--k", "--limit", "--out", "--bundle-id")
def _refused_over_a_folder(argv: Sequence[str]) -> list[str]:
options = {
option
for action in _parser()._actions
for option in action.option_strings
if option.startswith("--") and option not in (*FOLDER_FLAGS, "--help")
}
return sorted({token.split("=", 1)[0] for token in argv} & options)
def _main_over_a_folder(args: argparse.Namespace, argv: Sequence[str]) -> int:
"""`okf consume <folder>`: the server's `okf_ask`, with its own bytes.
No ranking and no cut of its own. The reply is the one `okf_ask` gives
with no bundle named -- or with `--bundle-id` as its `bundle_id` -- so the
command line and the server cannot come to disagree about an answer.
"""
from . import mcp_server
refused = _refused_over_a_folder(argv)
if refused:
print(
f"okf_consume: FAILED - {', '.join(refused)} act(s) on one bundle's cut; "
f"over a folder the server's own reading is used. Point at one bundle "
f"to use {'them' if len(refused) > 1 else 'it'}",
file=sys.stderr,
)
return 2
arguments: dict[str, object] = {
"questions": list(args.question),
"k": args.k,
"limit": args.limit,
}
if args.bundle_id is not None:
arguments["bundle_id"] = args.bundle_id
try:
surface = mcp_server.build_surface(bundle=None, roots=[args.bundle])
reply = mcp_server.call_ask(surface, arguments)
except mcp_server.ToolError as error:
print(f"okf_consume: FAILED - refused ({error.code}): {error}", file=sys.stderr)
return 1
except OSError as error:
print(f"okf_consume: FAILED - a bundle could not be read: {error}", file=sys.stderr)
return 2
text = serialise(reply)
if args.out is None:
sys.stdout.write(text)
else:
args.out.write_text(text, encoding="utf-8", newline="\n")
return 0
def main(argv: list[str] | None = None) -> int:
@ -3308,10 +3387,20 @@ def main(argv: list[str] | None = None) -> int:
run did not happen. Collapsing 2 into 1 would report an unread bundle as a
failed cut -- two findings with different owners under one number.
"""
args = parse_args(argv)
raw = list(argv) if argv is not None else sys.argv[1:]
args = parse_args(raw)
if not args.bundle.is_dir():
print(f"okf_consume: FAILED - {args.bundle} is not a directory", file=sys.stderr)
return 2
if not (args.bundle / "index.md").is_file():
return _main_over_a_folder(args, raw)
if args.bundle_id is not None:
print(
f"okf_consume: FAILED - --bundle-id names a bundle under a folder, and "
f"{args.bundle} is itself a bundle",
file=sys.stderr,
)
return 2
try:
if len(args.question) > 1:
if args.cost_vocabulary or args.rarity_weight or args.reserve_top_rank:

View file

@ -137,3 +137,80 @@ def test_a_broken_bundle_under_the_folder_is_reported(folder: Path, tmp_path: Pa
assert overview["unreadable"] == [
{"directory": "odelagt", "reason": "index.md declares no bundle_id"}
]
# --- F2: one call across the folder ------------------------------------------
QUESTIONS = ("hvor lenge hever surdeigen", "hvor ofte vannes tomatene")
def _ask(folder: Path, *extra: str) -> subprocess.CompletedProcess[str]:
argv = ["consume", str(folder)]
for question in QUESTIONS:
argv += ["--question", question]
return _okf(*argv, *extra)
def test_one_call_over_a_folder_answers_from_every_bundle(folder: Path) -> None:
run = _ask(folder)
assert run.returncode == 0, run.stderr
reply = json.loads(run.stdout)
assert reply["asked"] == ["bakeri", "hage"]
assert reply["questions"] == list(QUESTIONS)
by_bundle = {answer["bundle_id"]: answer["payload"] for answer in reply["answers"]}
assert set(by_bundle) == {"bakeri", "hage"}
# Every excerpt names the bundle it came from, and it is the right one.
for bundle_id, payload in by_bundle.items():
assert payload["excerpts"], bundle_id
assert {excerpt["bundle_id"] for excerpt in payload["excerpts"]} == {bundle_id}
delivered = {
answer["bundle_id"]: " ".join(excerpt["text"] for excerpt in answer["payload"]["excerpts"])
for answer in reply["answers"]
}
assert "tolv timer" in delivered["bakeri"]
assert "hver morgen" in delivered["hage"]
def test_one_call_over_a_folder_is_the_servers_own_ask(folder: Path) -> None:
"""No ranking of its own: the bytes are `okf_ask`'s with no bundle named."""
reply = json.loads(_ask(folder).stdout)
assert reply == mcp_server.call_ask(_surface(folder), {"questions": list(QUESTIONS)})
def test_naming_one_bundle_under_the_folder_asks_only_that_one(folder: Path) -> None:
reply = json.loads(_ask(folder, "--bundle-id", "hage").stdout)
assert reply["asked"] == ["hage"]
assert reply == mcp_server.call_ask(
_surface(folder), {"questions": list(QUESTIONS), "bundle_id": "hage"}
)
def test_an_unknown_bundle_name_is_refused(folder: Path) -> None:
run = _ask(folder, "--bundle-id", "finnes-ikke")
assert run.returncode == 1
assert "bundle_unknown" in run.stderr
def test_a_reading_flag_the_server_does_not_take_is_refused_over_a_folder(folder: Path) -> None:
"""A flag that would be silently dropped is refused: the folder door reads
exactly as the server reads, and a flag it ignored would make the caller
believe in a cut that never happened."""
run = _ask(folder, "--no-source-quota")
assert run.returncode == 2
assert "--no-source-quota" in run.stderr
def test_bundle_id_on_one_bundle_is_refused(folder: Path) -> None:
run = _okf("consume", str(folder / "hage"), "--question", "vanning", "--bundle-id", "hage")
assert run.returncode == 2
assert "--bundle-id" in run.stderr
def test_one_bundle_is_read_as_before(folder: Path) -> None:
"""Pointed at one bundle, the payload is the single-bundle payload."""
run = _okf("consume", str(folder / "hage"), "--question", "vanning")
assert run.returncode == 0, run.stderr
payload = json.loads(run.stdout)
assert "answers" not in payload
assert payload["bundle"]["bundle_id"] == "hage"