portfolio-optimiser/docs/2026-09-12-p14-kontekstsett.md
Kjell Tore Guttormsen f13dc64a0a fix(okf): top-level frontmatter title survives a nested sources: title
P15 (order 20260912T220951Z). okf._frontmatter_from_text was linewise
last-write-wins over EVERY line regardless of indentation, so a curated
concept's own top-level `title:` got silently overwritten by the nested
`sources:\n  - title: ...` block's title. Fix: a top-level (unindented)
key always wins over an indented one of the same name; a nested line
with no top-level counterpart is still preserved (SPEC §4).

Red-before/green-after: new test
test_parse_frontmatter_top_level_title_survives_nested_sources_title
(tests/test_okf.py) failed on 45edbf5 (fm["title"] == "N500:2024",
expected the concept's own), green after the fix.

Re-measured on all four vegnormal-okf bases (concept files / distinct
titles): n100-2023 446/446 (was 1) - n200-2024 1133/1133 (was 1) -
n500-2024 270/270 (was 1) - r761-2025 2756/2407 (genuine repeated
process names, not a collapse). directory_listing on krav/N500:
269 documents / 269 distinct titles (was 1).

tests/test_context_sets_loadbearing.py:
- The P14 tripwire test (asserting parse_frontmatter DID collapse
  titles) is INVERTED, not deleted, per the order: it now asserts the
  fix holds, as a live regression guard.
- own_frontmatter() stays (not replaced by parse_frontmatter): measured
  29,500 field reads (type/title/req_number/prosessnr, all four bases)
  agree exactly except for quote-stripping (2,728/29,500, zero value
  mismatches) - own_frontmatter unquotes for fasit comparison,
  parse_frontmatter deliberately doesn't (D1/(a)/(i): unquote_scalar is
  the ONE unquoting rule).

docs/2026-09-12-p14-kontekstsett.md Part B correction: the "22 of 22
cost words absent from n100/n200/n500" claim was false - n500-2024
carries `kroner` as a false positive (substring match inside
"borkroner", drill bits, not money). The original 22-word list was
never persisted, so only ~9 of the 22 survive named. Replaced with a
newly named, persisted 22-word list and the actual re-measured count:
n100 22/22 absent - n200 22/22 - n500 21/22 (kroner via borkroner) -
r761 18/22 (4 genuine cost words). No gate touched (no fasit anchor is
`kroner`).

Verification: full suite 1643 passed / 5 skipped (was 1642/5 on
45edbf5, +1 new test, 0 removed) - `uv run pytest -q`. ruff check +
ruff format --check clean on the three changed source/test files.
Golden transcripts byte-unchanged: shasum -a 1
tests/golden/demo-transcript.stdout = ea8c534773acdbe41ae68f2c55724d69aaf8be4f,
demo-transcript.stderr = ede3e2f685ce6a14ad9888e9de421d1a66f6c611.
No version bump, no push (both forbidden by the order).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 07:40:57 +02:00

20 KiB
Raw Blame History

P14 — kontekstsettet for stresstesten

Ordre 20260912T202210Z-7590723260-from-.claude, økt 118, 2026-09-12. Operatørens valg 12.09, ORDRETT fra ~/.claude/docs/2026-09-12-ukesgrunnlag-uke39.md § Operatørens valg 12.09: «Tredje lesning: D-1 står; mål, bundle-valg og all kontekst gis til po per prosjektkjøring (se § 1). Rad 2 + 3, ikke 3». Ordren leser dette som P14-valg (a) — én bundle per kjøring, fire kjøringer — og fører (c), CLI-flate for run_mandate_across_bundles, som en EGEN bygge-ordre etter første stressrunde. (c) er IKKE bygget her.

Ingen modellkall, ingen Azure. Alt under er målt mot disken, ikke antatt.


1. Formen (DEL 1)

Ett kontekstsett = én katalog under contexts/<prosjekt-id>/:

Fil Rolle Leses av
mandate.json kommisjonen — Mandate-skjemaet ORDRETT (mandate.py) load_mandate (fail-fast)
bundle.txt hvilken kunnskapsbase settet hører til, og hvilken id den erklærer testen + operatøren
fasit.json fasiten: hva et riktig svar MÅ peke på, hva som IKKE kan besvares, og ærlighetsfeltet testen + operatøren etter kjøringen
docs/ 0n syntetiske prosjektdokumenter ingen i dag — se § 1.4

