feat(explore): katalogkallet koster O(baser), ikke O(korpus) (ORDRE 20260825T213645Z)

list_bundles returnerte hele rot-indeksens body for HVER konfigurert base samtidig,
pluss ett JSON-objekt per ufulgt kryss-lenke. Begge vokser med korpuset, saa prisen
paa aa finne ut HVILKE baser som finnes ble satt av hvor mye de INNEHOLDER - progressiv
disclosure snudd paa hodet.

Maalt med o200k_base, instrumentet foerst validert mot commons' egne fasittall:
  tre flate Vegnormal-baser   112 116 -> 362 tokens   (-99,7 %)
  171 grenbaser               124 942 -> 21 448       (-82,8 %)
Grenformen (vegnormal-okf 8145c23) lukket bundle-siden og gjorde katalogsiden verre,
noeyaktig som det repoet forutsa.

Et premiss ble felt FOER noe ble bygget paa det: "indeksbodyen forteller hva basen
handler om" er usant for maskin-importerte baser - grenbasenes index.md er en ren
lenkeliste uten frontmatter og prosa, saa feltet var dyrt OG innholdsloest der.

Fast vindu (200 tegn), aldri en andel av basen. Avkorting annonseres som FELT
(index_truncated), og en base som passer blir ikke merket avkortet. En ufulgt lenke
overlever som ANTALL; per-lenke-detaljen blir liggende der den er handlingsbar.
Hele indeksen er fortsatt ett read_file(id, "index.md") unna.

Taket (500 tegn/base) bor i TESTEN, ikke i explore.py.

Load-bearing MAALT: tests/test_catalogue_cost_loadbearing.py, 7 armer, ni mutasjoner
alle roede mot HELE suiten + groenn kontroll 1066/5 og golden demo-transcript.stdout
byte-uendret (ea8c534773acdbe41ae68f2c55724d69aaf8be4f).

Ogsaa: MINOR-1 i syretest-rapporten rettet - flatheten er Doer C sin
(llm-ingestion-okf importer.py, §6-index-blokka), ikke vegnormals emitterform.
Verifisert mot kilden, ikke mot meldingen.

Maaling: docs/2026-08-26-katalogkostnaden.md
This commit is contained in:
Kjell Tore Guttormsen 2026-08-26 14:45:16 +02:00
commit a1f8522bdf
6 changed files with 421 additions and 11 deletions

View file

