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
|
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.
|
monteringer med samme basenavn, nå kolliderer to ULIKT navngitte kataloger som erklærer samme id.
|
||||||
Kontrakt: `docs/okf-konsum-kontrakter.md § 3.1`.
|
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.
|
- **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.
|
- 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 = {
|
_INSTRUCTIONS: Final = {
|
||||||
NAVIGATOR_ROLE: (
|
NAVIGATOR_ROLE: (
|
||||||
"You read the project's knowledge bases. Use list_bundles to see what exists, "
|
"You read the project's knowledge bases, one level at a time. Use list_bundles to see "
|
||||||
"read_bundle to list the documents inside ONE of them, and read_file to read a document "
|
"what exists, read_bundle to open ONE of them and see its top level, read_dir to open a "
|
||||||
"you picked from that list. read_bundle returns a LISTING, never the documents — quote "
|
"directory that listing named, and read_file to read a document you picked. read_bundle "
|
||||||
"only what read_file gave you, and never guess at content you have not read."
|
"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: (
|
HYPOTHESISER_ROLE: (
|
||||||
"You shape ONE candidate cost-saving direction at a time from what the navigator found. "
|
"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(
|
@tool(
|
||||||
name="read_bundle",
|
name="read_bundle",
|
||||||
description=(
|
description=(
|
||||||
"Open ONE knowledge base by id and list what it holds: one entry per concept "
|
"Open ONE knowledge base by id and list its TOP LEVEL: the directories it is divided "
|
||||||
"document with its name, declared type, title and size in characters. This is a "
|
"into (each with how many concept documents its whole subtree holds) and the documents "
|
||||||
"LISTING, not the documents themselves — read_file(id, name) returns one whole."
|
"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_dir = _resolve_bundle(index, bundle_id)
|
||||||
bundle = okf.navigate_bundle(bundle_dir)
|
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
|
# 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
|
# filesystem accident, recorded by the run rather than blocked here — but two concepts
|
||||||
# declaring two different corpora is a base no fallback can settle.
|
# declaring two different corpora is a base no fallback can settle.
|
||||||
okf.assert_declared_ids_agree(bundle)
|
okf.assert_declared_ids_agree(bundle)
|
||||||
# ``context_files``, NEVER ``files``: it is the property that drops the ``type: verdict``
|
# ONE renderer for both rungs (kø-(p)): this tool and ``read_dir`` differ only in WHICH
|
||||||
# layer AND nested ``index.md`` at every level. Building from ``files`` would put prior
|
# level they ask for, and two copies of a listing rule would drift into two answers about
|
||||||
# verdicts in front of the navigator around the gated ExpeL fold (målbilde §4), and the
|
# one bundle. ``okf`` owns it, so the context seam stays framework-neutral.
|
||||||
# cost arms of the gate would all stay green while it happened.
|
return okf.directory_listing(bundle)
|
||||||
return [
|
|
||||||
{
|
@tool(
|
||||||
"name": f.name,
|
name="read_dir",
|
||||||
# ``or "document"`` mirrors ``okf.bundle_context``'s own fallback for a file with
|
description=(
|
||||||
# no declared type, so the two renderings of one bundle cannot disagree about it.
|
"Open ONE directory inside a knowledge base, by base id and the path a previous "
|
||||||
"type": f.type or "document",
|
"listing gave you. Answers in the same shape as read_bundle: the directories one level "
|
||||||
# ``okf`` owns the unquoting rule; a hand-rolled strip here would be a second copy
|
"further down, and the concept documents that sit in this one. An unknown path is "
|
||||||
# of it, and a second copy is the one that drifts (kø-(p)).
|
"refused rather than answered as an empty directory."
|
||||||
"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.
|
def read_dir(bundle_id: str, path: str) -> dict[str, Any]:
|
||||||
"chars": len(f.body),
|
bundle_dir = _resolve_bundle(index, bundle_id)
|
||||||
}
|
bundle = okf.navigate_bundle(bundle_dir)
|
||||||
for f in bundle.context_files
|
okf.assert_declared_ids_agree(bundle)
|
||||||
]
|
return okf.directory_listing(bundle, path)
|
||||||
|
|
||||||
@tool(
|
@tool(
|
||||||
name="read_file",
|
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.
|
# navigation walk uses rather than a second, laxer check.
|
||||||
return Path(safe_resolve(bundle_dir, path)).read_text(encoding="utf-8")
|
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(
|
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:
|
def bundle_context(bundle: Bundle, *, dimension: str | None = None) -> str:
|
||||||
"""Render a navigated bundle as agent read-context via progressive disclosure: the ``index.md``
|
"""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
|
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
|
"""(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."""
|
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),))}
|
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 ``!= ""``
|
# See test_read_bundle_cost_loadbearing: the tool returns a LISTING since S2c and ONE LEVEL of
|
||||||
# holds for every list ever built — this arm is about the reconciliation, so the call must
|
# one since S7a-3, and ``!= ""`` holds for every container ever built — this arm is about the
|
||||||
# still be a call that DID something.
|
# 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)]
|
assert [e["name"] for e in tools["read_bundle"].func(bundle_id=_BYGG.name)["documents"]]
|
||||||
assert str(_BYGG) in _spy
|
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.
|
# Every read tool, called on the same base, with model-shaped arguments.
|
||||||
tools = {t.name: t for t in explore.navigator_tools((str(base),))}
|
tools = {t.name: t for t in explore.navigator_tools((str(base),))}
|
||||||
assert tools["list_bundles"].func()[0]["id"] == base.name
|
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
|
# A LISTING since S2c and ONE LEVEL of one since S7a-3, so ``!= ""`` would be vacuously true
|
||||||
# shape the tool now promises instead of the shape it used to.
|
# for any container at all — assert the shape the tool now promises instead of the one it used
|
||||||
assert [e["name"] for e in tools["read_bundle"].func(bundle_id=base.name)]
|
# 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") != ""
|
assert tools["read_file"].func(bundle_id=base.name, path="index.md") != ""
|
||||||
explore.quick_validate_tool((str(base),)).func(
|
explore.quick_validate_tool((str(base),)).func(
|
||||||
bundle_id=base.name, proposal_json=json.dumps(_micro_projection())
|
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
|
growth does not force a rewrite. The order's own criterion is verified directly, once, by the
|
||||||
instrument in the measurement doc.
|
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:
|
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
|
(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),))}
|
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)
|
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:
|
def _blob(payload: object) -> str:
|
||||||
return json.dumps(payload, ensure_ascii=False)
|
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)
|
small = _write_base(tmp_path, "small", body_chars=500)
|
||||||
large = _write_base(tmp_path, "large", body_chars=5_000)
|
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,
|
# 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.
|
# 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.
|
context files rather than to a number written here.
|
||||||
"""
|
"""
|
||||||
bundle = okf.navigate_bundle(str(_TUNNEL))
|
bundle = okf.navigate_bundle(str(_TUNNEL))
|
||||||
entries = _listing(_TUNNEL)
|
entries = _documents(_TUNNEL)
|
||||||
|
|
||||||
assert len(entries) == len(bundle.context_files) > 0, (
|
assert len(entries) == len(bundle.context_files) > 0, (
|
||||||
"a listing that omits documents is a base the navigator cannot fully see"
|
"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)
|
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 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 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:
|
def test_the_whole_document_is_still_one_call_away() -> None:
|
||||||
"""(e) The bound is a disclosure LEVEL, not data loss."""
|
"""(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"]))
|
biggest = max(entries, key=lambda e: int(e["chars"]))
|
||||||
|
|
||||||
whole = _tools(_TUNNEL)["read_file"].func(bundle_id=_TUNNEL.name, path=str(biggest["name"]))
|
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