1.1 mandate.json — ingen ny form

Filen er Mandate slik mandate.py allerede definerer den. Ingen felt er funnet på: objective, success_criteria, approaches[] med id/label/description/affected_codes/ claimed_saving_nok/bundle_id. allow_own_proposals står på sin default (true), fordi kravet er «disse og/eller dine egne» og den permissive halvdelen er dagens oppførsel.

bundle_id settes EKSPLISITT på hver approach, også når settet bare har én base. Med én base ville route_by_bundle løst et tomt felt til den ene basen uansett (sole-regelen), så feltet er strengt tatt valgfritt her — men det er nøyaktig det feltet (c) kommer til å rute på, og et sett som allerede bærer det er dispatchbart uendret den dagen (c) finnes. Å la det stå tomt nå ville betydd fire redigeringer da.

1.2 bundle.txt — symbolsk navn, aldri en absolutt sti

To linjer, nøkkel: verdi:

name: n500-2024
bundle_id: vegnormal-n500-2024

Symbolsk navn, ikke absolutt sti — ordren tillater begge. En absolutt sti ville pinnet settet til én maskins hjemmekatalog i et repo som publiseres på open/, og git archive HEAD-pakka bærer tracked filer (Fase 5-invarianten), altså ville stien fulgt med ut. Navnet resolveres mot PORTFOLIO_VEGNORMAL_ROOT, som defaulter til ~/repos/vegnormal-okf/build/ferdig.

bundle_id er DEKLARERT i fila fordi den gjør DEL 3(d) målbar uten basen til stede: testen kan sammenligne mandatets bundle_id mot settets erklæring i enhver checkout. Når basen ER til stede, verifiseres erklæringen i tillegg mot basens egen okf.reconcile_bundle_id — så erklæringen kan ikke drifte fra basen i stillhet.

1.3 fasit.json — ÉN maskinlesbar kilde, ikke fasit.md

Ordren foreslår fasit.md («f.eks.»). Avvik, uttalt: fasiten er fasit.json. Grunnen er kø-(p): testen må lese fasit-id-ene, og en prosa-fil ved siden av en maskinlesbar ville vært to kopier av ett faktum, fri til å drifte. Prosaen bor derfor INNI JSON-en (rationale, honesty, question), så mennesket og testen leser den samme fila.

{
  "project_id": "...",
  "bundle": "n500-2024",
  "must_cite": [                      // per approach: hva et riktig svar MÅ peke på
    {"approach_id": "...", "rationale": "...",
     "concepts": [{"path": "krav/N500/id-….md", "title": "…", "ref": "Krav 10.4.3—2"}]}
  ],
  "unanswerable": [                   // falsifiseringsgaten
    {"question": "…", "anchors": ["enhetspris"], "expected": "ubesvart — riktig svar er å si det"}
  ],
  "honesty": "…"                      // DEL 2(iii)
}

path er bundle-relativ, altså nøyaktig den identifikatoren read_file(bundle_id, path) tar. Det er den eneste formen som er både grep-bar mot basen og brukbar ORDRETT som neste kalls argument (S7a-3s regel om at en sti aldri skal måtte komponeres av en modell). title/ref er målt ut av konseptets egen frontmatter ved forfatting og re-verifiseres av testen — de er et opptak, ikke en andre kilde.

1.4 docs/ er TOM, og det er en beslutning

Scenarioet er MS Office + PDF. Ingen av dem kan mates inn her: --docs-dir-omveien er frarådet (P13 — den omgår stigen list_bundles → read_bundle → read_dir → read_file og de to gatene §4.1a-dimensjonen og verdict-laget), og ordren forbyr å bygge den. Veien et ekte prosjektdokument skal ta er gjennom okf-ingest inn i en base, altså en base til, ikke en katalog ved siden av. Katalogen finnes derfor med en README.md som sier nøyaktig det, og 0 dokumenter — som ordrens «0n» tillater.


2. De fire settene (DEL 2)

Basene er MÅLT, ikke lest av ordren (find <base> -name '*.md' | wc -l):

