# Syretest S7a — K2 → kunnskapsbase → utforskning → revise **Dato:** 2026-09-03 · **Ordre:** `20260902T233842Z-2955810469-from-.claude` · **Grunnlag:** `~/.claude/docs/helhetlig-plan-okf-og-portfolio-optimiser.md` § 0, § 5, § 9.1 Dette er en **måling**. Ingen produksjonskode er endret; hele måleoppsettet ligger utenfor treet (`scratchpad/s7a/`, utracket) og hver tabell under er produsert av en kommando som står i teksten. **Ingen validert besparelse er forventet eller funnet** — K2 er pre-award og prisskjemaet er uutfylt (plan § 9.1). Det som måles er navigasjonen, dialogen, revise-sløyfa, falsifiseringen mot proveniens og tokenprofilen. --- ## 0. Instrumentet, validert mot en kjent positiv FØR bruk Alle tokentall er `tiktoken` `o200k_base` over den fulle prompt-blobben (tekst + `function_call` + `function_result`), kjørt med `uv run --with tiktoken` — `tiktoken` er fortsatt ikke en prosjektavhengighet. | Kjent-positiv sjekk | Publisert fasit | Målt nå | |---|---|---| | `read_bundle` over `tunnel-hauglia` | 259 tok (`docs/2026-09-02-read-bundle-kontekstkostnad.md`) | **259 tok** | | `bundle_context` over `tunnel-hauglia` | 12 595 tok (samme) | **12 595 tok** | Instrumentet reproduserer begge eksakt. Først etter det er K2-tallene under lest som tall. **To instrumentfeil ble fanget underveis, og begge er skrevet ned fordi de er samme klasse som misjonsreview v2 rapporterte.** (i) Første prompt-sonde leste `repr(Content)` og fikk `` for **hver** melding — en prompt som så tom ut mens den bar 108 000 tegn. Sonden henter nå `text`/`result`/`arguments` ut av hvert `Content`. (ii) Den rettede sonden summerte så `Message.text` **og** hvert `Content.text`, altså tekstdelen to ganger. Den feilen ble ikke synlig i totalen — den ble synlig først da § 5s sammensetning ble målt, og det er hele grunnen til at sammensetningen ble målt i det hele tatt: en total ingen har brutt ned er en total ingen har kontrollert. --- ## 1. Kunnskapsbasen slik den faktisk er levert Bygget av `llm-ingestion-okf` 02.09 (`tools/okf_corpus_run.py`, steg 17) til `/tmp/k2-trinn1-bundle`; kopiert til `scratchpad/s7a/k2-bundle` og målt der (`/tmp` er flyktig). | Egenskap | Målt | |---|---| | Konseptfiler (`context_files`) | **39** | | Filer totalt (`files`) | 40 (39 + `index.md`) | | `type: verdict`-lag | 0 | | Ufulgte kryss-lenker (`skipped`) | **0** | | `bundle_id` / opphav | `k2-bundle` / **`mount-derived`** | | `index.md`-body | 6 044 tegn, ingen frontmatter, ren lenkeliste | | `log.md` (OKF § 7) | **finnes ikke** | **Ordrens `N=43, merged 39` er delvis verifiserbart.** `merged = 39` er målt direkte (39 konseptfiler). `N = 43` er produsentens tall og kan **ikke** verifiseres fra bundelen: den bærer ingen `log.md` og ingen manifest-oppføring som teller inn-dokumentene. Det står som produsentens påstand, ikke som en måling gjort her. ### `adjudication`-nøkkelen: fraværende, med nevner ``` cd scratchpad/s7a/k2-bundle && grep -l '^adjudication:' *.md | wc -l # 0 grep -l '^type:' *.md | wc -l # 39 (kjent-positiv kontroll) ``` Frontmatter-folketellingen over alle 39, med sifre i nøkkelnavnet tatt med: | Nøkkel | Antall (av 39) | |---|---| | `type`, `title`, `source_file`, `source_sha256`, `ingested_at`, `generated`, `derived` | 39 | | `references` | 20 | | **`adjudication`** | **0** | | **`verified`** | **0** | | **`sources`** | **0** | **Ordrens premiss om «den nye profilen (`adjudication`)» holder ikke mot det som ble levert.** Bundelen bærer ingen `adjudication`-nøkkel og ingen provenienss-nøkkel i det hele tatt. Det er et faktum om bundelen, ikke om leseren — se § 4, der gaten bevises å kunne si ja. ### Navigasjonskostnad, per kall | Kall | Tegn | o200k-tokens | |---|---|---| | `list_bundles` (1 base) | 352 | **127** | | `read_bundle("k2-bundle")` | 6 244 | **2 312** (39 oppføringer) | | `read_file` × 39, sum | — | **684 236** | | `read_file`, median / min / maks | — | 6 709 / 541 / **121 462** | | `bundle_context` (formen før MAJOR-3) | — | **682 303** | Tre ting følger av tabellen. **(a) MAJOR-3 er det som gjør K2 navigerbar i det hele tatt.** Den gamle `read_bundle`-kroppen returnerte `bundle_context` — 682 303 tokens for denne basen, altså mer enn noe kontekstvindu tar, og den ville ridd med i hver senere prompt. Den nye formen koster 2 312. Reduksjonen er **99,7 %**, og forskjellen her er ikke «billigere», den er «mulig mot umulig». **(b) Katalogtaket holder på et 39-dokuments korpus.** `list_bundles` koster 127 tokens, og `index_truncated` kommer tilbake `true` — de 6 044 tegnene i indeksen blir til et 200-tegns utdrag. Det bekrefter det `docs/2026-08-26-katalogkostnaden.md` forutså for maskin-importerte baser: utdraget er nesten innholdsløst (første lenkelinje), men det er *bundet*, og hele indeksen er ett `read_file(id, "index.md")` unna. **(c) `read_bundle` er O(dokumenter), og på 39 dokumenter er det 2 312 tokens.** Det målte faktumet: `tests/test_read_bundle_cost_loadbearing.py` setter taket til 1 500 **tegn** mot 3-dokuments baser, og K2s `read_bundle` er 6 244 tegn. Det er ikke et brudd — gaten måler sine egne baser — men tallet er verdt å legge fram for den som eier gaten. Hva taket *er ment* å binde, er deres avgjørelse, ikke min lesning her. **(d) To dokumenter sprenger § 5s tokenkriterium hvis de leses hele.** Kriteriet er «ingen enkeltpost > 100 000 tok uten en navngitt grunn». To av 39 passerer det som `read_file`: | Dokument | Tokens | |---|---| | `inbox-del-ii-bilag-3-1-milj-teknisk-rapport-med-tiltaksplan.md` | 121 462 | | `inbox-del-ii-bilag-9-1-avtale-som-tiltransporteres-norconsult-as.md` | 119 909 | Grunnen er navngitt her: begge er enkeltdokumenter fra korpuset som ikke er delt opp i konsepter. De ble **ikke** lest i kjøringene under — men en navigatør som velger dem, betaler det. --- ## 2. Dialogen: én offline utforskning med verktøyspor ```bash PYTHONIOENCODING=utf-8 uv run python -m portfolio_optimiser.run K2-STANGE \ --docs-dir scratchpad/s7a/k2-bundle --bundle-dir scratchpad/s7a/k2-bundle \ --explore "Finn kostnadsbesparelser i Stange skole-anbudet" \ --explore-config scratchpad/s7a/explore-config.json \ --scripted-replies scratchpad/s7a/scripted-replies.json \ --outbox-dir scratchpad/s7a/outbox --run-id s7a-offline ``` MAJOR-1s trinn-manus er brukt: navigatøren får `{"call": …}`-steg, manageren én tekst per stadium (fakta, plan, tre ledgere, sluttsvar). Uten manager-manuset avslutter kjøringen etter én runde uten å gi noen deltaker en tur — det er MAJOR-1s egen måling, og den er grunnen til at manageren er skriptet i stadier. `{run_id}-exploration.json`: | Felt | Målt | |---|---| | `completed` | `true` | | `stop` | `null` | | `tool_calls` | **`list_bundles` → `read_bundle(k2-bundle)` → `read_file(k2-bundle)`**, i kallrekkefølge | | `rounds` | 3, alle med `speaker_known: true` og en ekte deltaker som `next_speaker` | | `approaches` (i mandatet) | 1 — `hypothesis-1 — SENTINEL-ALFA prisskjema-post 82` | **Plan § 5s dialog-rad er oppfylt og målt:** ≥ 1 `read_bundle`/`read_file`-kall er logget i artefaktet. Kjøringen er ikke-vakuøs i den forstand MAJOR-1 definerte: tre runder, kjent taler hver gang, tre verktøykall, ett mandat. ### Kjøringen ender likevel rc = 1, og det er S7bs skarpeste funn ``` run refused: IR projection not found in bundle: 'validator-input.json' ``` Utforskningen fullførte, mandatet ble kunngjort — og så nektet pipelinen. Bundelen bærer ingen `validator-input.json`, så `run_project`s bundle-arm kan ikke slå opp prosjektet. Artefaktet overlevde fordi det skrives fra en `finally`, som er nøyaktig hva den konstruksjonen finnes for. Dette er et **mer spesifikt** S7b-gap enn «K2 har ingen priser»: selv med priser ville kjøringen stoppet her. Ingesterings-profilen emitterer ingen IR-projeksjon. **Kryss-repo-funn for `llm-ingestion-okf`.** ### Levende kjøring: ikke kjørt Ordrens escape-klausul fyrer. Målt, ikke antatt: | Variabel / fil | Tilstand | |---|---| | `PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT` | tom/usatt | | `FOUNDRY_PROJECT_ENDPOINT` | tom/usatt | | `PORTFOLIO_MODEL_MAP` | tom/usatt | | `OPENAI_API_KEY` / `AZURE_OPENAI_API_KEY` | tom/usatt | | `data/model_map.json` | finnes ikke | `az account show` svarer riktignok med en konto, men Fase 4e målte at AZURE-armen slår opp et deployment-navn i modell-mappet **før** noen klient bygges — uten mappet er en innlogget CLI ikke nok. **Levende kjøring ikke gjort, mangler nøkkel og modell-map.** Ingen konflikt med STATE-ens `IKKE RØR AZURE` oppsto. --- ## 3. Revise-sløyfa ```bash printf 'revise SENTINEL-BRAVO: …\napprove\n' | uv run python -m portfolio_optimiser.run K2-STANGE \ … --explore-config scratchpad/s7a/explore-config-review.json --plan-review \ --outbox-dir scratchpad/s7a/outbox-review --run-id s7a-review ``` | Egenskap | Målt | |---|---| | Plan-reviewer stilt | **2** (`#1` besvart `revise`, `#2` besvart `approve`) | | Manageren replanla mellom dem | ja — plan `#1` 1 115 tegn, plan `#2` 1 132 tegn, ulik tekst | | Tilbakemelding i artefaktet | **ordrett**, `SENTINEL-BRAVO: …` | | `"object at"` i artefakt eller stdout | **0 forekomster** (BLOCKER-1 bekreftet lukket på begge dørene) | | `current_progress` | `"(no progress ledger yet)"` — ikke ordet `None` (PM-tillegg 4 bekreftet lukket) | ### Mandat-diffen er TOM, og grunnen er instrumentet — ikke systemet Mandatet bygges av hypotesiserens `HYPOTHESIS:`-merkede turer. Under et **konstant** manus sier hypotesiseren det samme uansett hva revisjonen sa, så før/etter er identiske ved konstruksjon. Å rapportere den tomme diffen som et funn ville vært å lese et instrumentresultat som et faktum om verden. Det som *kan* måles offline er **kjeden**: hvilke prompter bærer operatørens tilbakemelding? Med to ASCII-sentineler (norske tegn serialiseres escaped i prompten og ville gitt falske nuller — samme felle misjonsreview v2 rapporterte): | Ledd | Bærer | Målt | |---|---|---| | 1. Operatørens revise-tekst → managerens prompt | `SENTINEL-BRAVO` | **6 av 8** manager-kall | | 1b. Samme tekst → deltakernes prompt | `SENTINEL-BRAVO` | **0 av 4** navigatør, **0 av 1** hypotesiser | | 2. Managerens post-revise-output → hypotesiserens prompt | `SENTINEL-CHARLIE` i `instruction_or_question` | **1 av 1** | | 2b. Samme, via plan-TEKSTEN | revidert plan-streng | **0 av 1** hypotesiser, **0 av 4** navigatør | | 3. Endret hypotese ut av deltakeren | — | **ikke målbar offline** | **Mekanismen, uttalt:** operatørens tilbakemelding når **manageren**, aldri deltakerne direkte. Den når deltakerne bare gjennom det manageren selv skriver i neste rundes `instruction_or_question` — og det er bevist diskriminerende: sentinelen manageren skrev *etter* revisjonen står i hypotesiserens prompt, mens plan-teksten (verken den opprinnelige eller den reviderte) gjør det ikke. Sløyfa er altså hel, men leddet som faktisk former hypotesen er managerens **omskriving** av tilbakemeldingen. Under et skriptet manus er den omskrivingen min, ikke en modells. **At en revise endrer hva hypotesiseren foreslår er derfor ikke bevist her, og kan ikke bevises offline** — det krever en levende manager. Det er S7bs andre åpne krav. --- ## 4. Falsifisering mot proveniens (K5-terskelen) Ordren ber om et K2-konsept med `adjudication: proposed`. **Det finnes ikke: 0 av 39.** Terskelen er derfor kjørt på det korpuset faktisk bærer, mot en påstand om den ene prisede posten: > «Post 82 *Prosjektering (tiltransport av prosjekterende)* i Bilag 7 Prisskjema er priset til > 5 647 500 NOK og er derfor en kandidat for kostnadskutt.» | | K2 slik den er levert | Kjent-positiv kontroll (patchet kopi) | |---|---|---| | `adjudication_for` | `unknown` | `adjudicated` | | `(state, reason, items_seen)` | `(absent, None, 0)` | `(present, None, 1)` | | `trust_tier` | `None` | `human-reviewed` | | `admits_falsification` | **`False`** | **`True`** | Over alle 39, med nevner: `adjudication` `unknown` 39/39 · `evidence.state` `absent` 39/39 · `tier` `None` 39/39 · `admits_falsification` `False` **39/39**. **Kontrollen er bærende.** Uten den er «alle 39 diskontert» ikke til å skille fra «funksjonen diskonterer alltid» — repoets vakuøs-gate-klasse. Kontrollen er en kopi under `scratchpad/s7a/k2-control/` der ett konsept har fått `verified: { by: human:…, at: … }` + `adjudication: adjudicated`; gaten flipper `False → True`. Den leverte bundelen er aldri skrevet til. **Dommen, slik skillen krever den:** `undecided`, aldri `survived`. Diskonteringen sies eksplisitt med trippelen, ordrett fra `evidence_notice`: ``` provenance absent in …/inbox-del-ii-bilag-7-prisskjema.md; items_seen=0 ``` Og med skillens egen regel: en gjendrivelse som ikke navngir en gjendriver er ingen gjendrivelse — her finnes ingen kilde som klarerer terskelen, så «vi kan ikke bekrefte» må aldri skrives om til «påstanden er gal». --- ## 5. Tokenprofil Ekte `usage` finnes ikke: `ScriptedChatClient` rapporterer 8 tokens per svar, så artefaktets `tokens_spent` (88 offline, 104 med review) er **syntetisk** og sier ingenting om kostnad. Profilen under er derfor instrument-målt over de faktiske prompt-blobbene i revise-kjøringen. > **Instrumentfeil funnet og rettet før dette tallet ble stående.** Første sonde summerte > `Message.text` **og** deretter hvert `Content.text` — men `Message.text` *er* sammenkjedingen av > meldingens tekst-innhold, så tekstdelen ble talt to ganger. Feilen kom for dagen først da > sammensetningen under ble målt, ikke da totalen ble lest. Tallene i denne seksjonen er fra den > rettede sonden (`m5_prompt_composition.py`); det forkastede tallet var 45 643. **13 prompter, 40 320 o200k-tokens.** | Rolle | Prompter | Tokens | Andel | |---|---|---|---| | manager | 8 | 24 465 | 61 % | | navigator | 4 | 9 251 | 23 % | | hypothesiser | 1 | 6 604 | 16 % | De tre største postene — til sammen 22 037 tokens, **55 % av hele kjøringen**: | # | Post | Tokens | |---|---|---| | 1 | manager-kall 6 (ledger etter at navigatøren hadde lest prisskjemaet) | 7 532 | | 2 | manager-kall 5 (samme, runden før) | 7 468 | | 3 | manager-kall 7 (sluttsvar) | 7 037 | **Hva de består av — målt, ikke antatt.** Hver av de tre er brutt ned per innholdstype: | Del | manager #6 | manager #5 | manager #7 | |---|---|---|---| | verktøy**resultater** | 6 527 (87 %) | 6 527 (87 %) | 6 527 (93 %) | | tekst (fakta, plan, ledger-historikk) | 965 (13 %) | 901 (12 %) | 470 (7 %) | | verktøy**argumenter** | 40 (0 %) | 40 (0 %) | 40 (0 %) | Og de 6 527 tokenene enkeltvis, i den rekkefølgen navigatøren hentet dem: | Verktøyresultat | Tokens i prompten | Andel av de 6 527 | |---|---|---| | `list_bundles` | 130 | 2 % | | `read_bundle` | 2 354 | 36 % | | **`read_file` (Bilag 7 Prisskjema)** | **4 043** | **62 %** | `read_file`-resultatet er altså 4 043 tokens i prompten og 4 043 tokens målt isolert — samme tall, så det som rir med er hele returverdien, ikke en forkortet form. (`read_bundle` er 2 354 i prompten mot 2 312 isolert; differansen er JSON-innrammingen meldingen legger rundt.) **Mekanismen er den samme som MAJOR-3 målte, ett trinn ned på stigen.** Utforskningens deltakere deler **én** samtalehistorikk, så ett `read_file` betales av hver senere prompt i kjøringen. Her utgjør de tre verktøyresultatene 87–93 % av hver av de tre største postene, og prisskjemaet alene er 62 % av dem. Ingen enkeltpost passerer § 5s 100 000-tak i denne kjøringen; de to dokumentene som *ville* gjort det (§ 1d) ble ikke åpnet. --- ## 6. Hva som mangler for S7b (validert besparelse) Tre krav, alle målt her, i den rekkefølgen de blokkerer: **1. Kunnskapsbasen mangler en IR-projeksjon.** `run_project`s bundle-arm nekter med `IR projection not found in bundle: 'validator-input.json'` (§ 2). Uten den stopper kjøringen etter utforskningen uansett hva prisskjemaet inneholder. Ingesterings-profilen emitterer den ikke. **Kryss-repo-krav mot `llm-ingestion-okf`** — ingen brief i dette repoet kan lukke det. **2. Prisskjemaet er uutfylt, og MAJOR-4 nekter — korrekt.** Kjørt mot det ekte K2-prisskjemaet: ``` CostBaselineDerivationError: no cost table found in bundle 'scratchpad/s7a/k2-bundle': no concept file carries a markdown table whose header names all three of ['code', 'quantity', 'unit_cost'] ``` Kontroll: samme funksjon mot `tests/fixtures/k2-prisskjema-SYNTETISK` gir **3 kostlinjer**, så avlederen virker — nekten er en egenskap ved K2, ikke ved koden. Formen er målt presist: K2s Bilag 7 er **ett** ark (`## Prissammenstilling`), en pandoc SIMPLE table med kolonneparet `Post … SUM`. Ordet `Enhetspris` forekommer **0 ganger** i hele filen, og `Mengde`/`Timepris` står bare i prosa — de tilhørende cellene er tomme, fordi totalentreprenøren skal fylle dem. Den **eneste** prisede raden i hele skjemaet er `82 Prosjektering (tiltransport av prosjekterende) = 5 647 500`. S7b trenger altså den syntetisk utfylte kopien plan § 9.1 anbefaler. MAJOR-4 har allerede formen: `okf.derive_cost_baseline` (eier B) og fixturparet `tests/fixtures/k2-prisskjema-SYNTETISK` / `…-uprisert-SYNTETISK`, med `--derive-cost-baseline` som CLI-døra. **3. `adjudication` mangler i hele korpuset (0/39).** K5-terskelen diskonterer derfor hvert konsept (§ 4). En validert besparelse kan formelt sett fortsatt produseres — terskelen gjelder falsifiseringsdommen, ikke validatoren — men den kan ikke *hvile* på noe i basen. **Kryss-repo-krav mot `llm-ingestion-okf`**, samme klasse som punkt 1. Ett krav er **ikke** blokkerende og bør ikke telles med: at en revise endrer hypotesen (§ 3). Det krever en levende manager, ikke en endring i noe av dette. --- ## 7. Verifiseringslogg | # | Påstand | Kommando → resultat | |---|---|---| | 1 | Instrumentet reproduserer publisert fasit | `uv run --with tiktoken python scratchpad/s7a/m1_navigation.py` → tunnel `read_bundle` 259, `bundle_context` 12 595 | | 2 | 39 konsepter, 0 `adjudication`, kjent-positiv 39 `type` | `grep -l '^adjudication:' *.md \| wc -l` → 0; `grep -l '^type:'` → 39 | | 3 | `N=43` ikke verifiserbart | ingen `log.md` i bundelen (`Path(base)/'log.md').exists()` → `False`) | | 4 | Navigasjonstall | samme skript → § 1-tabellen | | 5 | Verktøysporet | `outbox/s7a-offline-exploration.json` → `tool_calls` = 3, i rekkefølge | | 6 | Pipelinen nekter på manglende IR | stderr → `IR projection not found in bundle` | | 7 | Levende ikke kjørbar | alle fem env-variabler tomme; `data/model_map.json` finnes ikke | | 8 | Revise → replan → nytt spørsmål | `outbox-review/s7a-review-exploration.json` → 2 `plan_reviews`, ulik `plan` | | 9 | Ingen `object at` | `grep -c 'object at'` over artefakter + stdout → 0 | | 10 | Kjeden, tre ledd | `uv run --with tiktoken python scratchpad/s7a/m3_revise_chain.py` → § 3-tabellen | | 11 | K5 diskriminerer | `uv run python scratchpad/s7a/m4b_falsification.py` → `False` vs `True` | | 12 | MAJOR-4 nekter mot ekte K2, godtar syntetisk | `uv run python scratchpad/s7a/m6_derive.py` → feilmelding vs 3 kostlinjer | | 13 | Tokenprofil + sammensetning | `uv run --with tiktoken python scratchpad/s7a/m5_prompt_composition.py` → § 5-tabellene | **Ikke verifisert her:** at en levende modell kaller verktøyene eller velger godt (samme grense som structured-output-raden); at `N=43` stemmer; at leverandørens caching treffer prefiksene; K2s Prisskjema slik en utfylt versjon faktisk ville sett ut (fixturen er syntetisk). **Premisser som ble felt av målingen, og som er rapportert i stedet for omgått:** ordrens «ny profil med `adjudication`» (0/39), ordrens punkt 4 (ingen `proposed`-konsept finnes), og antakelsen om at mandat-diffen ville vise revisjonens virkning (tom ved konstruksjon).