portfolio-optimiser/tests/test_read_bundle_cost_loadbearing.py
Kjell Tore Guttormsen baa6f450e8 feat(explore): stigen faar sitt manglende trinn - read_bundle gir ETT nivaa, read_dir det neste
S7a-3 pkt. 2. MAJOR-3 bygde read_bundle om fra HELE basen til en oppfoering per
konseptfil. Saa kom det foerste ekte korpuset: K2 navigerer til 629 konsepter bak
478 nestede indekser, og en listing av 629 koster 42 761 o200k-tokens som rir i
7 av 12 prompter = 89 % av alle prompt-tokens. Bindingen holdt asymptotisk og
priset likevel hele korpuset. De 478 indeksene ble bygget, konsumert og flatet ut
- agenten saa 629 soesken og fikk aldri vite at korpuset hadde en form.

MAALT (BEFORE og AFTER i samme oekt, samme kode, BEFORE som mutasjon):
  read_bundle-nyttelast  110 581 tegn / 42 761 tok  ->  3 954 tegn / 1 495 tok
  listing-tokens totalt         307 573 (89 %)      ->  12 595 (26 %)
  prompt-tokens i kjoeringen         343 826        ->  49 225   (-86 %)
BEFORE reproduserer S7a-2s publiserte tall til 0,03 % - kjent-positiv kontroll
paa instrumentet, som ogsaa maatte rettes (resultatet baerer name=None, saa en
sonde nøklet paa verktoeynavn rapporterer 0 kopier og leses som en ekte null).

- okf.directory_listing er ENESTE renderer; begge verktoey ER den paa hvert sitt
  nivaa. Kataloger utledes av STIER, aldri av index.md. Bygget av context_files,
  ALDRI files. Hver sti er bundle-relativ, brukbar ordrett i neste kall.
- Ukjent sti NEKTES ved navn (BundlePathNotFound) - en tom listing er umulig aa
  skille fra en katalog som finnes og er tom.
- Verktoeybeskrivelsene og navigatoerinstruksjonen flyttet i SAMME commit.

PREMISS FELT FOER BYGGING: context_files har aldri holdt hierarkiet tilbake -
navnene er fulle bundle-relative stier; det var RENDERINGEN som flatet det ut.
Derfor er bundle_context og begge nav-goldenene byte-identiske, gratis.

AVVIK fra ordren, uttalt: K2 kan ikke vaere testavhengighet (utenfor repoet), og
1 500 tegn er ikke oppnaaelig for en rot med 39 identifiserbare oppfoeringer
(maalt 3 954). Gaten binder 1 500 tegn per listing over basene den KAN se, pluss
egenskapen, med en FLAT kontroll over 5x taket.

Load-bearing MAALT: 9 mutasjoner alle roede mot HELE suiten, groenn kontroll
1252 passed / 5 skipped, golden byte-uendret. N1 4 / N2 7 / N3 12 / N4 5 / N5 1 /
N6 6 / N7 1 / N8 2 / N9 1.

Maaling: docs/2026-09-03-hierarkisk-navigasjon-k2.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 07:38:30 +02:00

221 lines
11 KiB
Python