Sett Base .md-filer konsepter (type: Krav/Prosess) erklært bundle_id
gate-nordvik-2027 n100-2023 450 445 Krav vegnormal-n100-2023
fv412-dekkefornyelse-2027 n200-2024 1 137 1 132 Krav vegnormal-n200-2024
tunnel-hauglia-2027 n500-2024 274 269 Krav vegnormal-n500-2024
kontrakt-sorasen-2027 r761-2025 5 514 2 727 Prosess + 28 Kapittel vegnormal-r761-2025

Ordrens tall (450 / 1 137 / 274 / 5 514) reproduseres eksakt. Differansen mellom .md-filer og konsepter er index.md-ene, som er navigasjon og ikke innhold.

2.1 affected_codes — hvor de kommer fra, og hvor de IKKE gjør det

Bare kontrakt-sorasen-2027 har ekte koder. R761 bærer prosessnr i frontmatter (12.1, 22.1, 51.1, 52.1 …), altså en kodeserie som finnes i basen og er grep-bar. De tre vegnormal-settene bærer INGEN kostkoder — N100/N200/N500 er krav, ikke prissatte poster — så kodene der er syntetiske prosjekt-kostlinjer jeg har funnet på, navngitt i ærlighetsfeltet i hvert sett. Dette er ikke pynt: candidate_from_approach (S7b-døra, --proposals-from-mandate) avviser en kode basens baseline ikke bærer, så de tre settene er BEVISST ikke kjørbare på den døra. Den døra er ikke stresstestens sti — stresstesten går --mandate over den ordinære løkka.

2.2 Falsifiseringsgaten — og hvorfor den er så skarp her

RETTET 2026-09-13 (P15, ordre 20260912T220951Z). Den opprinnelige setningen her — «samtlige 22 er FRAVÆRENDE fra n100/n200/n500» — var USANN og er fjernet. Kun 8 av de 22 opprinnelig prøvde ordene ble noensinne navngitt (listen over, med et avsluttende «…»), og selve 22-ordslisten ble ALDRI persistert noe sted i repoet — den kan derfor ikke re-måles verbatim. Det er en nevner-svikt av Verifiseringslovens ansikt 4-type: et «fraværende»-utsagn uten en gjenfinnbar liste er ikke et mål, det er en påstand om et mål som fant sted.

Ny, navngitt og persistert 22-ordsliste (2026-09-13, denne rettelsen — velges for domene-treffsikkerhet, ikke for å oppnå et bestemt utfall), re-målt med gatens egen substring-regel (anchors_are_absent, tests/test_context_sets_loadbearing.py) mot alle fire basene:

enhetspris, kroner, budsjett, kostnadsestimat, prisskjema, timepris, nåverdi, driftskostnad, anleggskostnad, materialkostnad, arbeidskostnad, investeringskostnad, vedlikeholdskostnad, kapitalkostnad, finansieringskostnad, livssykluskostnad, kontraktssum, anbudspris, tilbudspris, merverdiavgift, avskrivning, besparelsespotensial.

Faktisk telling per base (446/1133/270/2756 konsepter):

  • n100-2023: 22 av 22 fraværende.
  • n200-2024: 22 av 22 fraværende.
  • n500-2024: 21 av 22 fraværende — IKKE 22. Basen bærer kroner, men som en FALSK POSITIV: ordet forekommer kun inni borkroner (boreutstyr, ikke penger) i krav/N500/id-41a2f459-e362-42d8-d725-aaee8290c7c1.md — «(styrestenger, borkroner), nøyaktighet ved a…». Delstreng-regelen i Rule U treffer bevisst uten ordgrenser (§ 2.3), og dette er den konkrete prisen for det: en ekte nulltreff-base kan likevel «bære» et pengeord gjennom et urelatert sammensatt ord. Funnet endrer INGEN gate — ingen fasit-anchor er kroner — men det gjør STATE-påstanden usann og er derfor rettet her.
  • r761-2025: 18 av 22 fraværende (uendret konklusjon fra tidligere, men re-målt mot den nye listen): basen bærer enhetspris, kroner, budsjett og kapitalkostnad — alle fire er ekte kostnadsspråk (f.eks. «kapitalkostnader», «mengder i kroner avviker fra summen»), konsistent med at prosesskoden ER kontraktsspråk.

