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:
parent
df83c65e32
commit
718c064279
4 changed files with 194 additions and 4 deletions
|
|
@ -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
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue