feat(mcp): the bundle's map, and a working method that reads it first
C5. `bundlemap.build_map` lists a bundle in its own words: one line per source document -- its name, then the titles of its concepts in document order -- and documents whose names differ only in their numbers (a changelog per release, a note per week) as ONE line: the name with every number as `#`, the count, the first and last by natural order, and the titles across the series that are words. `SERIES_MIN` = 5, at most `TITLES_PER_LINE` = 24 titles a line, the lines capped at `MAP_MAX_BYTES` = 48 000 together with `lines_truncated` counting the rest. Derived on every call, never stored. The card (`okf card`, `okf_describe`) carries it as `map` and no longer carries `source_files`: that list named every document a second time with no series collapsed, a quarter of the reply on a large bundle, for names the map already carries. Chose removal over keeping both because the describe reply has to fit a client's tool-reply limit and the map says more. The working method now reads: take the map first (`okf card`, or `okf_describe`), write two to four sub-questions in its words, and send them in ONE call (`--question` repeated, or `okf_ask` `questions`). Changed in the skill template, the generated `skills/okf-consume`, and the server instructions (held under the 2 KB a client keeps). The regeneration recipe for `skills/okf-consume` gains `--for-bundle`: since v1.1 the generator writes the generic skill by default, so the recipe as published produced the other file. A test holds a four-sub-question `okf_ask` over concepts far over the passage size under 50 000 bytes of reply text (25 000 tokens at a pessimistic two bytes a token). The real-collection measurements are kept in local state. Suite on a clean tree after `git add`: 2429 passed, 2 skipped, 4 xfailed. ruff, ruff format, mypy --strict clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f7cd84c5e6
commit
da6faf8776
9 changed files with 450 additions and 64 deletions
153
tests/test_bundle_map.py
Normal file
153
tests/test_bundle_map.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
"""The map a reader writes sub-questions from (v1.1 order C, C5).
|
||||
|
||||
A question is best put in the collection's OWN words, and the one place those
|
||||
words are listed is the collection itself. `bundlemap.build_map` lists them:
|
||||
one line per source document -- its name, then the titles of its concepts in
|
||||
document order -- and a SERIES of documents whose names differ only in their
|
||||
numbers (a changelog per release, a note per week) as ONE line with the span,
|
||||
because four hundred lines saying the same thing crowd out the rest.
|
||||
|
||||
`okf card` and `okf_describe` carry it as `map`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from llm_ingestion_okf import bundlemap, consume, mcp_server
|
||||
|
||||
TOOLS = Path(__file__).resolve().parent.parent / "tools"
|
||||
if str(TOOLS) not in sys.path:
|
||||
sys.path.insert(0, str(TOOLS))
|
||||
|
||||
import okf_retrieval_gate as retrieval # noqa: E402
|
||||
|
||||
|
||||
def _doc(name: str, *titles: str) -> retrieval.DocumentSpec:
|
||||
return retrieval.DocumentSpec(
|
||||
name,
|
||||
f"{name}.md",
|
||||
tuple(
|
||||
retrieval.ConceptSpec(slug=f"s{position}", title=title, body=f"About {title}.")
|
||||
for position, title in enumerate(titles, start=1)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
RELEASES = tuple(_doc(f"changes-1-{minor}", f"1.{minor}") for minor in range(2, 14))
|
||||
WEEKLY = tuple(
|
||||
_doc(f"notes-2026-w{week}", "Highlights", f"Week {week} fixes") for week in (1, 2, 3, 4, 5)
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def bundle(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
||||
spec = retrieval.BundleSpec(
|
||||
"map-synthetic",
|
||||
(
|
||||
_doc("guide-setup", "Setup", "Install the tool", "Configure a project"),
|
||||
_doc("guide-hooks", "Hooks", "Hook events", "Hook events"),
|
||||
*RELEASES,
|
||||
*WEEKLY,
|
||||
),
|
||||
)
|
||||
return retrieval.build_bundle(tmp_path_factory.mktemp("map") / "bundle", spec)
|
||||
|
||||
|
||||
def _map(bundle: Path) -> dict[str, object]:
|
||||
return bundlemap.bundle_map(bundle, profile=consume.DEFAULT_PROFILE)
|
||||
|
||||
|
||||
def test_one_line_per_document_with_its_own_titles_in_order(bundle: Path) -> None:
|
||||
lines = _map(bundle)["lines"]
|
||||
assert isinstance(lines, list)
|
||||
assert "guide-setup: Setup · Install the tool · Configure a project" in lines
|
||||
# A title the document repeats is listed once.
|
||||
assert "guide-hooks: Hooks · Hook events" in lines
|
||||
|
||||
|
||||
def test_a_series_is_one_line_with_its_span(bundle: Path) -> None:
|
||||
lines = _map(bundle)["lines"]
|
||||
assert isinstance(lines, list)
|
||||
series = [line for line in lines if line.startswith("changes-#-#")]
|
||||
assert series == ["changes-#-# (12 documents: changes-1-2 … changes-1-13)"]
|
||||
assert not any(line.startswith("changes-1-") for line in lines)
|
||||
|
||||
|
||||
def test_a_series_keeps_the_titles_that_are_words(bundle: Path) -> None:
|
||||
lines = _map(bundle)["lines"]
|
||||
assert isinstance(lines, list)
|
||||
(line,) = [line for line in lines if line.startswith("notes-#-w#")]
|
||||
assert line.startswith("notes-#-w# (5 documents: notes-2026-w1 … notes-2026-w5): Highlights")
|
||||
assert "Week 1 fixes" in line
|
||||
|
||||
|
||||
def test_the_map_states_its_denominators(bundle: Path) -> None:
|
||||
built = _map(bundle)
|
||||
assert built["documents"] == 2 + 12 + 5
|
||||
assert built["concepts"] == 3 + 3 + 12 + 10
|
||||
assert built["lines_count"] == 4
|
||||
|
||||
|
||||
def test_a_long_document_is_cut_and_says_so(tmp_path: Path) -> None:
|
||||
titles = [f"Section {n}" for n in range(bundlemap.TITLES_PER_LINE + 5)]
|
||||
spec = retrieval.BundleSpec("long", (_doc("big", *titles),))
|
||||
bundle = retrieval.build_bundle(tmp_path / "bundle", spec)
|
||||
(line,) = _map(bundle)["lines"] # type: ignore[misc]
|
||||
assert line.endswith("· (+5 more)")
|
||||
assert line.count(" · ") == bundlemap.TITLES_PER_LINE
|
||||
|
||||
|
||||
def test_the_map_is_deterministic(bundle: Path) -> None:
|
||||
assert json.dumps(_map(bundle)) == json.dumps(_map(bundle))
|
||||
|
||||
|
||||
def test_describe_and_the_card_carry_the_map(bundle: Path) -> None:
|
||||
surface = mcp_server.build_surface(bundle=bundle, roots=())
|
||||
described = mcp_server.call_describe(surface, {})
|
||||
assert described["map"] == _map(bundle)
|
||||
assert mcp_server.card(bundle, profile=consume.DEFAULT_PROFILE)["map"] == _map(bundle)
|
||||
|
||||
|
||||
def test_the_card_names_documents_through_the_map_alone(bundle: Path) -> None:
|
||||
"""`source_files` listed every document a second time, one name per line
|
||||
and no series collapsed; the map names every document or series already."""
|
||||
assert "source_files" not in mcp_server.card(bundle, profile=consume.DEFAULT_PROFILE)
|
||||
|
||||
|
||||
def test_the_working_method_is_map_first_then_one_call() -> None:
|
||||
from llm_ingestion_okf import skill as okf_skill
|
||||
|
||||
generic = okf_skill.render_generic()
|
||||
assert "`map`" in generic
|
||||
assert "ONE run" in generic
|
||||
# The example command itself carries more than one sub-question.
|
||||
command = next(
|
||||
line
|
||||
for line in generic.splitlines()
|
||||
if line.startswith("okf consume ") and "sub-question" in line
|
||||
)
|
||||
assert command.count("--question ") >= 2
|
||||
instructions = mcp_server.SERVER_INSTRUCTIONS
|
||||
assert "map" in instructions
|
||||
assert "`questions`" in instructions
|
||||
assert "ONE call" in instructions
|
||||
assert len(instructions.encode("utf-8")) <= 2048
|
||||
|
||||
|
||||
def test_the_map_has_a_ceiling_and_says_what_it_left_out(
|
||||
bundle: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
whole = _map(bundle)
|
||||
assert whole["lines_truncated"] == 0
|
||||
lines = whole["lines"]
|
||||
assert isinstance(lines, list)
|
||||
monkeypatch.setattr(bundlemap, "MAP_MAX_BYTES", len(lines[0].encode("utf-8")) + 1)
|
||||
cut = _map(bundle)
|
||||
assert cut["lines"] == lines[:1]
|
||||
assert cut["lines_truncated"] == len(lines) - 1
|
||||
assert cut["lines_count"] == len(lines)
|
||||
Loading…
Add table
Add a link
Reference in a new issue