Konklusjonen står: tre av fire baser er reelt uten kostnadsspråk (n500s ene treff er en falsk positiv, ikke et pengeord), og det er hele grunnen til at gaten er verdt å teste. En kjøring som svarer med et kronebeløp mot N100 har ikke lest basen — den har funnet på.

2.3 Regelen for «kan ikke besvares» (DEL 3c) — skrevet ned

Regel U. Hvert ubesvarbart spørsmål erklærer ≥ 1 anchor: et ord på ≥ 4 tegn, små bokstaver. Spørsmålet er admittert iff hver anchor er fraværende — case-insensitivt, som delstreng — fra HELE teksten (frontmatter + kropp) i HVERT konseptdokument i basen.

Tre valg i den regelen er bevisste:

  1. Anchor, ikke «deler ingen nøkkelord». Ordrens bokstav ville krevd at spørsmålet ikke deler ett eneste ord med noen konsepttittel. I en domenebase er det ikke oppnåelig — et tunnelspørsmål deler «tunnel» med hundrevis av titler, og det beviser ingenting. Det som gjør et spørsmål ubesvarbart er at basen mangler saken, ikke vokabularet. Anchoren ER den saken.
  2. Hele teksten, ikke bare tittelen. En tittel-regel er en proxy: et ord kan mangle i hver tittel og stå i hver kropp. Fullteksten koster ingenting ekstra (filene leses uansett i sin helhet — målt 0,77 s for r761, den største basen), så den svakere regelen hadde ingen pris å forsvare seg med.
  3. Nevner, alltid. Skanningen rapporterer hvor mange konsepter den så, og en skanning som ser null konsepter er RØD. Uten den ville «anchoren ble ikke funnet» vært like sant om en base som ikke ble lest (Verifiseringsloven ansikt 4).

Ærlighets-grense, uttalt: regelen beviser at ORDET ikke står i basen, ikke at SPØRSMÅLET er ubesvarbart. Et spørsmål kan omskrives til synonymer basen bærer og da være besvarbart uten at anchoren dukker opp. Anchoren er valgt til å være spørsmålets bærende begrep nettopp for å gjøre det gapet lite, men det er en proxy, og det står her i stedet for å bli bortforklart.


3. Gaten (DEL 3)

tests/test_context_sets_loadbearing.py. Fire krav, og hvert av dem har sin egen kjent-positiv: et bevisst ødelagt sett bygget i tmp_path som gjør nøyaktig den armen rød.

Arm Krever basen? Hva den nekter
(a) hvert mandate.json lastes gjennom load_mandate nei et sett som ikke laster
(d) bundle_id i mandatet == settets erklærte id nei et mandat som peker på feil base
(b) hver fasit-sti finnes i basen, og title/ref er basens egne ja en fasit-id som ikke finnes, eller en tittel som har driftet
(c) Regel U over hele basen ja et «ubesvarbart» spørsmål basen faktisk bærer ordet for
(e) den erklærte bundle_id == basens egen reconcile_bundle_id ja en erklæring som har driftet fra basen

Basene er IKKE en repo-avhengighet, og de bundle-krevende armene SKIPPER når roten mangler. Det er MAJOR-3-gatens egen begrensning (K2 kunne heller ikke være en testavhengighet) og samme klasse som de fem betalte skippene suiten alt har: basene ligger utenfor repoet, og en hard feil ville brutt uv run pytest i overleveringspakka for enhver ekstern mottaker. Skippet navngir roten, og armen som kjører bærer nevner-kontrollen fra § 2.3, så et stille tomt skann kan ikke bli grønt. De to offline-armene er ubetingede og kan aldri være fraværende.


4. Målepunktet: kan én kjøring bære flere bundler? (DEL 4 — ikke bygget)

4.1 Hva (a) koster i dag

Fire kjøringer, fire run_id, fire utbokser. Per sett (<sett> og <base> fra tabellen i § 2):

uv run python -m portfolio_optimiser.run <project_id> \
  --bundle-dir "$PORTFOLIO_VEGNORMAL_ROOT/<base>" \
  --mandate contexts/<sett>/mandate.json \
  --run-id <sett>-01 \
  --outbox-dir scratchpad/p14-stress/<sett> \
  --profile azure --max-rounds 8 --max-tokens 120000