@ -1038,6 +1038,41 @@ Python ≥3.10. MAF (`agent-framework-core` 1.9.0). Pakkehåndtering: `uv`. To b
stall) har ingen nåbar sti under det skriptede manuset, så carry-overen som betjener den drives
gjennom en CRAFTED parkert tilstand (`budget_stop`-presedensen); og resume-legets
`PlanReviewParked` er et NORMALT utfall, ikke en feil.
- **Katalogkallet koster O(BASER), aldri O(KORPUS) — og det er stigens billigste trinn, ikke dens
dyreste (ordre `20260825T213645Z`, økt 65):** `list_bundles` returnerte hele rot-indeksens body
for HVER konfigurert base samtidig, pluss ett JSON-objekt per ufulgt kryss-lenke. Begge vokser med
korpuset, så prisen på å finne ut *hvilke baser som finnes* ble satt av hvor mye de *inneholder*
progressiv disclosure snudd på hodet (målbilde §2/§4). **MÅLT med `o200k_base`, instrumentet først
validert mot commons' egne fasittall:** 112 116 tokens over tre flate Vegnormal-baser, og
**124 942 over de 171 grenbasene** som erstattet dem — grenformen (`vegnormal-okf` `8145c23`)
lukket bundle-siden (82…92 % på `read_bundle`) og gjorde katalogsiden VERRE, nøyaktig som det
repoet forutså. Etter: **362** og **21 448** (per base 37 372 → 121 og 731 → 125).
**Et premiss ble felt FØR noe ble bygget på det:** «indeksbodyen forteller hva basen handler om»
er USANT for maskin-importerte baser — grenbasenes `index.md` har verken frontmatter eller prosa,
den er en ren lenkeliste (målt: 959 bytes, første tegn `-`), så feltet var dyrt OG innholdsløst
der. **Fast vindu, aldri en andel av basen** (`_CATALOGUE_EXCERPT_CHARS = 200`): en andel skalerer
med korpuset igjen, bare med mindre konstant. **Avkorting ANNONSERES som FELT**
(`index_truncated` ved siden av utdraget, aldri en markør limt inn i det — `BudgetExceeded`s
kø-(y)-regel), og en base som PASSER blir ikke merket avkortet og får hele bodyen: omisjon, aldri
en løgn i noen av retningene. **En ufulgt lenke overlever som ANTALL** — økt 51s «et hopp er
tolerert, men ikke lenger taust» står, mens per-lenke-detaljen blir liggende der den er
handlingsbar (`RunResult.skipped_links` / `DryRunReport.skipped_links`) og ikke rir med i et kall
hvis hele jobb er å være billig. Hele indeksen er fortsatt ETT `read_file(id, "index.md")` unna —
et disclosure-nivå, ikke datatap. **Taket (500 tegn/base) bor i TESTEN, ikke i `explore.py`:** en
test som importerte implementasjonens budsjett ville flyttet seg med det, og å heve budsjettet er
nøyaktig regresjonen gaten finnes for. Load-bearing MÅLT
(`tests/test_catalogue_cost_loadbearing.py`, 7 armer), **ni mutasjoner alle røde mot HELE suiten**
+ grønn kontroll 1066/5 og golden `demo-transcript.stdout` BYTE-UENDRET
(`ea8c534773acdbe41ae68f2c55724d69aaf8be4f`): ingen binding (4 røde) · bundet men vakuøst (2) ·
stille kutt (1) · over-annonsert (1) · per-lenke-lista rir med igjen (1) · det ufulgte faktumet
slettet (1) · suffiks i stedet for ordrett prefiks (2) · en andel i stedet for fast vindu (2) ·
`documents` gjort konstant (1). **M9 ble kjørt fordi `documents` var et felt uten gate** — et felt
ingen test kan se, råtner. **MAJOR-1 var IKKE nødvendig:** bindingen sitter i verktøykroppen bak
en uendret CLI-flate. Ærlighets-grenser, uttalt: `navigate_bundle` kalles fortsatt per base per
katalogkall (I/O og veggklokke, ikke tokens — ikke målt her); ingen LEVENDE modell har kalt det
nye verktøyet, så at en manager velger BEDRE med et utdrag enn med hele indeksen er ikke bevist
(structured-output-grensens klasse); og ordrens nevner for N100:2023 var 34 mens disken viser 40 —
tallene bruker den målte nevneren. Måling: `docs/2026-08-26-katalogkostnaden.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.

View file

@ -513,6 +513,15 @@ when the seam is detached, so the loop cannot silently degrade into theater.
is refused when several are configured, rather than routed to a guess. The CLI's `--bundle-dir`
stays single-valued; multi-base is a library door today.
Surveying those bases is deliberately cheap. `list_bundles` costs **O(bases), never O(corpus)**:
each entry carries the base's id, a bounded verbatim opening of its index (with
`index_truncated` beside it when the opening was cut), how many documents and prior expert
verdicts it holds, whether it ships a cost baseline, and how many cross-links could not be
followed — never the whole index. The full index stays one `read_file(id, "index.md")` away, so
the bound is a disclosure level rather than data loss. Measured 2026-08-26 over a real corpus:
112 116 → 362 tokens for three bases, 124 942 → 21 448 for 171
([report](docs/2026-08-26-katalogkostnaden.md)).
`--semantic-retrieval` (S3.1) is an **opt-in** ranking change, **off by default**. Off, prior
verdicts are ranked exactly as before: a structural score over the affected cost-code set,
measure type and magnitude bucket, with surface text deliberately excluded. On, that score is

View file

@ -84,6 +84,13 @@ enkelt katalogkall bruker mer enn et normalt tak. En 128k-modell kan ikke ta N20
er den harde grensen for vei A/B live, og den var ikke målt før i dag.
**Fil:linje:** `src/portfolio_optimiser/explore.py:419-438` (`list_bundles`), `:444-445` (`read_bundle`).
> **Oppdatert 2026-08-26 (økt 65, ordre `20260825T213645Z-9019120455`):** katalog-halvdelen er
> **lukket**. `list_bundles()` over de samme tre basene koster nå **362 tokens** (fra 112 116), og
> over alle 171 grenbaser **21 448** (fra 124 942). `read_bundle`-halvdelen ble lukket på korpussiden
> av `vegnormal-okf` `8145c23` (grener som egne baser). Måling og gate:
> [docs/2026-08-26-katalogkostnaden.md](2026-08-26-katalogkostnaden.md). Setningen over står som
> den ble målt 25.08 — den er historikk, ikke en gjeldende tilstand.
### MAJOR-1 — gjentatt `--bundle-dir` forkastes STILLE; kjøringen ser ut som multi-base og er det ikke
Ordrens pkt. 3 forutsatte at CLI-en tar tre baser. Målt:
@ -172,9 +179,21 @@ som kontekst») er en **annen form**, og den finnes allerede — bare ikke i dis
### MINOR-1 — importformen legger alt på ett nivå, så progressiv disclosure gir null lettelse
0 underkataloger, 0 nestede `index.md`, 1733 av 1733 konsepter lenket direkte fra rot. Navigasjonen
har ingenting å utsette; hele korpuset er ett flatt nivå. Dette er en egenskap ved **kilden**
(vegnormal-okf sin importform), ikke ved `okf.py`. Det er også hele grunnen til BLOCKER-1: med
nestede indekser kunne manageren åpnet én gren om gangen.
har ingenting å utsette; hele korpuset er ett flatt nivå. Dette er en egenskap ved **kilden**, ikke
ved `okf.py`.
> **Rettet 2026-08-26 (økt 65): tilskrivelsen var feil, og `vegnormal-okf` har rett.** Flatheten er
> **Dør C** sin, ikke vegnormals emitterform. Verifisert mot kilden, ikke mot deres melding:
> `llm-ingestion-okf` `src/llm_ingestion_okf/importer.py` (§6-index-blokka) kaller
> `link_in_index(bundle, entry.path.name, _index_label(entry.concept_path))` per merget oppføring —
> altså én flat lenke i rot-`index.md` for hvert konsept, uansett hvor nestet konseptstien er.
> Vegnormals emitter skriver allerede et tonivåtre. Setningen over sto uendret som «vegnormal-okf
> sin importform» til dette punktet.
Dette var også hele grunnen til BLOCKER-1: med nestede indekser kunne manageren åpnet én gren om
gangen. Løsningen ble en annen — grener som **egne baser** (`vegnormal-okf` `8145c23`), med den
målte begrunnelsen at basegrensen er der OKF-navigasjonen stopper, så ingen indeksstruktur INNE i en
base senker prisen på å åpne den.
### NICE-1 — `read_bundle` nekter ukjent base ved navn, som lovet

View file

@ -0,0 +1,120 @@
# Katalogkallet — `list_bundles()` fra 112 116 til 362 tokens
**Ordre:** `20260825T213645Z-9019120455-from-.claude`. **Dato:** 2026-08-26 (økt 65).
**Kode:** `src/portfolio_optimiser/explore.py` (`_index_excerpt`, `navigator_tools`).
**Gate:** `tests/test_catalogue_cost_loadbearing.py` (7 tester). **Suite:** 1066 passed / 5 skipped.
**Golden `demo-transcript.stdout` byte-uendret** (`ea8c534773acdbe41ae68f2c55724d69aaf8be4f`).
**Eksponerings-grense (arvet fra syretesten, holdt):** dette repoet pusher til `open/`. Rapporten
bærer kun tall, stier, kommandoer og egne observasjoner. Ingen bundle-fil er kopiert, og ingen
kravtekst er gjengitt — alle konsept-tall er lengdemålinger, ikke innhold.
---
## 1. Instrumentet, validert før noe tall ble trodd
`tiktoken.get_encoding("o200k_base")` over `json.dumps(list_bundles(), ensure_ascii=False)`, kjørt
med `uv run --with tiktoken` (`tiktoken` er fortsatt **ikke** en prosjekt-avhengighet). Samme
instrument som syretesten 25.08.
**Kjent-positiv kontroll (Verifiseringsloven ansikt 4):** de tre flate Vegnormal-basene målte
**201 196 tegn / 112 116 tokens** før endringen — tallet syretesten publiserte, reprodusert eksakt.
Uten den kontrollen ville «etter»-tallet vært en måling ingen visste kunne treffe.
## 2. Før og etter, med nevner
| Katalog | Baser (nevner) | Tegn før | **Tokens før** | Tegn etter | **Tokens etter** | Endring |
|---|---:|---:|---:|---:|---:|---:|
| Tre flate baser (syretestens sett) | 3 | 201 196 | **112 116** | 877 | **362** | **99,7 %** |
| Grener, N100:2023 | 40 | 63 375 | **33 889** | 11 679 | **5 017** | 85,2 % |
| Grener, N200:2024 | 99 | 134 667 | **71 726** | 28 894 | **12 396** | 82,7 % |
| Grener, N500:2024 | 32 | 36 569 | **19 329** | 9 408 | **4 037** | 79,1 % |
| **Alle grener samlet** | **171** | 234 611 | **124 942** | 49 981 | **21 448** | **82,8 %** |
| *kontroll:* commons' tre eksempelbaser | 3 | 11 050 | 3 472 | 989 | 321 | 90,8 % |
Per base: **731 → 125 tokens** i grenformen, **37 372 → 121** i den flate.
**Nevner-avvik mot ordren, uttalt:** ordren oppgir 34 grener for N100:2023. Målt på disk
(`ls ~/repos/vegnormal-okf/build | grep -c '^B-n100-2023-gren-.*-importert$'`) er tallet **40**.
N200:2024 = 99 og N500:2024 = 32 stemmer. Tallene over bruker den målte nevneren, ikke ordrens.
**Ordrens hypotese bekreftet:** grenformen lukket bundle-siden og gjorde katalogsiden **verre**
124 942 tokens over 171 grener mot 112 116 over tre flate baser. Etter endringen er hele
maksimal-konfigurasjonen 21 448 tokens, altså under en femtedel av ett 128k-vindu.
## 3. Hva som ble endret, og hvorfor akkurat det
`list_bundles` returnerte `Bundle.index_summary`**hele** rot-indeksens body — for **hver**
konfigurert base samtidig, pluss ett JSON-objekt per ufulgt kryss-lenke. Begge vokser med korpuset.
Prisen på å finne ut *hvilke baser som finnes* ble altså satt av hvor mye de basene *inneholder*.
Det er progressiv disclosure snudd på hodet (målbilde §2/§4): katalogen er stigens billigste trinn,
og den var loopens dyreste kall.
Hver oppføring er nå bundet ved konstruksjon: `id`, en **ordrett prefiks** av indeksbodyen på maks
`_CATALOGUE_EXCERPT_CHARS` (200) tegn, `index_truncated`, `documents`, `verdict_count`,
`cost_baseline`, `unreachable_links`. Kostnaden er **O(baser), aldri O(korpus)**.
**Et premiss ble felt FØR noe ble bygget på det.** «Indeksbodyen forteller en manager hva basen
handler om» er **usant** for maskin-importerte baser: grenbasenes `index.md` har verken frontmatter
eller prosa — den er en ren lenkeliste (målt: `B-n200-2024-gren-1-1-importert/index.md`, 959 bytes,
første tegn er `-`). Feltet var altså ikke bare dyrt, det var dyrt **og** innholdsløst der. En
avkortet prefiks taper ingenting en manager brukte.
Tre valg, hvert med sin begrunnelse:
- **Fast vindu, ikke en andel av basen.** En andel ville skalert med korpuset igjen — bare med en
mindre konstant. Mutasjon M8 (`body[:len(body)//10]`) er rød mot nettopp den formen.
- **Avkorting ANNONSERES, som felt.** `index_truncated` står **ved siden av** utdraget, ikke som en
markør limt inn i det — samme grunn som `BudgetExceeded` bærer trippelen sin som felt (kø-(y)):
en konsument som må re-parse prosa for å vite om den holder helheten, har fått en diagnostikk den
ikke kan handle på. En base som **passer** blir ikke merket avkortet og får hele bodyen —
omisjon, aldri en løgn i noen av retningene (M3 og M4 er begge røde, hver sin vei).
- **En ufulgt kryss-lenke overlever som ANTALL.** Fakta fra økt 51 («et hopp er tolerert, men ikke
lenger taust») står; per-lenke-detaljen blir liggende der den er handlingsbar — på
`RunResult.skipped_links` / `DryRunReport.skipped_links` — og rir ikke med i et kall hvis hele
jobb er å være billig.
Hele indeksen er fortsatt **ett** `read_file(id, "index.md")` unna. Dette er et disclosure-nivå,
ikke datatap.
## 4. Gaten
Taket (500 tegn per base) bor i **testen**, ikke i `explore.py`: en test som importerte
implementasjonens budsjett ville flyttet seg med det, og å heve budsjettet er nøyaktig den
regresjonen fila finnes for.
Syv armer: (a) taket over mange store baser · (b) kostnaden sporer **ikke** korpus-størrelse ·
(c) katalogen identifiserer fortsatt hver base — anti-vakuitets-armen, uten den består «returner
bare id-ene» (a) perfekt · (d) avkorting annonsert, utdraget ordrett, begge retninger · (e) stigen
er intakt, hele indeksen er ett kall unna · (f) ufulgte lenker overlever som antall ·
(g) **kontrollen** — én bases rå indeksbody alene sprenger taket for hele katalogen, så en grønn (a)
betyr at bindingen fyrte, ikke at fixturen var liten.
**Ni mutasjoner, alle røde mot HELE suiten, hver med sin egen signatur** (grønn kontroll 1066/5):
| # | Mutasjon | Røde tester |
|---|---|---:|
| M1 | ingen binding i det hele tatt (tilbake til hele bodyen) | 4 |
| M2 | bundet, men vakuøst (utdraget tømt) | 2 |
| M3 | avkorting ikke annonsert (stille kutt) | 1 |
| M4 | over-annonsert (en hel indeks meldt avkortet) | 1 |
| M5 | per-lenke-lista rir med igjen | 1 |
| M6 | det ufulgte faktumet slettet | 1 |
| M7 | bundet, men suffiks i stedet for ordrett prefiks | 2 |
| M8 | en andel av basen i stedet for et fast vindu | 2 |
| M9 | størrelses-signalet (`documents`) gjort konstant | 1 |
M9 ble kjørt fordi `documents` var et felt uten gate — et felt ingen test kan se, råtner.
## 5. Grenser, uttalt
- **MAJOR-1 (repeterbart `--bundle-dir`) var ikke nødvendig for dette.** Bindingen sitter i
verktøykroppen, bak en uendret CLI-flate. Operatørbeslutning 26.08: repeterbart `--bundle-dir`
er **nei** — multi-base forblir bibliotek-partisjonen (`run_mandate_across_bundles`).
- **`navigate_bundle` kalles fortsatt per base per katalogkall**, altså leses hver fil på disk for å
telle dokumenter og dommer. Det er I/O og veggklokke, ikke tokens, og er utenfor denne
bestillingen — men det er ikke gratis ved 171 baser, og det er ikke målt her.
- **Ingen levende modell har kalt det nye verktøyet.** Formen er bevist offline; at en manager
faktisk velger bedre med et utdrag enn med hele indeksen er ikke målt (samme klasse som
structured-output-grensen).
- **N101 er ikke berørt** — utenfor bestillingen (operatørpresisering 26.08).

View file

@ -689,6 +689,32 @@ def _resolve_bundle(index: Mapping[str, str], bundle_id: str) -> str:
return index[bundle_id]
#: Characters of the root index body one catalogue entry may carry. The catalogue's job is to let a
#: manager pick a base, not to read one, so the excerpt is a fixed-size window rather than a share
#: of the base: cost then scales with how many bases are configured, which the operator chose, and
#: not with how much they contain, which they did not. The number is deliberately small — a headline
#: and a first line or two — and the ceiling that guards it lives in the TEST, not here, because
#: raising this constant is the regression the gate exists to catch.
_CATALOGUE_EXCERPT_CHARS: Final = 200
def _index_excerpt(body: str) -> tuple[str, bool]:
"""A bounded VERBATIM prefix of an index body, plus whether anything was cut.
Cut at the last line break inside the budget, never mid-line: half a markdown link is a target a
model may well try to follow, and a path that never existed is worse than no path. A first line
longer than the whole budget has no break to cut at, and is cut hard the bound is the promise.
Returns the body UNTOUCHED and ``False`` when it already fits: an index that was never cut must
not be reported as cut (omission, never a lie in either direction ``cost_baseline_notice``'s
rule applied to a value instead of a line).
"""
if len(body) <= _CATALOGUE_EXCERPT_CHARS:
return body, False
cut = body.rfind("\n", 0, _CATALOGUE_EXCERPT_CHARS + 1)
return (body[:cut] if cut > 0 else body[:_CATALOGUE_EXCERPT_CHARS]), True
def navigator_tools(bundle_dirs: Sequence[str]) -> list[FunctionTool]:
"""The navigator's three tools: survey the catalogue, open one base, read one document.
@ -697,6 +723,22 @@ def navigator_tools(bundle_dirs: Sequence[str]) -> list[FunctionTool]:
without one cannot have its hypotheses reconciled against the project's real cost lines, and a
manager that does not know which bases are anchored cannot plan around it (§ C.7).
**The catalogue costs O(bases), never O(corpus)**, and that is the rung's whole point. It used
to return the WHOLE root index body per base plus one object per unfollowed cross-link both
corpus-sized so the cheapest rung of the ladder was the most expensive call in the loop:
112 116 o200k_base tokens over three flat Vegnormal bases, 124 942 over the 171 branch bases
that replaced them (measured 25.-26.08; ``tests/test_catalogue_cost_loadbearing.py``). Each
entry is now bounded by construction: a ``_CATALOGUE_EXCERPT_CHARS``-long VERBATIM prefix of the
index body plus counts. The full index stays exactly one ``read_file(id, "index.md")`` away, so
this is a disclosure level, not data loss.
**Truncation is ANNOUNCED, never silent** ``index_truncated`` is a field beside the excerpt,
not a marker glued into it, for the reason ``BudgetExceeded`` carries its triple as fields
(-(y)): a consumer that has to re-parse prose to learn whether it is holding the whole thing
has been handed a diagnostic it cannot act on. And an unfollowed cross-link keeps a COUNT here
(session 51's "a skip is tolerated but no longer silent") while its per-link detail stays where
it is actionable, on ``RunResult.skipped_links`` / ``DryRunReport.skipped_links``.
``read_bundle`` returns ``okf.bundle_context``, which EXCLUDES the ``type: verdict`` layer by
construction prior verdicts reach a hypothesis only through the gated ExpeL fold inside
``run_project``, never by being read as context here.
@ -706,28 +748,30 @@ def navigator_tools(bundle_dirs: Sequence[str]) -> list[FunctionTool]:
@tool(
name="list_bundles",
description=(
"List the knowledge bases available to this exploration: id, what the index says the "
"base is about, how many prior expert verdicts it holds, and whether it ships a cost "
"baseline (without one, numbers cannot be reconciled against the project's own)."
"List the knowledge bases available to this exploration: id, the OPENING of what the "
"index says, how many documents and prior expert verdicts the base holds, whether it "
"ships a cost baseline (without one, numbers cannot be reconciled against the "
"project's own), and how many cross-links could not be followed. The excerpt is cut "
"when index_truncated is true; read_file(id, 'index.md') gives the whole index."
),
)
def list_bundles() -> list[dict[str, Any]]:
catalogue: list[dict[str, Any]] = []
for bundle_id, bundle_dir in index.items():
bundle = okf.navigate_bundle(bundle_dir)
excerpt, truncated = _index_excerpt(bundle.index_summary)
catalogue.append(
{
"id": bundle_id,
"index_summary": bundle.index_summary,
"index_excerpt": excerpt,
"index_truncated": truncated,
"documents": len(bundle.context_files),
"verdict_count": len(bundle.verdicts),
# Tolerant on CONTENT, fail-fast on the PATH: an operator's bad directory is
# refused by navigate_bundle above, while a navigable base that simply has no
# baseline is legitimate (load_optional_cost_baseline's own contract).
"cost_baseline": okf.load_optional_cost_baseline(bundle_dir) is not None,
"skipped_links": [
{"from_file": s.from_file, "target": s.target, "reason": s.reason}
for s in bundle.skipped
],
"unreachable_links": len(bundle.skipped),
}
)
return catalogue

View file

@ -0,0 +1,183 @@
"""The catalogue call costs O(bases), never O(corpus) — and what it drops, it SAYS it dropped.
Measured 2026-08-25 (session 60, ``docs/2026-08-25-syretest-vei-ab.md``) and re-measured 26.08 with
the same instrument (``tiktoken`` ``o200k_base``, run through ``uv run --with tiktoken``, validated
first against the three commons example bundles whose numbers commons itself publishes):
list_bundles() over the three flat Vegnormal bases -> 201 196 chars / 112 116 tokens
list_bundles() over the 171 branch bases -> 234 611 chars / 124 942 tokens
The branch form (``vegnormal-okf`` ``8145c23``) closed the *bundle* side ``read_bundle`` fell 82-92
percent and made the *catalogue* side WORSE, exactly as that repo predicted: one call now costs
more than a 128k window, before the manager has read a single document.
The cause is in this repo. ``list_bundles`` returned ``Bundle.index_summary`` the WHOLE root index
body for EVERY configured base at once, plus one JSON object per unfollowed cross-link. Both grow
with the corpus, so the price of *finding out which bases exist* was set by how much those bases
contain. That is the opposite of progressive disclosure (målbilde §2/§4): the catalogue is the
cheapest rung of the ladder, and it was the most expensive.
**A MEASURED premise, felled before anything was built on it:** "the index body tells a manager what
the base is about" is FALSE for machine-imported bases. The branch bases' ``index.md`` carries no
frontmatter and no prose it is a pure link list (measured: ``B-n200-2024-gren-1-1-importert``,
959 bytes, first byte is ``-``). So the old field was not merely expensive, it was expensive AND
uninformative there; a truncated prefix loses nothing a manager was using.
**The ceiling lives in this file, not in ``explore.py``.** 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. 500 characters per base is the number, chosen with headroom over the measured entry so that
ordinary field growth does not force a rewrite, and small enough that all 171 branch bases fit in
well under a tenth of the window they used to need.
What the arms pin, and what each one refuses:
(a) the bound itself, over many large bases refuses the unbounded form;
(b) cost does NOT track corpus size the same base with ten times the index costs the SAME, which
is the property "O(bases), not O(corpus)" stated directly rather than inferred from (a);
(c) the catalogue still IDENTIFIES what it lists without this, "return only the ids" passes (a)
perfectly and delivers a manager nothing to choose between (the repo's vacuous-gate class);
(d) truncation is ANNOUNCED, never silent, and the excerpt is a VERBATIM prefix validation, never
repair (``write_concept_file``'s rule). A base whose index FITS is not marked truncated and gets
its whole body: omission, never a lie in either direction;
(e) the ladder is intact ``read_file(id, "index.md")`` still returns the COMPLETE index, so the
bound is a disclosure level, not data loss;
(f) an unreachable link survives as a COUNT the fact stays visible (session 51's "a skip is
tolerated but no longer silent"), while the unbounded per-link detail does not ride along in a
call whose whole job is to be cheap. The detail is still carried where it is actionable, on
``RunResult.skipped_links`` / ``DryRunReport.skipped_links``;
(g) the CONTROL the ceiling is proved to discriminate. One base's raw index body alone exceeds the
budget for the entire catalogue, so a green (a) means the bound fired, not that the fixture was
small (Verifiseringsloven face 4: a gate that can only pass proves nothing).
"""
from __future__ import annotations
import json
from pathlib import Path
from portfolio_optimiser.explore import navigator_tools
#: Characters per base the catalogue may cost. Test-owned on purpose — see the module docstring.
_CEILING_CHARS_PER_BASE = 500
#: Big enough that the old form blew the ceiling by two orders of magnitude (arm (g) measures it).
_MANY_CONCEPTS = 300
def _write_base(root: Path, name: str, concepts: int, *, dangling: int = 0) -> str:
base = root / name
base.mkdir(parents=True)
lines = [f"# {name}", "", "Denne basen dekker et avgrenset fagområde.", ""]
for i in range(concepts):
lines.append(f"- [konsept-{i:04d}](konsept-{i:04d}.md)")
(base / f"konsept-{i:04d}.md").write_text(
f"---\ntype: concept\n---\n\n# Konsept {i}\n\nInnhold.\n", encoding="utf-8"
)
for i in range(dangling):
lines.append(f"- [borte-{i:04d}](borte-{i:04d}.md)")
(base / "index.md").write_text(
"---\ntype: index\n---\n\n" + "\n".join(lines) + "\n", encoding="utf-8"
)
return str(base)
def _catalogue(dirs: list[str]) -> list[dict[str, object]]:
tools = {t.name: t for t in navigator_tools(tuple(dirs))}
return tools["list_bundles"].func()
def _blob(entries: object) -> str:
return json.dumps(entries, ensure_ascii=False)
def test_catalogue_cost_is_bounded_per_base(tmp_path: Path) -> None:
"""(a) Many large bases, one call: the payload stays under a per-base ceiling."""
dirs = [_write_base(tmp_path, f"base-{n}", _MANY_CONCEPTS) for n in range(5)]
blob = _blob(_catalogue(dirs))
assert len(blob) <= len(dirs) * _CEILING_CHARS_PER_BASE, (
f"catalogue cost {len(blob)} chars over {len(dirs)} bases exceeds the ceiling "
f"{len(dirs) * _CEILING_CHARS_PER_BASE}"
)
def test_catalogue_cost_does_not_track_corpus_size(tmp_path: Path) -> None:
"""(b) Ten times the index, the same price. O(bases), not O(corpus)."""
small = _write_base(tmp_path, "small", 30)
large = _write_base(tmp_path, "large", 300)
entries = {str(e["id"]): e for e in _catalogue([small, large])}
small_entry, large_entry = entries["small"], entries["large"]
# The excerpt is a fixed window, so ten times the index yields the same number of characters.
# (The counts beside it grow by a DIGIT, which is honest and logarithmic — this arm is about
# the field that used to grow linearly, and asserting on the whole blob would measure that
# digit instead of the property.)
assert len(str(large_entry["index_excerpt"])) == len(str(small_entry["index_excerpt"]))
assert len(_blob(large_entry)) <= _CEILING_CHARS_PER_BASE
def test_catalogue_still_identifies_every_base(tmp_path: Path) -> None:
"""(c) The anti-vacuity arm: bounded is not the same as empty."""
dirs = [_write_base(tmp_path, f"base-{n}", 50) for n in range(3)]
entries = _catalogue(dirs)
assert sorted(str(e["id"]) for e in entries) == ["base-0", "base-1", "base-2"]
for entry in entries:
excerpt = str(entry["index_excerpt"])
assert excerpt.strip(), "a catalogue that says nothing about a base cannot be chosen from"
assert str(entry["id"]) in excerpt
# How big the base is, is part of choosing one: with the index body no longer riding along,
# this count is the only thing left that says what read_bundle would cost.
assert entry["documents"] == 50
def test_truncation_is_announced_and_the_excerpt_is_verbatim(tmp_path: Path) -> None:
"""(d) Both directions: a cut index SAYS it was cut; a whole one is not marked, and is whole."""
from portfolio_optimiser import okf
large = _write_base(tmp_path, "large", 300)
tiny = _write_base(tmp_path, "tiny", 1)
entries = {str(e["id"]): e for e in _catalogue([large, tiny])}
assert entries["large"]["index_truncated"] is True
body = okf.navigate_bundle(large).index_summary
assert body.startswith(str(entries["large"]["index_excerpt"]))
assert len(str(entries["large"]["index_excerpt"])) < len(body)
assert entries["tiny"]["index_truncated"] is False
assert str(entries["tiny"]["index_excerpt"]) == okf.navigate_bundle(tiny).index_summary
def test_the_full_index_is_still_one_call_away(tmp_path: Path) -> None:
"""(e) The bound is a disclosure LEVEL, not data loss."""
large = _write_base(tmp_path, "large", 300)
tools = {t.name: t for t in navigator_tools((large,))}
whole = tools["read_file"].func(bundle_id="large", path="index.md")
assert whole.count("- [konsept-") == 300
def test_unreachable_links_survive_as_a_count(tmp_path: Path) -> None:
"""(f) The fact stays; the unbounded per-link detail does not ride along."""
base = _write_base(tmp_path, "holes", 20, dangling=40)
entry = _catalogue([base])[0]
assert entry["unreachable_links"] == 40
assert len(_blob([entry])) <= _CEILING_CHARS_PER_BASE
def test_control_the_unbounded_form_would_blow_the_ceiling(tmp_path: Path) -> None:
"""(g) The ceiling discriminates — proved, not assumed."""
from portfolio_optimiser import okf
large = _write_base(tmp_path, "large", _MANY_CONCEPTS)
body = okf.navigate_bundle(large).index_summary
assert len(body) > 5 * _CEILING_CHARS_PER_BASE