"""``read_bundle`` costs O(DOCUMENTS in one base), never O(bytes of that base).
MAJOR-3 (``docs/2026-09-02-misjonsreview-v2.md`` § 7), measured before anything was changed in
``docs/2026-09-02-read-bundle-kontekstkostnad.md``:
read_bundle payload bygg 3 861 / veglys 10 406 / tunnel 12 595 o200k_base tokens
prompts one result rides in FIVE (navigator 1, manager 3, hypothesiser 1)
share of every prompt-token 54 % / 59 % / 59 % of one CLI ``--explore`` run
``read_bundle`` returned ``okf.bundle_context`` — the WHOLE navigated base. One call turned the
base into a ``function_result``, and because the exploration's participants share one conversation
history that result rides in **every later prompt**, at full price, without anyone asking for it
again. That is the same shape ``list_bundles`` was measured in and rebuilt out of in session 65
(``tests/test_catalogue_cost_loadbearing.py``), one rung down the ladder: the catalogue answers
*which bases exist*, ``read_bundle`` answers *what is in this one*, and ``read_file`` answers *what
does this document say*. Only the last of the three should cost what a document costs.
So ``read_bundle`` now returns the CATALOGUE FORM: one entry per concept document — ``name``,
``type``, ``title``, ``chars`` — and ``read_file(id, name)`` is the next rung. This is a disclosure
level, not data loss: every byte is still exactly one call away, and a navigator now pays for the
documents it chose to open instead of for the ones it did not.
**A premise felled before it was built on** (see the measurement doc § 2): "the index body is the
base's own navigation prose, so it belongs here". The tunnel base's root index is **4 763 chars
alone ≈ 1 400 tokens** — nearly the entire ceiling, for a field ``list_bundles`` already excerpts
and ``read_file(id, "index.md")`` still returns whole.
**The ceiling lives in THIS FILE, not in ``explore.py``** — the catalogue gate's rule, for the
catalogue gate's reason: a test that imported the implementation's budget would move with it, and
raising the budget is precisely the regression this file exists to catch.
**DEVIATION from the order, stated:** the order words the gate as "< 1 500 o200k-tokens". This
file bounds CHARACTERS instead. ``tiktoken`` is not a project dependency (and adding one for a
gate would be a bigger decision than the gate), and a gate that skips when an optional package is
missing is a gate that can be silently absent. The character ceiling is a proxy whose conversion
was MEASURED rather than assumed: the new payload over the real tunnel base is **748 chars / 259
o200k tokens** (2.89 chars/token for this Norwegian markdown), so 1 500 characters is ≈ 520 tokens
— comfortably inside the order's criterion, and twice the measured payload, so ordinary field
growth does not force a rewrite. The order's own criterion is verified directly, once, by the
instrument in the measurement doc.
**AMENDED 2026-09-03 (S7a-3 pkt. 2), and the amendment is one level, not a rewrite.** The listing
this file bounds is now the base's TOP LEVEL rather than every document in it: on K2 the flat form
still cost 42 761 o200k tokens across 629 documents, because O(documents in the base) is only cheap
while the base is small. ``read_dir`` is the rung that was missing. Every arm below still holds over
the flat example bases -- a flat base has no subdirectories, so its document list is unchanged, byte
for byte -- and the hierarchy's own gate is ``tests/test_hierarchical_navigation_loadbearing.py``.
``_documents()`` exists because ``len()`` of the payload is now the number of KEYS.
What the arms pin, and what each one refuses:
(a) the bound itself, over the REAL base the order names — refuses the unbounded form. A synthetic
fixture here would measure the fixture writer, not the base;
(b) cost tracks DOCUMENT COUNT, not document SIZE — ten times the prose, the same price. That is
the property stated directly rather than inferred from (a);
(c) the anti-vacuity arm: every concept document is still IDENTIFIED, with all four fields, and the
entry count equals the navigated context files. Without this, "return an empty list" passes (a)
perfectly and hands the navigator nothing to choose between — the repo's vacuous-gate class,
which has now bitten twelve times;
(d) the ``type: verdict`` layer stays EXCLUDED. ``read_bundle`` built from ``bundle.files`` instead
of ``bundle.context_files`` would route prior verdicts into a hypothesis prompt around the
gated ExpeL fold (målbilde §4), and nothing else in the suite would notice;
(e) the ladder is intact — ``read_file`` still returns the COMPLETE document, so the bound is a
disclosure level, not data loss;
(f) the CONTROL the order names: ONE concept document alone blows the ceiling for the whole
listing. A green (a) then means the bound fired, not that the base was small
(Verifiseringsloven face 4: a gate that can only pass proves nothing).
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from portfolio_optimiser import okf
from portfolio_optimiser.explore import navigator_tools
_EXAMPLES = Path(__file__).resolve().parents[1] / "shared" / "examples"
#: The base the order names. Measured, not chosen: the largest of the three example bases.
_TUNNEL = _EXAMPLES / "tunnel-hauglia"
#: Characters one ``read_bundle`` listing may cost. Test-owned on purpose — see the docstring, and
#: the measured chars/token conversion that ties it to the order's token criterion.
_CEILING_CHARS = 1_500
def _tools(bundle_dir: Path) -> dict[str, Any]:
return {t.name: t for t in navigator_tools((str(bundle_dir),))}
def _listing(bundle_dir: Path) -> dict[str, Any]:
"""The WHOLE payload one ``read_bundle`` costs — since S7a-3 that is one LEVEL of the base
(subdirectories plus the documents at this one), not every document in it."""
return _tools(bundle_dir)["read_bundle"].func(bundle_id=bundle_dir.name)
def _documents(bundle_dir: Path) -> list[dict[str, Any]]:
"""The document half alone. A separate helper because ``len()`` of the payload is the number of
KEYS — three, always — and an arm counting entries through it would be vacuously green."""
return list(_listing(bundle_dir)["documents"])
def _blob(payload: object) -> str:
return json.dumps(payload, ensure_ascii=False)
def _write_base(
root: Path, name: str, *, body_chars: int, concepts: int = 3, verdicts: int = 0
) -> Path:
base = root / name
base.mkdir(parents=True)
lines = [f"# {name}", ""]
for i in range(concepts):
lines.append(f"- [konsept-{i}](konsept-{i}.md)")
(base / f"konsept-{i}.md").write_text(
f"---\ntype: concept\ntitle: Konsept {i}\n---\n\n" + ("innhold. " * (body_chars // 9)),
encoding="utf-8",
)
for i in range(verdicts):
lines.append(f"- [dom-{i}](dom-{i}.md)")
(base / f"dom-{i}.md").write_text(
f"---\ntype: verdict\ntitle: Dom {i}\n---\n\nEksperten godkjente tiltaket.\n",
encoding="utf-8",
)
(base / "index.md").write_text(
"---\ntype: index\n---\n\n" + "\n".join(lines) + "\n", encoding="utf-8"
)
return base
def test_read_bundle_over_the_real_tunnel_base_is_bounded() -> None:
"""(a) The headline, over the base the order names — not a fixture of my own making."""
blob = _blob(_listing(_TUNNEL))
assert len(blob) <= _CEILING_CHARS, (
f"read_bundle over {_TUNNEL.name} costs {len(blob)} chars, over the ceiling "
f"{_CEILING_CHARS}; it used to be 39 583 (12 595 o200k tokens), riding in five prompts"
)
def test_read_bundle_cost_does_not_track_document_size(tmp_path: Path) -> None:
"""(b) Ten times the prose, the same price. O(documents), not O(bytes)."""
small = _write_base(tmp_path, "small", body_chars=500)
large = _write_base(tmp_path, "large", body_chars=5_000)
small_entries, large_entries = _documents(small), _documents(large)
# Same document COUNT, same number of entries — and the only field that grew is the honest,
# logarithmic ``chars`` digit, so the payloads differ by a handful of characters at most.
assert len(small_entries) == len(large_entries) == 3
assert abs(len(_blob(large_entries)) - len(_blob(small_entries))) < 20, (
"ten times the body must not cost ten times the listing"
)
assert len(_blob(large_entries)) <= _CEILING_CHARS
def test_the_listing_still_identifies_every_document(tmp_path: Path) -> None:
"""(c) The anti-vacuity arm: bounded is not the same as empty.
Bounded-and-useless passes (a) perfectly. What a navigator needs in order to choose a document
is what it IS (``type``), what it is CALLED (``title``), what to ask for (``name``) and what it
will cost (``chars``) — so all four are asserted, and the entry count is tied to the navigated
context files rather than to a number written here.
"""
bundle = okf.navigate_bundle(str(_TUNNEL))
entries = _documents(_TUNNEL)
assert len(entries) == len(bundle.context_files) > 0, (
"a listing that omits documents is a base the navigator cannot fully see"
)
by_name = {str(e["name"]): e for e in entries}
for f in bundle.context_files:
entry = by_name[f.name]
assert entry["type"] == (f.type or "document")
assert entry["title"] == okf.unquote_scalar(f.frontmatter.get("title", f.name))
assert entry["chars"] == len(f.body)
assert str(entry["title"]).strip(), "an untitled entry cannot be chosen between"
def test_the_verdict_layer_is_still_excluded(tmp_path: Path) -> None:
"""(d) Prior verdicts reach a hypothesis ONLY through the gated ExpeL fold (målbilde §4).
The old body returned ``okf.bundle_context``, which excludes ``type: verdict`` by construction.
A listing built from ``bundle.files`` instead of ``bundle.context_files`` would put them back
in front of the navigator — around the gate — and every other arm here would stay green.
"""
base = _write_base(tmp_path, "med-dommer", body_chars=200, concepts=2, verdicts=2)
entries = _documents(base)
assert len(entries) == 2, f"the verdict layer must not be listed as context: {entries!r}"
assert not [e for e in entries if e["type"] == "verdict"]
assert not [e for e in entries if str(e["name"]).startswith("dom-")]
# The control that proves the fixture actually HAS verdicts to leak — without it this arm is
# green against a bundle that simply carries none.
assert len(okf.navigate_bundle(str(base)).verdicts) == 2
def test_the_whole_document_is_still_one_call_away() -> None:
"""(e) The bound is a disclosure LEVEL, not data loss."""
entries = _documents(_TUNNEL)
biggest = max(entries, key=lambda e: int(e["chars"]))
whole = _tools(_TUNNEL)["read_file"].func(bundle_id=_TUNNEL.name, path=str(biggest["name"]))
assert len(whole) > _CEILING_CHARS, "read_file must still return the document, not a summary"
assert str(biggest["chars"]) != "0" and int(biggest["chars"]) <= len(whole)
def test_control_one_document_alone_would_blow_the_ceiling() -> None:
"""(f) The ceiling discriminates — proved, not assumed. The order names this control."""
bundle = okf.navigate_bundle(str(_TUNNEL))
biggest = max(len(f.body) for f in bundle.context_files)
assert biggest > _CEILING_CHARS, (
f"the largest document in {_TUNNEL.name} is {biggest} chars; a ceiling it does not exceed "
"would be a ceiling this base could pass while carrying everything"
)