Kostnaden ved (a) er altså ikke fire ganger modellprisen mot én — det er fire uavhengige kjøringer som hver bærer sin egen base, og det er dét som gjør dem sammenlignbare. Det den koster er operatørarbeid og sammenstilling: fire kommandoer, fire utbokser, fire dom-nøkler, og ingen felles MultiBaseResult som sier hva kommisjonen samlet ble til. En kryss-base-kollisjon (D2) kan ikke oppstå, fordi hver kjøring har sin egen VerdictStore — læring fra base k når aldri base k+1. Dét er hovedtapet ved (a), og det er nøyaktig det (c) kjøper.

4.2 Nøyaktig hva (c) ville trenge

Motoren finnes ferdig. run.run_mandate_across_bundles (run.py:2124) tar allerede mandate + bundle_dirs: Sequence[str], partisjonerer via mandate.route_by_bundle, tråder ÉN VerdictStore på tvers, og returnerer MultiBaseResult med unreached og collisions. Den tar bevisst ingen project_id (hver bases prosjekt leses av _project_from_bundle).

Det som mangler er utelukkende kallstedet, og det er tre ting — MÅLT, ikke anslått:

  1. Argparse: en repeterbar --bundle-dir finnes ikke (run.py:2444parser.add_argument("--bundle-dir", default=None, …), altså ÉN katalog), og STATE fører «repeterbart --bundle-dir = NEI» som stående beslutning. (c) trenger derfor et nytt, eget flagg — f.eks. --bundle-dirs (action="append") eller --mandate-across — aldri en utvidelse av det eksisterende, som ville endret en flate fire eksisterende gater pinner.
  2. run_id-mynting: run_mandate_across_bundles wirer ikke utboksen. Funksjonens egen docstring sier hvorfor: «The outbox is NOT wired: N runs need N run_ids, and minting them here would default a key this repo requires a caller to supply» (run.py:2178-2180), og kroppens ene await run_project( (run.py:2260) sender verken outbox_dir eller run_id. (c) må altså avgjøre myntingsregelen på KALLER-siden — f.eks. <run_id>-<bundle_id> — og den regelen er en operatørbeslutning, ikke en default dette laget får ta.
  3. Partisjonen i main(): hvert nytt flagg må inn i de tre nekt-settene som allerede finnes — --portfolio-partisjonen (run.py:2771 ff.), report_forbidden (run.py:2859 ff.) og dry-run-partisjonen — hver med sin rc-0-kontroll. Det er repoets egen regel om at en utelatelse i report_forbidden er et stille dropp, ikke en nekt (F4-gapet).

Rekkefølgen er dermed gitt: (c) er én ordre med ett nytt flagg, én myntingsregel operatøren bestemmer, og tre partisjons-rader. Den skrives etter første stressrunde — når vi vet om (a)s manglende kryss-base-læring faktisk kostet noe.


5. FUNN — konsept-titlene kollapser på vei gjennom navigasjonsstigen

Målt, ikke antatt, og funnet fordi P14 leste basene i stedet for å anta dem.

okf.parse_frontmatter er linjeorientert og last-write-wins — det står ordrett i dens egen docstring («every frontmatter line that carries a colon becomes one key: value pair, last write winning»). Hver eneste vegnormal-konseptfil avslutter frontmatteren med en sources:-blokk:

sources:
  - resource: https://viewers.vegnorm.vegvesen.no/api/nisosts/859990?languageCode=nb
    title: N500:2024

Den innrykkede title overskriver dermed konseptets egen. Målt på n500-2024:

Målt Resultat
okf.navigate_bundle(...)context_files 270 konseptfiler, 1 distinkt tittel (N500:2024, 270 ganger)
okf.directory_listing(bundle, path="krav/N500") 269 dokumenter, alle med "title": "N500:2024"
konseptenes EGNE toppnivå-titler 269 distinkte (Krav 1.1—2 Generelle bestemmelser, …)

Over de fire basene: 445/445 · 1 132/1 132 · 269/269 · 2 378/2 727 distinkte egne titler, mot 1 distinkt gjennom parse_frontmatter på hver av dem.

Hvorfor det betyr noe for stresstesten. S7a-3 bygde stigen list_bundles → read_bundle → read_dir → read_file nettopp for at navigatøren skal kunne VELGE hvilket dokument den åpner. På disse basene er rung 2 og rung 3 informasjonsløse: navigatøren ser 269 oppføringer som skiller seg fra hverandre med et ugjennomsiktig UUID-filnavn og et tegnantall. Den kan ikke velge på annet enn tilfeldighet — og MAJOR-3s egen ærlighets-grense («at en LEVENDE modell velger BEDRE med en liste enn med hele konteksten er IKKE bevist») blir her målbart usann i den ene retningen ingen hadde målt: det er ingenting å velge PÅ.

IKKE fikset her, og det er en scope-grense, ikke en forglemmelse. Ordren forbyr å bygge utover P14, og en fiks er dessuten to ulike beslutninger med hver sin gate: enten endres parse_frontmatters pinnede last-write-wins-regel (som mange tester pinner), eller så bytter directory_listing tittelkilde til en toppnivå-leser. Begge er en egen ordre.

Gaten har derfor en TRIPWIRE i stedet. own_frontmatter i tests/test_context_sets_loadbearing.py leser konseptets egen erklæring (toppnivå-nøkler, første forekomst vinner) — uten den ville fasit-asserten sammenlignet hvert konsept mot den samme konstanten og vært VAKUØS, repoets egen vakuøs-gate-klasse. Arm test_the_fasit_titles_are_distinct_not_the_collapsed_sources_title asserterer BEGGE halvdeler: at de registrerte titlene skiller konseptene fra hverandre, og at parse_frontmatter fortsatt kollapser dem. Den dagen den andre halvdelen blir rød, er funnet borte og armen skal SLETTES, ikke svekkes — det står i armens docstring.


6. Leveransen (DEL 5)

  • Ingen produksjonskode er rørt. Leveransen er fire datasett + én gate + dette dokumentet.
  • Suite: basislinje etter P13b 1606 passed / 5 skipped1642 passed / 5 skipped (+36, 0 fjernet — strengt supersett). demo-transcript.stdout BYTE-UENDRET, shasum -a 1 av INNHOLDET = ea8c534773acdbe41ae68f2c55724d69aaf8be4f (aldri git-blob-id-en). ruff check + ruff format + mypy src grønne.
  • Seks mutasjoner, alle røde på sin egen arm (mot gate-fila; grep -rln contexts tests/ viser at INGEN annen test leser contexts/, så hele-suite-kjøringer per mutasjon ville ikke kunnet legge til informasjon — uttalt, ikke utelatt). Settene ble sikkerhetskopiert til scratchpad/p14-backup/ og gjenopprettet derfra, aldri med git checkout; de 16 filene er verifisert byte-identiske etterpå.
# Mutasjon Rød arm
M1 fasit peker på en sti basen ikke bærer (b) + tittel-tripwiren
M2 en approach rutet mot en annen base (d)
M3 en «ubesvarbar» anchor basen FAKTISK bærer (asfalt i n200) (c)
M4 duplisert approach-id (a) + (d) + fasit-dekningen
M5 erklært bundle_id driftet fra basens egen (d) + (e)
M6 en registrert tittel driftet fra basens egen (b)

Den aller første kjøringen av gaten var rød før settene fantes (expected four context sets … found []), som er Iron Law-rekkefølgen.

6.1 Ærlighets-grenser, uttalt

  1. De fire prosjektene er oppdiktet. Hvert sett bærer sitt eget honesty-felt som sier nøyaktig hva jeg har konstruert. Kort: navn, lengder, ÅDT og alle tolv beløp er satt av meg; affected_codes er syntetiske i tre av fire sett og EKTE prosessnr i kontrakt-sorasen-2027. Alt i must_cite er derimot lest ordrett ut av basenes egen frontmatter.
  2. Regel U beviser at ORDET mangler, ikke at spørsmålet er ubesvarbart (§ 2.3).
  3. De bundle-krevende armene skipper uten basene. På denne maskinen kjørte alle fire; i en ren klon uten ~/repos/vegnormal-okf skipper tre armer per sett med roten navngitt.
  4. Ingen kjøring er gjort. Ordren forbyr modellkall og Azure. At po faktisk KLARER å peke på fasit-konseptene, og at den faktisk SIER «dette kan ikke besvares», er ikke bevist her — det er nøyaktig det stresstesten 18.09 skal måle. Dette er måleoppsettet, ikke målingen.