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>
This commit is contained in:
parent
baae7507a9
commit
baa6f450e8
8 changed files with 584 additions and 40 deletions
48
CLAUDE.md
48
CLAUDE.md
|
|
@ -1529,6 +1529,54 @@ Python ≥3.10. MAF (`agent-framework-core` 1.16.0, `-orchestrations` 1.1.1 —
|
|||
rekkefølge, urørt); og dispatcherens «to baser deler id»-nekt er NYLIG NÅBAR — den krevde før to
|
||||
monteringer med samme basenavn, nå kolliderer to ULIKT navngitte kataloger som erklærer samme id.
|
||||
Kontrakt: `docs/okf-konsum-kontrakter.md § 3.1`.
|
||||
- **Stigen har tre trinn: `list_bundles` → `read_bundle` → `read_dir` → `read_file` (S7a-3 pkt. 2,
|
||||
økt 81):** MAJOR-3 bygde `read_bundle` om fra HELE basen til én oppføring per konseptfil, og på de
|
||||
tre eksempelbasene var det −77…91 %. Så kom det første 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, fordi prisen på å finne ut HVA en base inneholder var satt av hvor mye den
|
||||
inneholder. De 478 indeksene ble dessuten BYGGET av navigasjonen, KONSUMERT av den og så flatet
|
||||
ut — agenten så 629 søsken og fikk aldri vite at korpuset hadde en form. **MÅLT ETTER:** rotnivået
|
||||
er **3 954 tegn / 1 495 tok**, listing-tokens **307 573 → 12 595**, og utforskningens
|
||||
prompt-tokens **343 826 → 49 225 (−86 %)**. BEFORE og AFTER er kjørt i SAMME økt med samme kode
|
||||
(BEFORE er en mutasjon), og BEFORE reproduserer S7a-2s publiserte tall til 0,03 % — den
|
||||
kjent-positive kontrollen på instrumentet. **`okf.directory_listing` er ENESTE renderer** og
|
||||
begge verktøy ER den på hvert sitt nivå; to kopier av en listing-regel ville drevet til to svar om
|
||||
én base (kø-(p)). **ET PREMISS I ORDREN BLE FELT FØR NOE BLE BYGGET PÅ DET:** `context_files` har
|
||||
ALDRI holdt hierarkiet tilbake — hver `BundleFile.name` er allerede den fulle bundle-relative
|
||||
stien, så treet var utledbart fra navnene; det var RENDERINGEN som flatet det ut. Dét er grunnen
|
||||
til at `bundle_context` og begge nav-goldenene er BYTE-IDENTISKE etter endringen (ordrens eget
|
||||
krav), gratis og ikke av forsiktighet. **Kataloger utledes av STIER, aldri av `index.md`** (en
|
||||
nestet indeks er navigasjon, ikke innhold), og listingen bygges av `context_files`, ALDRI `files`
|
||||
— en katalog-TELLING fra `files` ville reklamert for dokumenter navigatøren ikke får se (N2 → 7
|
||||
røde). **Hver sti er BUNDLE-RELATIV, altså brukbar ordrett som neste kalls argument** — en
|
||||
nivå-relativ form måtte komponeres av en modell, og en sti som aldri fantes er verre enn ingen
|
||||
sti (`_index_excerpt`-regelen, ett trinn opp; N7 → 1 rød). `documents` på en katalog-oppføring er
|
||||
antallet i hele SUBTREET — hva subtreet holder, ikke hva ett `read_dir` returnerer — og
|
||||
beskrivelsen sier hvilken av de to det er. Ukjent sti NEKTES ved navn (`BundlePathNotFound`,
|
||||
`ValueError`-subklasse som `BundleIdMismatch`): en tom listing er umulig å skille fra en katalog
|
||||
som finnes og er tom (N5 → 1 rød). **Verktøybeskrivelsene og navigatørens instruksjon flyttet i
|
||||
SAMME commit** — en beskrivelse som lyver om kroppen ER modellens instruks (Fase 3-klassen; N9 →
|
||||
1 rød). **TO EKSISTERENDE ASSERTS VILLE BLITT VAKUØSE I STILLHET** og er styrket: `len(payload)`
|
||||
er nå antallet NØKLER (tre, alltid), så `test_read_bundle_cost_loadbearing`s størrelses-arm talte
|
||||
nøkler, og write-frihets-armen i `test_explore_loadbearing` kalte ikke det nye verktøyet i det
|
||||
hele tatt. **AVVIK fra ordren, uttalt og målt:** ordren binder «K2 < 1 500 tegn» — K2 kan ikke
|
||||
være en testavhengighet (utenfor repoet; MAJOR-3-gaten har samme begrensning), og tallet er ikke
|
||||
oppnåelig: rota bærer 39 identifiserbare oppføringer og måler 3 954 tegn. Gaten binder derfor
|
||||
1 500 tegn per listing over basene den KAN se, pluss egenskapen som ga fallet (kostnaden følger
|
||||
oppføringer på ETT nivå, ikke dokumenter i basen), med en FLAT kontroll over 5× taket — uten den
|
||||
ville en grønn binding like gjerne betydd at fixturen var liten. Load-bearing MÅLT
|
||||
(`tests/test_hierarchical_navigation_loadbearing.py`, 9 armer), **ni mutasjoner alle røde mot HELE
|
||||
suiten** + grønn kontroll **1252/5** og golden BYTE-UENDRET (`shasum -a 1` =
|
||||
`ea8c534773acdbe41ae68f2c55724d69aaf8be4f`): N1 reverter sømmen (4) · N2 bygg fra `files` (7) ·
|
||||
N3 bundet men VAKUØST tom (12) · N4 dropp `chars` (5) · N5 ukjent sti blir en tom listing (1) ·
|
||||
N6 `read_dir` ikke registrert (6) · N7 nivå-relative navn (1) · N8 katalog-tellingen konstant (2)
|
||||
· N9 instruksjonen beskriver den gamle stigen (1). **Ærlighets-grenser, uttalt:** dette er ikke
|
||||
«−86 % for enhver kjøring» (en navigatør som stiger ned *k* nivåer betaler *k* listinger); at en
|
||||
LEVENDE modell navigerer bedre med en struktur enn med 629 flate dokumenter er IKKE bevist
|
||||
(structured-output-grensens klasse); multiplikatoren gjelder dette manuset; og `navigate_bundle`
|
||||
kalles fortsatt per verktøykall (I/O, ikke tokens — ikke målt). Måling:
|
||||
`docs/2026-09-03-hierarkisk-navigasjon-k2.md`.
|
||||
- **STATE.md er local-only** (gitignored). Voyage session-state er efemert; STATE.md er kanonisk kontinuitet.
|
||||
- Prosess: Voyage-plugin (`/trekbrief → /trekplan → /trekexecute → /trekreview`) per større fase.
|
||||
|
||||
|
|
|
|||
103
docs/2026-09-03-hierarkisk-navigasjon-k2.md
Normal file
103
docs/2026-09-03-hierarkisk-navigasjon-k2.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# Hierarkisk navigasjon — målt på den ekte K2-bundelen
|
||||
|
||||
**Dato:** 2026-09-03 · **Ordre:** `20260903T034323Z-7704222791` punkt 2 (S7a-3) · **Økt 81**
|
||||
**Base:** `~/corpora/okf-telling-20260829/K2-bundle-20260903` — 629 konsepter, 478 nestede
|
||||
indekser, 1 108 filer på disk. Åpnet **slik den er levert** (mulig først etter punkt 1 i samme
|
||||
ordre; før det nektet `BundleIdMismatch` fordi katalogen heter noe annet enn den erklærte id-en).
|
||||
|
||||
## 1. Hva som ble målt, og med hvilket instrument
|
||||
|
||||
Instrumentet er S7a-2s rettede sonde: hver prompt telles som **tekst + `function_call` +
|
||||
`function_result`**, aldri `Message.text` alene (som måler en kontekstbærende prompt til noen få
|
||||
tegn). Tokenisering: `o200k_base` (`tiktoken`), som er en utviklingsavhengighet i målingen og
|
||||
**ikke** i pakka.
|
||||
|
||||
**Instrumentfeil funnet og rettet under målingen** (samme klasse som S7a-2s midtskive-sonde):
|
||||
verktøyresultatet bærer `name=None` på transporten, så en sonde som nøkler på
|
||||
`result:read_bundle` rapporterer **0 kopier** — som er umulig å skille fra en ekte null.
|
||||
Sonden nøkler nå på resultatets eget INNHOLD (`"directories"`), og BEFORE-kjøringen reproduserer
|
||||
S7a-2s publiserte tall (307 573 mot 307 496 tokens, 0,03 % avvik), som er den kjent-positive
|
||||
kontrollen på at instrumentet kan finne det det leter etter.
|
||||
|
||||
BEFORE og AFTER er kjørt **i samme økt med samme kode**, der BEFORE er en mutasjon som gir
|
||||
`read_bundle` den flate MAJOR-3-formen tilbake (mutasjons-harness-idiomet), ikke en historisk
|
||||
kjøring hentet fra en rapport.
|
||||
|
||||
## 2. Resultat
|
||||
|
||||
| | BEFORE (flat listing) | AFTER (ett nivå) |
|
||||
|---|---|---|
|
||||
| `read_bundle`-nyttelast alene | **110 581 tegn / 42 761 tok** (629 oppføringer) | **3 954 tegn / 1 495 tok** (28 kataloger + 11 dokumenter) |
|
||||
| Kopier i prompter | 7 | 8 (rot) + 7 (én `read_dir`) |
|
||||
| Listing-tokens totalt | **307 573** | **12 595** |
|
||||
| Andel av alle prompt-tokens | **89 %** | **26 %** |
|
||||
| Prompt-tokens i hele kjøringen | **343 826** | **49 225** |
|
||||
| Antall prompter | 12 | 13 |
|
||||
|
||||
**Utforskningens prompt-tokens faller 86 %** (343 826 → 49 225). `read_bundle`-nyttelasten alene
|
||||
faller **96,5 %** (42 761 → 1 495 tokens).
|
||||
|
||||
**Den 13. prompten er en ærlig kostnad, ikke støy:** navigatøren tar nå ett steg til (`read_dir`
|
||||
ned i prisskjema-katalogen) før den leser et dokument. Stigen har fått et trinn, og et trinn
|
||||
koster en tur.
|
||||
|
||||
## 3. Premisset som ble felt før noe ble bygget på det
|
||||
|
||||
Ordren sier at `context_files` «ikke lenger skal forkaste hierarkiet». **Den har aldri holdt det
|
||||
tilbake.** Hver `BundleFile.name` er allerede den fulle bundle-relative posix-stien
|
||||
(`del-ii-bilag-7-prisskjema/prissammenstilling-sheet-1.md`), så treet var alltid utledbart fra
|
||||
navnene alene. Det som flatet det ut var **renderingen**.
|
||||
|
||||
Det er ikke pedanteri — det er grunnen til at `okf.bundle_context` og begge nav-goldenene er
|
||||
**byte-identiske** etter endringen, som ordren krever eksplisitt. Egenskapen kom gratis, ikke av
|
||||
forsiktighet.
|
||||
|
||||
Kataloger utledes derfor av STIER, aldri av `index.md`-filer: en nestet indeks er navigasjon, ikke
|
||||
innhold (`context_files` dropper den på hvert nivå, og må fortsette med det, ellers følger
|
||||
dom-laget med). Utledning fra navn betyr også at en katalog nådd via en lenke som hoppet over
|
||||
indeksen fortsatt er synlig.
|
||||
|
||||
## 4. Avvik fra ordrens ordlyd, målt
|
||||
|
||||
Ordren formulerer taket som «`read_bundle` over K2-bundelen < 1 500 tegn». **Begge halvdeler av den
|
||||
setningen holder ikke, og begge er målt:**
|
||||
|
||||
1. **K2 kan ikke være en testavhengighet.** Basen ligger utenfor repoet (`~/corpora/`). MAJOR-3-gaten
|
||||
hadde samme begrensning og løste den likt: gaten binder den største basen som faktisk shippes,
|
||||
og K2-tallene bor i en rapport. Gaten her binder derfor `shared/examples/tunnel-hauglia` (flat),
|
||||
`shared/examples/nav-golden-hierarchy/bundle` (nestet) og en syntetisk base på 242 konsepter bak
|
||||
20 kataloger — der den FLATE formen er over 5× taket den nivå-formen ligger godt innenfor.
|
||||
2. **1 500 tegn er ikke oppnåelig for K2s rotnivå, og skal ikke være det.** Rota bærer **39
|
||||
identifiserbare oppføringer** (28 kataloger + 11 toppdokumenter) og måler **3 954 tegn ≈ 1 366
|
||||
o200k-tokens** (2,89 tegn/token). Å presse den under 1 500 tegn ville krevd at titler eller
|
||||
oppføringer ble droppet — repoets vakuøs-gate-klasse. Taket i testen er 1 500 TEGN per listing
|
||||
over de basene gaten kan se, og egenskapen som produserte fallet er gatet direkte (kostnaden
|
||||
følger oppføringer på ETT nivå, ikke dokumenter i basen).
|
||||
|
||||
## 5. Ærlighets-grenser
|
||||
|
||||
- **Dette er ikke «−86 % kostnad» for enhver kjøring.** En navigatør som stiger ned *k* nivåer
|
||||
betaler *k* listinger; gevinsten er at den betaler for det den VALGTE, og at hver listing rir fra
|
||||
SITT kall og framover.
|
||||
- **At en LEVENDE modell navigerer BEDRE med en katalogstruktur enn med 629 flate dokumenter er
|
||||
IKKE bevist** (samme klasse som structured-output-grensen). Manuset er skriptet.
|
||||
- **Multiplikatoren gjelder dette manuset.** 8 og 7 kopier er hva denne kontrakten og dette
|
||||
manuset ga; en annen kontrakt gir andre tall.
|
||||
- **`navigate_bundle` kalles fortsatt per verktøykall** — I/O og veggklokke, ikke tokens; ikke målt
|
||||
her (samme grense som katalogmålingen i økt 65).
|
||||
- **Debatten er urørt.** `okf.bundle_context` er byte-identisk, og demo-transkriptet er
|
||||
byte-uendret (`shasum -a 1` = `ea8c534773acdbe41ae68f2c55724d69aaf8be4f`).
|
||||
|
||||
## 6. Kommandoer
|
||||
|
||||
```sh
|
||||
# AFTER
|
||||
uv run --with tiktoken python scratchpad/s7a3/k2_profile.py after
|
||||
# BEFORE (patcher kilden, kjører, gjenoppretter)
|
||||
uv run python scratchpad/s7a3/k2_before.py
|
||||
# gatens egne tall
|
||||
uv run python scratchpad/s7a3/measure_sizes.py
|
||||
```
|
||||
|
||||
Kontroll ved slutten av punkt 2: **1 252 passed / 5 skipped**, `ruff` og `mypy` rene, golden
|
||||
byte-uendret. Ni mutasjoner, alle røde mot HELE suiten (N1–N9).
|
||||
|
|
@ -193,10 +193,12 @@ HYPOTHESIS_MARKER: Final = "HYPOTHESIS:"
|
|||
|
||||
_INSTRUCTIONS: Final = {
|
||||
NAVIGATOR_ROLE: (
|
||||
"You read the project's knowledge bases. Use list_bundles to see what exists, "
|
||||
"read_bundle to list the documents inside ONE of them, and read_file to read a document "
|
||||
"you picked from that list. read_bundle returns a LISTING, never the documents — quote "
|
||||
"only what read_file gave you, and never guess at content you have not read."
|
||||
"You read the project's knowledge bases, one level at a time. Use list_bundles to see "
|
||||
"what exists, read_bundle to open ONE of them and see its top level, read_dir to open a "
|
||||
"directory that listing named, and read_file to read a document you picked. read_bundle "
|
||||
"and read_dir return LISTINGS, never the documents — a knowledge base can hold hundreds, "
|
||||
"so descend to the part that matters instead of asking for all of it. Quote only what "
|
||||
"read_file gave you, and never guess at content you have not read."
|
||||
),
|
||||
HYPOTHESISER_ROLE: (
|
||||
"You shape ONE candidate cost-saving direction at a time from what the navigator found. "
|
||||
|
|
@ -893,12 +895,14 @@ def navigator_tools(bundle_dirs: Sequence[str]) -> list[FunctionTool]:
|
|||
@tool(
|
||||
name="read_bundle",
|
||||
description=(
|
||||
"Open ONE knowledge base by id and list what it holds: one entry per concept "
|
||||
"document with its name, declared type, title and size in characters. This is a "
|
||||
"LISTING, not the documents themselves — read_file(id, name) returns one whole."
|
||||
"Open ONE knowledge base by id and list its TOP LEVEL: the directories it is divided "
|
||||
"into (each with how many concept documents its whole subtree holds) and the documents "
|
||||
"that sit directly at the top, each with name, declared type, title and size in "
|
||||
"characters. Paths are usable as they are — read_dir(id, path) opens a directory, "
|
||||
"read_file(id, name) returns one document whole."
|
||||
),
|
||||
)
|
||||
def read_bundle(bundle_id: str) -> list[dict[str, Any]]:
|
||||
def read_bundle(bundle_id: str) -> dict[str, Any]:
|
||||
bundle_dir = _resolve_bundle(index, bundle_id)
|
||||
bundle = okf.navigate_bundle(bundle_dir)
|
||||
# The base is OPENED here, so this is where it must be able to say what it IS (S7a-3
|
||||
|
|
@ -906,25 +910,25 @@ def navigator_tools(bundle_dirs: Sequence[str]) -> list[FunctionTool]:
|
|||
# filesystem accident, recorded by the run rather than blocked here — but two concepts
|
||||
# declaring two different corpora is a base no fallback can settle.
|
||||
okf.assert_declared_ids_agree(bundle)
|
||||
# ``context_files``, NEVER ``files``: it is the property that drops the ``type: verdict``
|
||||
# layer AND nested ``index.md`` at every level. Building from ``files`` would put prior
|
||||
# verdicts in front of the navigator around the gated ExpeL fold (målbilde §4), and the
|
||||
# cost arms of the gate would all stay green while it happened.
|
||||
return [
|
||||
{
|
||||
"name": f.name,
|
||||
# ``or "document"`` mirrors ``okf.bundle_context``'s own fallback for a file with
|
||||
# no declared type, so the two renderings of one bundle cannot disagree about it.
|
||||
"type": f.type or "document",
|
||||
# ``okf`` owns the unquoting rule; a hand-rolled strip here would be a second copy
|
||||
# of it, and a second copy is the one that drifts (kø-(p)).
|
||||
"title": okf.unquote_scalar(f.frontmatter.get("title", f.name)),
|
||||
# What the next rung COSTS, in the unit the ceiling is measured in. A navigator
|
||||
# that cannot see the price cannot choose against a budget.
|
||||
"chars": len(f.body),
|
||||
}
|
||||
for f in bundle.context_files
|
||||
]
|
||||
# ONE renderer for both rungs (kø-(p)): this tool and ``read_dir`` differ only in WHICH
|
||||
# level they ask for, and two copies of a listing rule would drift into two answers about
|
||||
# one bundle. ``okf`` owns it, so the context seam stays framework-neutral.
|
||||
return okf.directory_listing(bundle)
|
||||
|
||||
@tool(
|
||||
name="read_dir",
|
||||
description=(
|
||||
"Open ONE directory inside a knowledge base, by base id and the path a previous "
|
||||
"listing gave you. Answers in the same shape as read_bundle: the directories one level "
|
||||
"further down, and the concept documents that sit in this one. An unknown path is "
|
||||
"refused rather than answered as an empty directory."
|
||||
),
|
||||
)
|
||||
def read_dir(bundle_id: str, path: str) -> dict[str, Any]:
|
||||
bundle_dir = _resolve_bundle(index, bundle_id)
|
||||
bundle = okf.navigate_bundle(bundle_dir)
|
||||
okf.assert_declared_ids_agree(bundle)
|
||||
return okf.directory_listing(bundle, path)
|
||||
|
||||
@tool(
|
||||
name="read_file",
|
||||
|
|
@ -937,7 +941,7 @@ def navigator_tools(bundle_dirs: Sequence[str]) -> list[FunctionTool]:
|
|||
# navigation walk uses rather than a second, laxer check.
|
||||
return Path(safe_resolve(bundle_dir, path)).read_text(encoding="utf-8")
|
||||
|
||||
return [list_bundles, read_bundle, read_file]
|
||||
return [list_bundles, read_bundle, read_dir, read_file]
|
||||
|
||||
|
||||
def quick_validate_tool(
|
||||
|
|
|
|||
|
|
@ -1015,6 +1015,84 @@ def assert_declared_ids_agree(bundle: Bundle) -> None:
|
|||
)
|
||||
|
||||
|
||||
class BundlePathNotFound(ValueError):
|
||||
"""A listing was asked for a directory the navigated bundle does not have.
|
||||
|
||||
A ``ValueError``, the ``BundleIdMismatch`` precedent: it must land on the CLI's refusal tuple
|
||||
and hosting's 400 arm rather than the crash channel. Refusing is the point — an unknown path
|
||||
rendered as an empty listing is indistinguishable from a directory that exists and holds
|
||||
nothing, and the caller is a model choosing a path out of a previous listing.
|
||||
"""
|
||||
|
||||
|
||||
def directory_listing(bundle: Bundle, path: str = "") -> dict[str, Any]:
|
||||
"""One LEVEL of a navigated bundle: the subdirectories under ``path`` with what each holds, and
|
||||
the concept documents that sit directly in it.
|
||||
|
||||
The rung between ``list_bundles`` (which bases exist) and ``read_file`` (what one document
|
||||
says). Before it existed, ``read_bundle`` answered with EVERY concept document in the base —
|
||||
measured on K2 at 42 761 o200k tokens riding in 7 of 12 prompts, 90 % of the run — because the
|
||||
price of finding out what a base contains was set by how much it contains.
|
||||
|
||||
**The tree is derived from ``context_files`` NAMES, never from ``index.md`` files.** Every name
|
||||
is already the full bundle-relative posix path, so the shape was never lost — only the
|
||||
rendering flattened it, which is why ``bundle_context`` and both nav-goldens are byte-identical
|
||||
after this was added. Deriving from names also means a directory whose index was never linked is
|
||||
still visible, and that the two renderings of one bundle cannot disagree about what a concept is.
|
||||
|
||||
``context_files``, NEVER ``files``: it is the property that drops the ``type: verdict`` layer
|
||||
AND nested ``index.md`` at every level. A listing built from ``files`` would put prior verdicts
|
||||
in front of the navigator around the gated ExpeL fold (målbilde §4) — and a directory COUNT
|
||||
built from ``files`` would advertise documents the navigator is not allowed to be shown.
|
||||
|
||||
**Every path is BUNDLE-RELATIVE, i.e. usable verbatim as the next call's argument.** A level-
|
||||
relative name would have to be composed by the caller, and the caller is a model: a path that
|
||||
never existed is worse than no path (``_index_excerpt``'s rule, one rung up).
|
||||
|
||||
``documents`` on a directory entry is the count of concept documents in its whole SUBTREE — what
|
||||
the subtree holds, not what one ``read_dir`` on it returns. It is the price signal a navigator
|
||||
chooses against, and the tool description says which of the two it is rather than leaving the
|
||||
reader to guess.
|
||||
|
||||
:raises BundlePathNotFound: no navigated concept document lives under ``path``.
|
||||
"""
|
||||
prefix = "" if path in ("", ".") else path.strip("/") + "/"
|
||||
directories: dict[str, int] = {}
|
||||
documents: list[dict[str, Any]] = []
|
||||
for f in bundle.context_files:
|
||||
if not f.name.startswith(prefix):
|
||||
continue
|
||||
rest = f.name[len(prefix) :]
|
||||
head, sep, _ = rest.partition("/")
|
||||
if sep:
|
||||
directories[prefix + head] = directories.get(prefix + head, 0) + 1
|
||||
else:
|
||||
documents.append(
|
||||
{
|
||||
"name": f.name,
|
||||
# ``or "document"`` mirrors ``bundle_context``'s own fallback for a file with no
|
||||
# declared type, so the two renderings cannot disagree about it.
|
||||
"type": f.type or "document",
|
||||
"title": unquote_scalar(f.frontmatter.get("title", f.name)),
|
||||
# What the next rung COSTS, in the unit the ceiling is measured in. A navigator
|
||||
# that cannot see the price cannot choose against a budget.
|
||||
"chars": len(f.body),
|
||||
}
|
||||
)
|
||||
if prefix and not directories and not documents:
|
||||
raise BundlePathNotFound(
|
||||
f"knowledge base {bundle.dir!r} has no directory {path!r}; it holds no concept "
|
||||
"document under that path"
|
||||
)
|
||||
return {
|
||||
"path": path,
|
||||
"directories": [
|
||||
{"path": name, "documents": count} for name, count in sorted(directories.items())
|
||||
],
|
||||
"documents": documents,
|
||||
}
|
||||
|
||||
|
||||
def bundle_context(bundle: Bundle, *, dimension: str | None = None) -> str:
|
||||
"""Render a navigated bundle as agent read-context via progressive disclosure: the ``index.md``
|
||||
summary, then each concept file as ``## {type}: {title}\\n{body}``. ``type: verdict`` files are
|
||||
|
|
|
|||
|
|
@ -244,10 +244,10 @@ def test_explore_read_bundle_reconciles_the_base_it_opens(_spy: list[str]) -> No
|
|||
"""(h) The tool is driven DIRECTLY: measured in okt 56, a scripted client returns TEXT and never
|
||||
emits a tool call, so a gate that only drove ``explore()`` would never enter this body."""
|
||||
tools = {t.name: t for t in explore.navigator_tools((str(_BYGG),))}
|
||||
# See test_read_bundle_cost_loadbearing: the tool returns a LISTING since S2c, and ``!= ""``
|
||||
# holds for every list ever built — this arm is about the reconciliation, so the call must
|
||||
# still be a call that DID something.
|
||||
assert [e["name"] for e in tools["read_bundle"].func(bundle_id=_BYGG.name)]
|
||||
# See test_read_bundle_cost_loadbearing: the tool returns a LISTING since S2c and ONE LEVEL of
|
||||
# one since S7a-3, and ``!= ""`` holds for every container ever built — this arm is about the
|
||||
# reconciliation, so the call must still be a call that DID something.
|
||||
assert [e["name"] for e in tools["read_bundle"].func(bundle_id=_BYGG.name)["documents"]]
|
||||
assert str(_BYGG) in _spy
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -683,9 +683,13 @@ async def test_an_exploration_leaves_the_knowledge_base_byte_identical(tmp_path:
|
|||
# Every read tool, called on the same base, with model-shaped arguments.
|
||||
tools = {t.name: t for t in explore.navigator_tools((str(base),))}
|
||||
assert tools["list_bundles"].func()[0]["id"] == base.name
|
||||
# A LISTING since S2c, so ``!= ""`` would be vacuously true for any list at all — assert the
|
||||
# shape the tool now promises instead of the shape it used to.
|
||||
assert [e["name"] for e in tools["read_bundle"].func(bundle_id=base.name)]
|
||||
# A LISTING since S2c and ONE LEVEL of one since S7a-3, so ``!= ""`` would be vacuously true
|
||||
# for any container at all — assert the shape the tool now promises instead of the one it used
|
||||
# to. ``read_dir`` is driven too: this arm's whole claim is that EVERY read tool leaves the base
|
||||
# untouched, so a tool it does not call is a hole in it. ``path=""`` is the root level, which is
|
||||
# the only path a flat base has.
|
||||
assert [e["name"] for e in tools["read_bundle"].func(bundle_id=base.name)["documents"]]
|
||||
assert tools["read_dir"].func(bundle_id=base.name, path="")["documents"]
|
||||
assert tools["read_file"].func(bundle_id=base.name, path="index.md") != ""
|
||||
explore.quick_validate_tool((str(base),)).func(
|
||||
bundle_id=base.name, proposal_json=json.dumps(_micro_projection())
|
||||
|
|
|
|||
291
tests/test_hierarchical_navigation_loadbearing.py
Normal file
291
tests/test_hierarchical_navigation_loadbearing.py
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
"""S7a-3 pkt. 2 - the ladder gains its missing rung: ``read_bundle`` -> ``read_dir`` -> ``read_file``.
|
||||
|
||||
**The measurement** (``docs/2026-09-03-syretest-s7a2-k2.md``). MAJOR-3 (session 77) rebuilt
|
||||
``read_bundle`` from "the whole navigated base" into "one entry per concept document", and on the
|
||||
three example bases that was a 77-91 % cut. Then the first real corpus arrived: K2 navigates to
|
||||
**629 concept documents behind 478 nested indexes**, and a listing of 629 documents costs
|
||||
**42 761 o200k tokens** and rides in 7 of 12 prompts - **90 % of every prompt token in the run**.
|
||||
The bound held asymptotically and still priced the whole corpus, because the rung was priced by the
|
||||
size of the base rather than by what the navigator had asked to see.
|
||||
|
||||
Worse, the 478 nested indexes were BUILT by the navigation, CONSUMED by it, and then **flattened
|
||||
away**: the agent saw 629 sibling documents and was never told the corpus had a shape.
|
||||
|
||||
**A premise of the order, corrected by measurement.** The order says ``context_files`` must stop
|
||||
discarding the hierarchy. It never held it back: every ``BundleFile.name`` is already the full
|
||||
bundle-relative posix path (``del-ii-bilag-7-prisskjema/prissammenstilling-sheet-1.md``), so the
|
||||
tree was always recoverable from the names alone. What flattened it was the RENDERING. That matters
|
||||
because it is why ``okf.bundle_context`` and both nav-goldens are byte-identical after this change -
|
||||
the order requires exactly that, and it comes for free rather than by care.
|
||||
|
||||
So: ``okf.directory_listing`` is ONE renderer, and BOTH tools are it at a different path.
|
||||
``read_bundle`` is the root level (subdirectories with a document count, plus the documents that sit
|
||||
at the top), ``read_dir(id, path)`` is one level down, ``read_file(id, path)`` is unchanged. Every
|
||||
byte is still exactly one call away; what changed is that a navigator pays for the level it asked
|
||||
for.
|
||||
|
||||
**Directories are derived from PATHS, never from ``index.md`` files.** A nested index is navigation,
|
||||
not content (``context_files`` drops it at every level, and must keep doing so or the verdict layer
|
||||
comes with it). Deriving the tree from names means a directory reached by a link that skipped its
|
||||
index is still visible - and it means the two renderings of one bundle cannot disagree about what
|
||||
the concepts are.
|
||||
|
||||
**DEVIATION from the order, stated and measured.** The order words the ceiling as "``read_bundle``
|
||||
over the K2 bundle < 1 500 chars". K2 lives outside the repository (``~/corpora/``) and cannot be a
|
||||
test dependency - the MAJOR-3 gate had the same constraint and solved it the same way, by bounding
|
||||
the largest base that IS shipped. And the number does not hold: K2's root level carries **39
|
||||
identifiable entries** (28 directories + 11 top-level documents) and measures **3 954 chars ~ 1 366
|
||||
o200k tokens**. Fitting 1 500 would mean dropping titles or dropping entries, which is the
|
||||
vacuous-gate class this repo has been bitten by twelve times. The measured drop is from 42 761 to
|
||||
1 366 tokens (-97 %); the K2 numbers live in the report, and the gate here bounds what it can
|
||||
actually see, plus the PROPERTY that produced the drop (arm (b)).
|
||||
|
||||
Arms: (a) a FLAT base lists exactly what it listed before * (b) cost tracks entries at ONE LEVEL,
|
||||
not documents in the base - with the flat control that proves the hierarchy did it * (c) ``read_dir``
|
||||
over the biggest directory is bounded too * (d) anti-vacuity: every document is still reachable, and
|
||||
the counts add up * (e) the verdict layer stays out of both halves * (f) the ladder is intact *
|
||||
(g) an unknown path is refused BY NAME, never rendered as an empty directory * (h) the navigator's
|
||||
instruction and the tool descriptions describe the ladder they now have.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from portfolio_optimiser import explore, okf
|
||||
from portfolio_optimiser.explore import navigator_tools
|
||||
|
||||
_EXAMPLES = Path(__file__).resolve().parents[1] / "shared" / "examples"
|
||||
#: The largest FLAT base shipped - the one the MAJOR-3 gate bounds.
|
||||
_TUNNEL = _EXAMPLES / "tunnel-hauglia"
|
||||
#: The only NESTED base shipped, and it is tiny: three levels, six concepts. It proves the shape is
|
||||
#: read correctly; it cannot prove the shape pays for itself, which is what arm (b) is for.
|
||||
_NESTED = _EXAMPLES / "nav-golden-hierarchy" / "bundle"
|
||||
|
||||
#: Characters ONE listing may cost - test-owned, 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. Same number as the MAJOR-3 gate, deliberately: this rung's
|
||||
#: price must not creep just because it now has a rung below it.
|
||||
_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 _read_bundle(bundle_dir: Path) -> dict[str, Any]:
|
||||
return _tools(bundle_dir)["read_bundle"].func(bundle_id=bundle_dir.name)
|
||||
|
||||
|
||||
def _read_dir(bundle_dir: Path, path: str) -> dict[str, Any]:
|
||||
return _tools(bundle_dir)["read_dir"].func(bundle_id=bundle_dir.name, path=path)
|
||||
|
||||
|
||||
def _blob(payload: object) -> str:
|
||||
return json.dumps(payload, ensure_ascii=False)
|
||||
|
||||
|
||||
def _entry(f: okf.BundleFile, name: str) -> dict[str, Any]:
|
||||
"""The four-field document entry MAJOR-3 introduced, written out here rather than imported: an
|
||||
assert against the implementation's own helper could not tell the two shapes apart."""
|
||||
return {
|
||||
"name": name,
|
||||
"type": f.type or "document",
|
||||
"title": okf.unquote_scalar(f.frontmatter.get("title", f.name)),
|
||||
"chars": len(f.body),
|
||||
}
|
||||
|
||||
|
||||
def _write_tree(root: Path, name: str, *, dirs: int, per_dir: int, top: int = 2) -> Path:
|
||||
"""A base shaped like a real ingested corpus: a root index linking to per-directory indexes,
|
||||
each linking to its own documents. Bodies are small on purpose - this fixture is about COUNT."""
|
||||
base = root / name
|
||||
base.mkdir(parents=True)
|
||||
root_lines = [f"# {name}", ""]
|
||||
for t in range(top):
|
||||
root_lines.append(f"- [Toppdokument {t}](topp-{t}.md)")
|
||||
(base / f"topp-{t}.md").write_text(
|
||||
f"---\ntype: concept\ntitle: Toppdokument {t}\n---\n\nkort tekst.\n", encoding="utf-8"
|
||||
)
|
||||
for d in range(dirs):
|
||||
sub = base / f"kategori-{d:02d}"
|
||||
sub.mkdir()
|
||||
root_lines.append(f"- [kategori-{d:02d} (index)](kategori-{d:02d}/index.md)")
|
||||
sub_lines = [f"# kategori-{d:02d}", ""]
|
||||
for i in range(per_dir):
|
||||
sub_lines.append(f"- [Dokument {i}](dok-{i:02d}.md)")
|
||||
(sub / f"dok-{i:02d}.md").write_text(
|
||||
f"---\ntype: concept\ntitle: Dokument {d:02d}-{i:02d}\n---\n\nkort tekst.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(sub / "index.md").write_text(
|
||||
"---\ntype: index\n---\n\n" + "\n".join(sub_lines) + "\n", encoding="utf-8"
|
||||
)
|
||||
(base / "index.md").write_text(
|
||||
"---\ntype: index\n---\n\n" + "\n".join(root_lines) + "\n", encoding="utf-8"
|
||||
)
|
||||
return base
|
||||
|
||||
|
||||
# --- (a) a flat base is untouched -----------------------------------------------------------------
|
||||
|
||||
|
||||
def test_a_flat_base_lists_exactly_what_it_listed_before() -> None:
|
||||
"""(a) The order names this one explicitly. Every base shipped in this repo except one is flat,
|
||||
and the whole MAJOR-3 measurement was taken over them: if the new rung changed what a flat base
|
||||
answers, this change would be a rewrite of that result rather than a level above it."""
|
||||
bundle = okf.navigate_bundle(str(_TUNNEL))
|
||||
listing = _read_bundle(_TUNNEL)
|
||||
|
||||
assert listing["documents"] == [_entry(f, f.name) for f in bundle.context_files]
|
||||
assert listing["directories"] == [], "a flat base has no subdirectories to report"
|
||||
assert len(_blob(listing)) <= _CEILING_CHARS
|
||||
|
||||
|
||||
# --- (b)/(c) the price is the LEVEL, not the base -------------------------------------------------
|
||||
|
||||
|
||||
def test_the_root_listing_costs_the_level_not_the_base(tmp_path: Path) -> None:
|
||||
"""(b) The headline, and the control that proves the hierarchy is what bounded it.
|
||||
|
||||
240 documents behind 20 directories: the root level is 22 entries and fits, while the FLAT
|
||||
listing of the same base - what ``read_bundle`` returned until today - is many times the
|
||||
ceiling. Without the flat control, a green bound here could just as well mean the fixture was
|
||||
small (Verifiseringsloven face 4: a gate that can only pass proves nothing).
|
||||
"""
|
||||
base = _write_tree(tmp_path, "stort-korpus", dirs=20, per_dir=12)
|
||||
bundle = okf.navigate_bundle(str(base))
|
||||
|
||||
listing = _read_bundle(base)
|
||||
flat = [_entry(f, f.name) for f in bundle.context_files]
|
||||
|
||||
assert len(bundle.context_files) == 242
|
||||
assert len(_blob(listing)) <= _CEILING_CHARS, (
|
||||
f"the root level costs {len(_blob(listing))} chars over the ceiling {_CEILING_CHARS}"
|
||||
)
|
||||
assert len(_blob(flat)) > 5 * _CEILING_CHARS, (
|
||||
"the CONTROL is inert: the flat listing must be the thing that does not fit, or the bound "
|
||||
"above is measuring a small fixture rather than the hierarchy"
|
||||
)
|
||||
|
||||
|
||||
def test_read_dir_over_the_biggest_directory_is_bounded_too(tmp_path: Path) -> None:
|
||||
"""(c) A rung that is only cheap at the top is not a ladder. The order asks for this one by
|
||||
name: ``read_dir`` over the largest directory must be bounded as well."""
|
||||
base = _write_tree(tmp_path, "stort-korpus", dirs=20, per_dir=12)
|
||||
|
||||
listing = _read_dir(base, "kategori-00")
|
||||
|
||||
assert len(listing["documents"]) == 12 and listing["directories"] == []
|
||||
assert len(_blob(listing)) <= _CEILING_CHARS
|
||||
|
||||
|
||||
# --- (d)/(e) bounded is not the same as empty -----------------------------------------------------
|
||||
|
||||
|
||||
def test_every_document_is_still_reachable_and_the_counts_add_up(tmp_path: Path) -> None:
|
||||
"""(d) The anti-vacuity arm. Returning ``{"directories": [], "documents": []}`` passes every
|
||||
bound above perfectly and hands the navigator nothing. Two things are asserted: each directory
|
||||
entry PRICES its subtree (so a navigator can choose against a budget), and the whole base is
|
||||
accounted for - every concept document is in exactly one level."""
|
||||
base = _write_tree(tmp_path, "stort-korpus", dirs=20, per_dir=12)
|
||||
bundle = okf.navigate_bundle(str(base))
|
||||
|
||||
root = _read_bundle(base)
|
||||
counted = len(root["documents"]) + sum(int(d["documents"]) for d in root["directories"])
|
||||
|
||||
assert counted == len(bundle.context_files) == 242
|
||||
assert {str(d["path"]) for d in root["directories"]} == {f"kategori-{d:02d}" for d in range(20)}
|
||||
assert all(int(d["documents"]) == 12 for d in root["directories"])
|
||||
for entry in root["documents"]:
|
||||
assert set(entry) == {"name", "type", "title", "chars"}
|
||||
assert str(entry["title"]).strip() and int(entry["chars"]) > 0
|
||||
|
||||
|
||||
def test_the_verdict_layer_is_excluded_from_both_halves(tmp_path: Path) -> None:
|
||||
"""(e) MAJOR-3's rule, extended to the new half: a listing built from ``bundle.files`` would
|
||||
route prior verdicts in front of the navigator AROUND the gated ExpeL fold (maalbilde §4) - and
|
||||
a DIRECTORY COUNT built from ``files`` would tell the navigator a subtree holds documents it is
|
||||
not allowed to be shown."""
|
||||
base = _write_tree(tmp_path, "med-dommer", dirs=2, per_dir=2, top=0)
|
||||
sub = base / "kategori-00"
|
||||
(sub / "dom-0.md").write_text(
|
||||
"---\ntype: verdict\ntitle: Dom\n---\n\nEksperten godkjente.\n", encoding="utf-8"
|
||||
)
|
||||
index = sub / "index.md"
|
||||
index.write_text(index.read_text(encoding="utf-8") + "- [dom](dom-0.md)\n", encoding="utf-8")
|
||||
|
||||
root = _read_bundle(base)
|
||||
inner = _read_dir(base, "kategori-00")
|
||||
|
||||
assert len(okf.navigate_bundle(str(base)).verdicts) == 1, "the fixture must HAVE one to leak"
|
||||
assert [d["documents"] for d in root["directories"] if d["path"] == "kategori-00"] == [2]
|
||||
assert not [e for e in inner["documents"] if e["type"] == "verdict"]
|
||||
assert len(inner["documents"]) == 2
|
||||
|
||||
|
||||
# --- (f)/(g) the ladder, and its fail-closed edge -------------------------------------------------
|
||||
|
||||
|
||||
def test_the_whole_document_is_still_one_call_away() -> None:
|
||||
"""(f) The bound is a disclosure LEVEL, not data loss - MAJOR-3's arm (e), re-asserted over the
|
||||
new shape because a listing that no longer names documents the way ``read_file`` takes them
|
||||
would have broken the ladder while every cost arm stayed green."""
|
||||
listing = _read_bundle(_TUNNEL)
|
||||
biggest = max(listing["documents"], 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
|
||||
assert int(biggest["chars"]) <= len(whole)
|
||||
|
||||
|
||||
def test_a_nested_document_is_reachable_through_the_rung_below() -> None:
|
||||
"""(f, nested) The three rungs composed on the only nested base shipped: the root names a
|
||||
directory, ``read_dir`` names a document in it, ``read_file`` returns that document whole."""
|
||||
root = _read_bundle(_NESTED)
|
||||
first_dir = str(root["directories"][0]["path"])
|
||||
|
||||
inner = _read_dir(_NESTED, first_dir)
|
||||
name = str(inner["documents"][0]["name"])
|
||||
whole = _tools(_NESTED)["read_file"].func(bundle_id=_NESTED.name, path=name)
|
||||
|
||||
assert root["documents"] and root["directories"], "the fixture must have BOTH halves"
|
||||
assert name.startswith(f"{first_dir}/"), (
|
||||
"every path a listing hands out must be usable AS IT IS: a level-relative name would have "
|
||||
"to be composed by a model, and a path that never existed is worse than no path"
|
||||
)
|
||||
assert whole.strip(), "the path the listing gave must resolve to the document itself"
|
||||
|
||||
|
||||
def test_an_unknown_directory_is_refused_by_name(tmp_path: Path) -> None:
|
||||
"""(g) Fail-closed, and it is the vacuity trap in its own right: an unknown path rendered as an
|
||||
empty listing is indistinguishable from a directory that exists and holds nothing. Validation,
|
||||
never invention (``write_concept_file``'s rule)."""
|
||||
base = _write_tree(tmp_path, "korpus", dirs=2, per_dir=2)
|
||||
|
||||
with pytest.raises(ValueError, match="kategori-99"):
|
||||
_read_dir(base, "kategori-99")
|
||||
|
||||
|
||||
# --- (h) the description must not lie about the body ----------------------------------------------
|
||||
|
||||
|
||||
def test_the_navigator_is_told_about_the_rung_it_now_has() -> None:
|
||||
"""(h) The Fase-3 class applied to a tool: a description that lies about the body IS the
|
||||
model's instruction. MAJOR-3 had to move this same prose when ``read_bundle`` stopped returning
|
||||
the navigated context; the ladder is three rungs now, and all three must be named."""
|
||||
instruction = explore._INSTRUCTIONS[explore.NAVIGATOR_ROLE]
|
||||
tools = {t.name: t for t in navigator_tools((str(_NESTED),))}
|
||||
|
||||
assert set(tools) == {"list_bundles", "read_bundle", "read_dir", "read_file"}
|
||||
for rung in ("list_bundles", "read_bundle", "read_dir", "read_file"):
|
||||
assert rung in instruction, f"the navigator is never told that {rung} exists"
|
||||
assert "read_dir" in tools["read_bundle"].description, (
|
||||
"read_bundle now answers with directories; a description that does not name the tool that "
|
||||
"opens one leaves the navigator holding a path it cannot use"
|
||||
)
|
||||
|
|
@ -39,6 +39,14 @@ o200k tokens** (2.89 chars/token for this Norwegian markdown), so 1 500 characte
|
|||
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
|
||||
|
|
@ -82,10 +90,18 @@ 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) -> list[dict[str, Any]]:
|
||||
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)
|
||||
|
||||
|
|
@ -129,7 +145,7 @@ def test_read_bundle_cost_does_not_track_document_size(tmp_path: Path) -> None:
|
|||
small = _write_base(tmp_path, "small", body_chars=500)
|
||||
large = _write_base(tmp_path, "large", body_chars=5_000)
|
||||
|
||||
small_entries, large_entries = _listing(small), _listing(large)
|
||||
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.
|
||||
|
|
@ -149,7 +165,7 @@ def test_the_listing_still_identifies_every_document(tmp_path: Path) -> None:
|
|||
context files rather than to a number written here.
|
||||
"""
|
||||
bundle = okf.navigate_bundle(str(_TUNNEL))
|
||||
entries = _listing(_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"
|
||||
|
|
@ -172,7 +188,7 @@ def test_the_verdict_layer_is_still_excluded(tmp_path: Path) -> None:
|
|||
"""
|
||||
base = _write_base(tmp_path, "med-dommer", body_chars=200, concepts=2, verdicts=2)
|
||||
|
||||
entries = _listing(base)
|
||||
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"]
|
||||
|
|
@ -184,7 +200,7 @@ def test_the_verdict_layer_is_still_excluded(tmp_path: Path) -> None:
|
|||
|
||||
def test_the_whole_document_is_still_one_call_away() -> None:
|
||||
"""(e) The bound is a disclosure LEVEL, not data loss."""
|
||||
entries = _listing(_TUNNEL)
|
||||
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"]))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue