test(p14): four stress-test context sets, gated -- and the fasit titles had to be read from the concept's OWN declaration

P14 valg (a): en bundle per kjoering, fire kjoeringer. Ingen modellkall, ingen
Azure, ingen produksjonskode roert -- leveransen er fire datasett, en gate og
maalingen bak dem.

FORMEN: contexts/<prosjekt>/ med mandate.json (Mandate ordrett), bundle.txt
(symbolsk basenavn + erklaert bundle_id -- aldri en absolutt sti, som ville
pinnet settet til en maskin og ridd ut i `git archive HEAD`), fasit.json og en
TOM docs/. Fasiten er EN maskinlesbar fil, ikke fasit.md + en tvilling: to
kopier av ett faktum er ko-(p), saa prosaen bor INNI JSON-en.

REGEL U (den maalbare formen for "basen kan ikke svare"): hvert ubesvarbart
spoersmaal erklaerer >=1 anchor, og admitteres iff HVER anchor er fravaerende
fra HELE teksten i HVERT konsept. Ikke "deler ingen noekkelord med noen tittel"
-- et tunnelspoersmaal deler "tunnel" med hundrevis av titler og det beviser
ingenting. Skanningen baerer alltid NEVNER; null konsepter er ROEDT.

MAALT, og verdt hele ordren: 22 av 22 proevde kostnadsord er FRAVAERENDE fra
n100/n200/n500 (r761 baerer 4). po sitt oppdrag er aa finne kostnadsbesparelser,
og tre av fire baser inneholder ikke ett pengeord.

FUNN, maalt og IKKE fikset (egen ordre): okf.parse_frontmatter er linjeorientert
last-write-wins, saa sources-blokkens innrykkede title overskriver konseptets
egen -- directory_listing paa krav/N500 returnerer 269 dokumenter, ALLE med
"title": "N500:2024". Navigasjonsstigens rung 2/3 skiller dem kun med et
UUID-filnavn og et tegnantall. En fasit-assert mot den tittelen ville vaert
VAKUOES, saa gaten leser toppnivaa-noekler og baerer en TRIPWIRE som asserterer
at kollapsen fortsatt finnes.

Load-bearing MAALT (36 armer), seks mutasjoner alle roede paa sin egen arm +
groenn kontroll 1642/5 (fra 1606/5, supersett, 0 fjernet) og golden BYTE-UENDRET
(shasum -a 1 av INNHOLDET = ea8c534773acdbe41ae68f2c55724d69aaf8be4f). Gaten var
ROED foer settene fantes.

AErlighets-grenser: de fire prosjektene er OPPDIKTET (hvert sett sier i sitt eget
honesty-felt hva jeg konstruerte); Regel U beviser at ORDET mangler, ikke at
spoersmaalet er ubesvarbart; de bundle-krevende armene SKIPPER uten basene; og
INGEN kjoering er gjort -- dette er maaleoppsettet, ikke maalingen. (c) er ikke
bygget: motoren finnes ferdig, det som mangler er ett nytt flagg, en run_id-
myntingsregel operatoeren maa ta, og tre partisjons-rader.

Ordre: 20260912T202210Z-7590723260-from-.claude
Maaling: docs/2026-09-12-p14-kontekstsett.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-12 23:48:20 +02:00
commit 45edbf5957
19 changed files with 1282 additions and 0 deletions

View file

@ -2359,6 +2359,50 @@ Python ≥3.10. MAF (`agent-framework-core` 1.16.0, `-orchestrations` 1.1.1 —
på okf sin GAMLE default-bundle (`K2-bundle-20260903`), så en re-måling på den nye (436 konsepter på okf sin GAMLE default-bundle (`K2-bundle-20260903`), så en re-måling på den nye (436 konsepter
/ 832 filer) er en senere, separat ordre. Måling: / 832 filer) er en senere, separat ordre. Måling:
`docs/2026-09-09-p8-forankringstilbudet.md`. `docs/2026-09-09-p8-forankringstilbudet.md`.
- **Stresstestens kontekstsett er DATA med en gate, og fasitens titler måtte leses av konseptets
EGEN erklæring (P14, 12.09):** `contexts/<prosjekt>/` bærer `mandate.json` (`Mandate` ordrett),
`bundle.txt` (symbolsk basenavn + erklært `bundle_id` — aldri en absolutt sti, som ville pinnet
settet til én maskin og ridd ut i `git archive HEAD`), `fasit.json` og en TOM `docs/`
(`--docs-dir`-omveien er frarådet; veien for et prosjektdokument er okf-ingest inn i en base).
**Fasiten er ÉN maskinlesbar fil, ikke `fasit.md` + en maskinlesbar tvilling** — to kopier av ett
faktum er kø-(p) — så prosaen (`rationale`, `honesty`, `question`) bor INNI JSON-en.
**`bundle_id` settes eksplisitt på hver approach også med én base:** `route_by_bundle` ville løst
et tomt felt til `sole`, men det er nøyaktig feltet en framtidig (c)-flate ruter på, og et sett som
alt bærer det er dispatchbart uendret. **Regel U** er den målbare formen for «basen kan ikke svare»:
hvert ubesvarbart spørsmål erklærer ≥1 `anchor` (små bokstaver, ≥4 tegn), og admitteres iff HVER
anchor er fraværende — case-insensitivt, som delstreng — fra HELE teksten i HVERT konsept.
**Ikke «deler ingen nøkkelord med noen tittel»:** 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.
Fullteksten koster ingenting ekstra (målt 0,77 s for r761, den største basen), så tittel-proxyen
hadde ingen pris å forsvare seg med, og skanningen bærer alltid NEVNER — null konsepter er RØDT,
aldri vakuøst fraværende (ansikt 4). **MÅLT og verdt hele ordren: 22 av 22 prøvde kostnadsord er
FRAVÆRENDE fra n100/n200/n500** (r761 bærer 4 av dem, fordi prosesskoden ER kontraktsspråk) — po
sitt oppdrag er å finne kostnadsbesparelser, og tre av fire baser inneholder ikke ett pengeord.
**Basene er IKKE en repo-avhengighet:** tre armer SKIPPER med roten navngitt når
`PORTFOLIO_VEGNORMAL_ROOT` ikke er montert (MAJOR-3-gatens egen begrensning — en hard feil ville
brutt `uv run pytest` i overleveringspakka), mens (a) mandatet laster og (d) ruting-mot-egen-base
er UBETINGEDE og aldri kan være fraværende. **FUNN, målt og IKKE fikset (egen ordre):**
`okf.parse_frontmatter` er linjeorientert last-write-wins, så `sources:`-blokkens innrykkede
`title` overskriver konseptets egen — `directory_listing``krav/N500` returnerer **269
dokumenter, alle med `"title": "N500:2024"`**, og navigasjonsstigens rung 2/3 skiller dem kun med
et UUID-filnavn og et tegnantall. En fasit-assert mot den tittelen ville vært VAKUØS, så gaten
leser toppnivå-nøkler (`own_frontmatter`, første forekomst vinner) og bærer en TRIPWIRE som
asserterer at kollapsen fortsatt finnes; blir den halvdelen rød er funnet borte og armen skal
SLETTES, ikke svekkes. Load-bearing MÅLT (`tests/test_context_sets_loadbearing.py`, 36 armer),
**seks mutasjoner alle røde på sin egen arm** + grønn kontroll **1642/5** (fra 1606/5, supersett,
0 fjernet) og golden BYTE-UENDRET (`shasum -a 1` av INNHOLDET =
`ea8c534773acdbe41ae68f2c55724d69aaf8be4f`): M1 fasit-sti basen ikke bærer (2) · M2 approach rutet
mot annen base (1) · M3 anchor basen FAKTISK bærer (1) · M4 duplisert approach-id (3) · M5 erklært
`bundle_id` driftet (2) · M6 registrert tittel driftet (1). Gaten var RØD før settene fantes.
**Ærlighets-grenser, uttalt:** de fire prosjektene er OPPDIKTET (hvert sett sier i sitt eget
`honesty`-felt hva jeg konstruerte — navn, lengder, ÅDT og alle tolv beløp; `affected_codes` er
syntetiske i tre sett og EKTE `prosessnr` i `kontrakt-sorasen-2027`, som gjør de tre andre bevisst
IKKE kjørbare via `--proposals-from-mandate`); Regel U beviser at ORDET mangler, ikke at
spørsmålet er ubesvarbart; og INGEN kjøring er gjort — dette er måleoppsettet, ikke målingen.
**(c), CLI-flate for `run_mandate_across_bundles`, er IKKE bygget:** motoren finnes ferdig
(`run.py:2124`), og det som mangler er ett nytt flagg (aldri et repeterbart `--bundle-dir`), en
`run_id`-myntingsregel operatøren må ta (utboksen er bevisst uwiret, `run.py:2178-2180`) og tre
partisjons-rader. Form, fire sett og måling: `docs/2026-09-12-p14-kontekstsett.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.

View file

@ -0,0 +1,2 @@
name: n200-2024
bundle_id: vegnormal-n200-2024

View file

@ -0,0 +1,7 @@
# Prosjektdokumenter
TOM med vilje. Scenarioet er MS Office + PDF, men ingen av dem kan mates
inn her: `--docs-dir`-omveien er fraradet (P13 - den omgar stigen `list_bundles -> read_bundle
-> read_dir -> read_file` og de to gatene, §4.1a-dimensjonen og verdict-laget). Veien et ekte
prosjektdokument skal ta er gjennom okf-ingest inn i en kunnskapsbase, altsa en base til - ikke
en katalog ved siden av. Se `docs/2026-09-12-p14-kontekstsett.md` § 1.4.

View file

@ -0,0 +1,82 @@
{
"project_id": "fv412-dekkefornyelse-2027",
"bundle": "n200-2024",
"bundle_id": "vegnormal-n200-2024",
"must_cite": [
{
"approach_id": "a1-resirkulert-asfalt",
"rationale": "Beskrivelsen forutsetter nytt bituminøst bærelag. Vi vil vite hva N200 tillater av resirkulert asfalt.",
"concepts": [
{
"path": "krav/N200/id-4b5dd93e-dc57-4a68-e4be-0af5279dd191.md",
"title": "Krav 4.7.2—1 Resirkulert asfalt i bærelag",
"ref": "Krav 4.7.2—1"
},
{
"path": "krav/N200/id-4ede1d67-be20-43f5-c238-8bd24cabe3d2.md",
"title": "Krav 4.8—1 Asfaltdekker",
"ref": "Krav 4.8—1"
}
]
},
{
"approach_id": "a2-barelag-gjenbruk",
"rationale": "Eksisterende bærelag er antatt utskiftet i sin helhet. Spørsmålet er hvilke krav N200 stiller til bærelag før gjenbruk kan vurderes.",
"concepts": [
{
"path": "krav/N200/id-dd38bce0-e39b-4e7f-b0fa-6d2845274dec.md",
"title": "Krav 3.3.2—1 Bærelag",
"ref": "Krav 3.3.2—1"
},
{
"path": "krav/N200/id-18e0246c-851d-4d6b-dea9-f63a74041f1b.md",
"title": "Krav 3.3.2—1_1 Bærelag",
"ref": "Krav 3.3.2—1_1"
}
]
},
{
"approach_id": "a3-frostsikring-tykkelse",
"rationale": "Frostsikringen er prosjektert med full tykkelse over hele strekningen. Vi vil vite hva N200 krever.",
"concepts": [
{
"path": "krav/N200/id-087e62da-e780-4d42-c785-5be9490a4000.md",
"title": "Krav 3.2.2—1 Frostsikring med ubundne materialer",
"ref": "Krav 3.2.2—1"
},
{
"path": "krav/N200/id-c2f574a8-27e5-48e1-9d26-80a73512fdcf.md",
"title": "Krav 3.2.2—2 Frostsikring med ubundne materialer",
"ref": "Krav 3.2.2—2"
}
]
}
],
"unanswerable": [
{
"question": "Hva er tonnprisen på resirkulert asfalt levert verk i dette området?",
"anchors": [
"enhetspris",
"markedspris"
],
"expected": "ubesvart - riktig svar er aa si at basen ikke baerer dette"
},
{
"question": "Hva blir den årlige vedlikeholdskostnaden for det valgte dekket?",
"anchors": [
"vedlikeholdskostnad",
"driftskostnad"
],
"expected": "ubesvart - riktig svar er aa si at basen ikke baerer dette"
},
{
"question": "Hvilket kostnadsestimat ligger til grunn for dekkefornyelsen?",
"anchors": [
"kostnadsestimat",
"kalkyle"
],
"expected": "ubesvart - riktig svar er aa si at basen ikke baerer dette"
}
],
"honesty": "Prosjektet fv. 412 dekkefornyelse er KONSTRUERT av meg: vegnummer, lengde, ÅDT og alle tre kostlinjene (DEKKE-ASF-01, DEKKE-BAER-01, DEKKE-FROST-01) er oppdiktet, og beløpene er satte størrelsesordener. Kravene i must_cite er lest ordrett ut av n200-2024-basens egen frontmatter. N200 bærer ingen priser, så kodene finnes ikke i basen."
}

View file

@ -0,0 +1,36 @@
{
"objective": "Kutt kostnad i dekkefornyelsen på fv. 412 (6,3 km, ÅDT 3 400) uten å bryte et eneste krav i N200.",
"success_criteria": "Minst én tilnærming validerer mot N200, og hver validerte tilnærming peker på de kravene i N200 som faktisk binder materialvalget.",
"approaches": [
{
"id": "a1-resirkulert-asfalt",
"label": "Økt andel resirkulert asfalt i bærelaget",
"description": "Beskrivelsen forutsetter nytt bituminøst bærelag. Vi vil vite hva N200 tillater av resirkulert asfalt.",
"affected_codes": [
"DEKKE-ASF-01"
],
"claimed_saving_nok": 2750000.0,
"bundle_id": "vegnormal-n200-2024"
},
{
"id": "a2-barelag-gjenbruk",
"label": "Gjenbruk av eksisterende bærelag i stedet for utskifting",
"description": "Eksisterende bærelag er antatt utskiftet i sin helhet. Spørsmålet er hvilke krav N200 stiller til bærelag før gjenbruk kan vurderes.",
"affected_codes": [
"DEKKE-BAER-01"
],
"claimed_saving_nok": 1450000.0,
"bundle_id": "vegnormal-n200-2024"
},
{
"id": "a3-frostsikring-tykkelse",
"label": "Tynnere frostsikringslag av ubundne materialer",
"description": "Frostsikringen er prosjektert med full tykkelse over hele strekningen. Vi vil vite hva N200 krever.",
"affected_codes": [
"DEKKE-FROST-01"
],
"claimed_saving_nok": 1900000.0,
"bundle_id": "vegnormal-n200-2024"
}
]
}

View file

@ -0,0 +1,2 @@
name: n100-2023
bundle_id: vegnormal-n100-2023

View file

@ -0,0 +1,7 @@
# Prosjektdokumenter
TOM med vilje. Scenarioet er MS Office + PDF, men ingen av dem kan mates
inn her: `--docs-dir`-omveien er fraradet (P13 - den omgar stigen `list_bundles -> read_bundle
-> read_dir -> read_file` og de to gatene, §4.1a-dimensjonen og verdict-laget). Veien et ekte
prosjektdokument skal ta er gjennom okf-ingest inn i en kunnskapsbase, altsa en base til - ikke
en katalog ved siden av. Se `docs/2026-09-12-p14-kontekstsett.md` § 1.4.

View file

@ -0,0 +1,82 @@
{
"project_id": "gate-nordvik-2027",
"bundle": "n100-2023",
"bundle_id": "vegnormal-n100-2023",
"must_cite": [
{
"approach_id": "a1-rundkjoring-forenklet",
"rationale": "Krysset er prosjektert signalregulert. Vi vil vite hvilke krav N100 stiller til rundkjøring før vi kan regne på et bytte.",
"concepts": [
{
"path": "krav/N100/id-a97dabc7-d35a-4e51-c1b9-5fcdf64ac96d.md",
"title": "Krav 4.1.2—1 Rundkjøringer",
"ref": "Krav 4.1.2—1"
},
{
"path": "krav/N100/id-8df9cff1-d7af-4313-df59-c6dee4271831.md",
"title": "Krav 4.1.2—2 Rundkjøringer",
"ref": "Krav 4.1.2—2"
}
]
},
{
"approach_id": "a2-gangfelt-antall",
"rationale": "Seks opphøyde gangfelt er prosjektert. Spørsmålet er hva N100 krever av kryssingspunkter for gående på en gate med denne fartsgrensen.",
"concepts": [
{
"path": "krav/N100/id-2ffbe00a-3275-4653-f248-47c15159ac0e.md",
"title": "Krav 4.2.5.1—1 Gangfelt og tilrettelagte kryssingspunkter",
"ref": "Krav 4.2.5.1—1"
},
{
"path": "krav/N100/id-92533134-32fd-4547-91e6-1f60bd75e9e7.md",
"title": "Krav 4.2.5.1—2 Gangfelt og tilrettelagte kryssingspunkter",
"ref": "Krav 4.2.5.1—2"
}
]
},
{
"approach_id": "a3-trafikkoy-utforming",
"rationale": "Trafikkøyene er prosjektert i full bredde i alle armer. Vi vil vite hva N100 faktisk krever.",
"concepts": [
{
"path": "krav/N100/id-b0586596-18e9-4d4c-a843-ea6018fcad3d.md",
"title": "Krav 4.1.2.4—1 Trafikkøy i rundkjøringsarmer",
"ref": "Krav 4.1.2.4—1"
},
{
"path": "krav/N100/id-a8df58ed-d301-439b-f6b6-675e6f091455.md",
"title": "Krav 4.1.2.4—2 Trafikkøy i rundkjøringsarmer",
"ref": "Krav 4.1.2.4—2"
}
]
}
],
"unanswerable": [
{
"question": "Hva koster en opphøyd gangfeltløsning per stk i denne gata?",
"anchors": [
"enhetspris",
"kostnadsestimat"
],
"expected": "ubesvart - riktig svar er aa si at basen ikke baerer dette"
},
{
"question": "Hvilken prisstigning skal legges inn fra prosjektering til utførelse?",
"anchors": [
"prisstigning",
"indeksregulering"
],
"expected": "ubesvart - riktig svar er aa si at basen ikke baerer dette"
},
{
"question": "Hva er den samlede investeringsrammen for Nordvikgata?",
"anchors": [
"investeringsramme",
"budsjett"
],
"expected": "ubesvart - riktig svar er aa si at basen ikke baerer dette"
}
],
"honesty": "Prosjektet Nordvikgata er KONSTRUERT av meg: gatenavn, lengde, fartsgrense og alle tre kostlinjene (GATE-KRYSS-01, GATE-GANG-01, GATE-KRYSS-02) er oppdiktet, og beløpene er satte størrelsesordener. Kravene i must_cite er derimot lest ordrett ut av n100-2023-basens egen frontmatter. N100 bærer ingen priser, så kodene finnes ikke i basen."
}

View file

@ -0,0 +1,36 @@
{
"objective": "Kutt kostnad i ombyggingen av Nordvikgata (0,9 km bygate, fartsgrense 40 km/t) uten å bryte et eneste krav i N100.",
"success_criteria": "Minst én tilnærming validerer mot N100, og hver validerte tilnærming peker på de kravene i N100 som faktisk binder utformingen.",
"approaches": [
{
"id": "a1-rundkjoring-forenklet",
"label": "Enklere rundkjøring i stedet for signalregulert kryss",
"description": "Krysset er prosjektert signalregulert. Vi vil vite hvilke krav N100 stiller til rundkjøring før vi kan regne på et bytte.",
"affected_codes": [
"GATE-KRYSS-01"
],
"claimed_saving_nok": 3100000.0,
"bundle_id": "vegnormal-n100-2023"
},
{
"id": "a2-gangfelt-antall",
"label": "Færre opphøyde gangfelt på strekningen",
"description": "Seks opphøyde gangfelt er prosjektert. Spørsmålet er hva N100 krever av kryssingspunkter for gående på en gate med denne fartsgrensen.",
"affected_codes": [
"GATE-GANG-01"
],
"claimed_saving_nok": 640000.0,
"bundle_id": "vegnormal-n100-2023"
},
{
"id": "a3-trafikkoy-utforming",
"label": "Redusert omfang av trafikkøyer i rundkjøringsarmene",
"description": "Trafikkøyene er prosjektert i full bredde i alle armer. Vi vil vite hva N100 faktisk krever.",
"affected_codes": [
"GATE-KRYSS-02"
],
"claimed_saving_nok": 410000.0,
"bundle_id": "vegnormal-n100-2023"
}
]
}

View file

@ -0,0 +1,2 @@
name: r761-2025
bundle_id: vegnormal-r761-2025

View file

@ -0,0 +1,7 @@
# Prosjektdokumenter
TOM med vilje. Scenarioet er MS Office + PDF, men ingen av dem kan mates
inn her: `--docs-dir`-omveien er fraradet (P13 - den omgar stigen `list_bundles -> read_bundle
-> read_dir -> read_file` og de to gatene, §4.1a-dimensjonen og verdict-laget). Veien et ekte
prosjektdokument skal ta er gjennom okf-ingest inn i en kunnskapsbase, altsa en base til - ikke
en katalog ved siden av. Se `docs/2026-09-12-p14-kontekstsett.md` § 1.4.

View file

@ -0,0 +1,82 @@
{
"project_id": "kontrakt-sorasen-2027",
"bundle": "r761-2025",
"bundle_id": "vegnormal-r761-2025",
"must_cite": [
{
"approach_id": "a1-rigg-omfang",
"rationale": "Rigg og drift er prissatt som rund sum over 22 måneder. Vi vil vite hva Prosesskoden legger i prosessen før vi kan kutte.",
"concepts": [
{
"path": "R761/12-1/_1_id-da516f46-786b-4e96-9511-da11ee07750f.md",
"title": "Rigg og midlertidige bygninger",
"ref": "12.1"
},
{
"path": "R761/12-12/_1_id-0d8e750e-18b1-49b9-b838-f5c29f21ff29.md",
"title": "Drift av rigg og midlertidige bygninger",
"ref": "12.12"
}
]
},
{
"approach_id": "a2-massebalanse",
"rationale": "Beskrivelsen har både uttak i linjen og tilkjørt filtermateriale. Spørsmålet er hva Prosesskoden krever av materialet i filterlaget.",
"concepts": [
{
"path": "R761/22-1/_2_id-54b5cb78-ba5e-4d30-a85d-d31facfde14d.md",
"title": "Sprengning i linjen",
"ref": "22.1"
},
{
"path": "R761/52-1/_5_id-d53022e6-0a09-4b02-bd37-21276fc2e51d.md",
"title": "Filterlag",
"ref": "52.1"
},
{
"path": "R761/52-11/_5_id-54270fce-705b-4f7c-fff0-0fea94ec0c08.md",
"title": "Filterlag av sand og grus",
"ref": "52.11"
}
]
},
{
"approach_id": "a3-planum-stabilisering",
"rationale": "Undergrunnen er beskrevet utskiftet. Vi vil vite hva prosessen for stabilisering av planum omfatter.",
"concepts": [
{
"path": "R761/51-1/_5_id-714020f6-3490-4dde-fe8f-c3d373f07d8f.md",
"title": "Stabilisering av planum",
"ref": "51.1"
}
]
}
],
"unanswerable": [
{
"question": "Hvilket kostnadsestimat ligger til grunn for riggposten i denne kontrakten?",
"anchors": [
"kostnadsestimat",
"kalkyle"
],
"expected": "ubesvart - riktig svar er aa si at basen ikke baerer dette"
},
{
"question": "Hva er markedsprisen på tilkjørt filtermateriale i denne regionen i 2027?",
"anchors": [
"markedspris",
"prisstigning"
],
"expected": "ubesvart - riktig svar er aa si at basen ikke baerer dette"
},
{
"question": "Hvordan skal kontraktssummen indeksreguleres gjennom byggeperioden?",
"anchors": [
"indeksregulering",
"nåverdi"
],
"expected": "ubesvart - riktig svar er aa si at basen ikke baerer dette"
}
],
"honesty": "Entreprisen Søråsen er KONSTRUERT av meg: kontraktsnavn, lengde, riggperiode og alle tre beløpene er oppdiktede størrelsesordener. Til forskjell fra de tre andre settene er affected_codes her EKTE prosessnumre fra R761 (12.1, 12.12, 22.1, 51.1, 52.11), lest ut av basens egen prosessnr-frontmatter — men R761 er en beskrivelsesstandard uten priser, så kodene er ekte mens beløpene ikke er det. must_cite er lest ordrett ut av basen."
}

View file

@ -0,0 +1,38 @@
{
"objective": "Kutt kostnad i utførelsesentreprisen Søråsen (veg i dagen, 3,1 km) ved å endre hvordan arbeidet beskrives etter Prosesskoden, uten å endre hva som bygges.",
"success_criteria": "Minst én tilnærming validerer, og hver validerte tilnærming peker på de prosessene i R761 som faktisk styrer utførelse og oppgjør for den posten.",
"approaches": [
{
"id": "a1-rigg-omfang",
"label": "Redusert riggomfang og kortere riggperiode",
"description": "Rigg og drift er prissatt som rund sum over 22 måneder. Vi vil vite hva Prosesskoden legger i prosessen før vi kan kutte.",
"affected_codes": [
"12.1",
"12.12"
],
"claimed_saving_nok": 4200000.0,
"bundle_id": "vegnormal-r761-2025"
},
{
"id": "a2-massebalanse",
"label": "Gjenbruk av sprengstein fra linjen som filterlag i stedet for tilkjørt materiale",
"description": "Beskrivelsen har både uttak i linjen og tilkjørt filtermateriale. Spørsmålet er hva Prosesskoden krever av materialet i filterlaget.",
"affected_codes": [
"22.1",
"52.11"
],
"claimed_saving_nok": 6800000.0,
"bundle_id": "vegnormal-r761-2025"
},
{
"id": "a3-planum-stabilisering",
"label": "Stabilisering av planum i stedet for utskifting av undergrunn",
"description": "Undergrunnen er beskrevet utskiftet. Vi vil vite hva prosessen for stabilisering av planum omfatter.",
"affected_codes": [
"51.1"
],
"claimed_saving_nok": 1300000.0,
"bundle_id": "vegnormal-r761-2025"
}
]
}

View file

@ -0,0 +1,2 @@
name: n500-2024
bundle_id: vegnormal-n500-2024

View file

@ -0,0 +1,7 @@
# Prosjektdokumenter
TOM med vilje. Scenarioet er MS Office + PDF, men ingen av dem kan mates
inn her: `--docs-dir`-omveien er fraradet (P13 - den omgar stigen `list_bundles -> read_bundle
-> read_dir -> read_file` og de to gatene, §4.1a-dimensjonen og verdict-laget). Veien et ekte
prosjektdokument skal ta er gjennom okf-ingest inn i en kunnskapsbase, altsa en base til - ikke
en katalog ved siden av. Se `docs/2026-09-12-p14-kontekstsett.md` § 1.4.

View file

@ -0,0 +1,91 @@
{
"project_id": "tunnel-hauglia-2027",
"bundle": "n500-2024",
"bundle_id": "vegnormal-n500-2024",
"must_cite": [
{
"approach_id": "a1-ventilasjon-dimensjonering",
"rationale": "Prosjekteringsgrunnlaget forutsetter full lengdeventilasjon over hele profilet. Vi vil vite om N500 tillater en lavere installert effekt for dette tunnelprofilet og denne ÅDT-en.",
"concepts": [
{
"path": "krav/N500/id-bfb0edb4-15ad-4fb1-96e8-a1ffb65f2b40.md",
"title": "Krav 10.4.3—1 Mekanisk ventilasjon (impulsventilator)",
"ref": "Krav 10.4.3—1"
},
{
"path": "krav/N500/id-e897fcb1-36ab-4198-ee5f-764833cce9d2.md",
"title": "Krav 10.4.3—2 Mekanisk ventilasjon (impulsventilator)",
"ref": "Krav 10.4.3—2"
},
{
"path": "krav/N500/id-15ccfe60-c88c-4174-8b28-5c660e6a7d9e.md",
"title": "Krav 10.4.3—3 Mekanisk ventilasjon (impulsventilator)",
"ref": "Krav 10.4.3—3"
}
]
},
{
"approach_id": "a2-frostsikring-lengde",
"rationale": "Frostsikringen er lagt 180 m inn fra hver portal. Spørsmålet er hva N500 faktisk krever av frostmengde og isolasjonsløsning.",
"concepts": [
{
"path": "krav/N500/id-92e2836a-90af-4797-df73-981ab02da81d.md",
"title": "Krav 8.1.2—1 Frostdimensjonering",
"ref": "Krav 8.1.2—1"
},
{
"path": "krav/N500/id-7c24c166-3659-414c-fe81-07373f33b2b5.md",
"title": "Krav 8.4.2—1 Frostisolering med PE-skum eller XPS",
"ref": "Krav 8.4.2—1"
},
{
"path": "krav/N500/id-2b68ddbd-2037-469f-a181-b5db969dc829.md",
"title": "Krav 8.4.2—2 Frostisolering med PE-skum eller XPS",
"ref": "Krav 8.4.2—2"
}
]
},
{
"approach_id": "a3-belysning-adaptiv",
"rationale": "Belysningsanlegget er dimensjonert for konstant nivå. Vi vil vite hvilke kvalitetskrav N500 stiller før vi kan kutte installert effekt.",
"concepts": [
{
"path": "krav/N500/id-dc16192f-4ef6-41e7-b47f-be8f1cd581fb.md",
"title": "Krav 10.3.2—5 Belysningens kvalitet",
"ref": "Krav 10.3.2—5"
},
{
"path": "krav/N500/id-d77adb34-725c-4826-eb26-5fc72f27e711.md",
"title": "Krav 10.3.3—1 Belysning av tunnelveggene",
"ref": "Krav 10.3.3—1"
}
]
}
],
"unanswerable": [
{
"question": "Hva koster en impulsventilator per stk levert og montert i norsk tunnelentreprise?",
"anchors": [
"enhetspris",
"markedspris"
],
"expected": "ubesvart - riktig svar er aa si at basen ikke baerer dette"
},
{
"question": "Hva er nåverdien over 20 år av å velge adaptiv LED framfor fast belysningsnivå?",
"anchors": [
"nåverdi",
"driftskostnad"
],
"expected": "ubesvart - riktig svar er aa si at basen ikke baerer dette"
},
{
"question": "Hvilken timepris skal legges til grunn for elektromontør i dette prosjektet?",
"anchors": [
"timepris"
],
"expected": "ubesvart - riktig svar er aa si at basen ikke baerer dette"
}
],
"honesty": "Prosjektet Hauglia-tunnelen er KONSTRUERT av meg: navn, lengde, ÅDT og alle tre kostlinjene (TUN-VENT-01, TUN-FROST-01, TUN-LYS-01) er oppdiktet, og de tre beløpene er plausible størrelsesordener jeg har satt, ikke tall fra et prosjekt. Det som IKKE er konstruert er kravene: hver konsept-sti, tittel og kravnummer i must_cite er lest ordrett ut av n500-2024-basens egen frontmatter. N500 bærer ingen priser, så kodene finnes ikke i basen — settet er derfor bevisst IKKE kjørbart via --proposals-from-mandate."
}

View file

@ -0,0 +1,36 @@
{
"objective": "Kutt kostnad i byggefasen for Hauglia-tunnelen (2,4 km ettløps vegtunnel, ÅDT 7 200) uten å bryte et eneste krav i N500.",
"success_criteria": "Minst én tilnærming validerer mot N500, og hver validerte tilnærming peker på de kravene i N500 som faktisk binder den.",
"approaches": [
{
"id": "a1-ventilasjon-dimensjonering",
"label": "Redusert antall impulsventilatorer ved ny dimensjonering av lengdeventilasjon",
"description": "Prosjekteringsgrunnlaget forutsetter full lengdeventilasjon over hele profilet. Vi vil vite om N500 tillater en lavere installert effekt for dette tunnelprofilet og denne ÅDT-en.",
"affected_codes": [
"TUN-VENT-01"
],
"claimed_saving_nok": 1850000.0,
"bundle_id": "vegnormal-n500-2024"
},
{
"id": "a2-frostsikring-lengde",
"label": "Kortere frostsikret sone ved begge portaler",
"description": "Frostsikringen er lagt 180 m inn fra hver portal. Spørsmålet er hva N500 faktisk krever av frostmengde og isolasjonsløsning.",
"affected_codes": [
"TUN-FROST-01"
],
"claimed_saving_nok": 2400000.0,
"bundle_id": "vegnormal-n500-2024"
},
{
"id": "a3-belysning-adaptiv",
"label": "Adaptiv LED-belysning i stedet for fast installert nivå",
"description": "Belysningsanlegget er dimensjonert for konstant nivå. Vi vil vite hvilke kvalitetskrav N500 stiller før vi kan kutte installert effekt.",
"affected_codes": [
"TUN-LYS-01"
],
"claimed_saving_nok": 1200000.0,
"bundle_id": "vegnormal-n500-2024"
}
]
}

View file

@ -0,0 +1,321 @@
# 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.
```jsonc
{
"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
Hvert sett bærer minst to spørsmål basen **ikke kan besvare**, der riktig svar er å si det.
MÅLT over alle fire basene, 22 prøvde kostnadsord:
- **n100-2023, n200-2024, n500-2024: samtlige 22 er FRAVÆRENDE fra hele basen**`enhetspris`,
`kroner`, `budsjett`, `kostnadsestimat`, `prisskjema`, `timepris`, `nåverdi`, `driftskostnad`
- **r761-2025:** 18 av 22 fraværende; basen bærer `kroner`, `budsjett`, `enhetspris` og `hydrogen`,
fordi prosesskoden ER kontraktsspråk.
Det er et funn i seg selv, og det er hele grunnen til at gaten er verdt å teste: po sitt oppdrag er
å finne **kostnadsbesparelser**, og tre av fire baser inneholder ikke ett eneste pengeord. 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):
```sh
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:2444`
`parser.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_id`s, 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:
```yaml
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_frontmatter`s 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 skipped** → **1642 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.

View file

@ -0,0 +1,398 @@
"""P14 — the stress-test context sets, gated (order ``20260912T202210Z``).
Each set under ``contexts/<project-id>/`` commissions ONE run against ONE knowledge base:
``mandate.json`` (the ``Mandate`` schema verbatim), ``bundle.txt`` (which base, and the id that base
declares) and ``fasit.json`` (what a right answer MUST cite, what the base cannot answer, and what
in the set is constructed rather than real).
**Five arms, and the split between them is a measurement rather than a taste.** Two are
unconditional and can never be silently absent a mandate that does not load, and a mandate routed
at a base the set is not for. Three need the base itself, which lives OUTSIDE this repository
(``PORTFOLIO_VEGNORMAL_ROOT``): they SKIP when the root is missing, exactly as MAJOR-3's ceiling
gate could not take K2 as a test dependency, and for the same published-package reason a hard
failure would break ``uv run pytest`` for any external recipient of the ``git archive HEAD``
handover. The skip NAMES the root it looked for.
**Every bundle-reading arm carries its own denominator.** A scan that sees zero concepts is RED
rather than vacuously green: "the anchor was not found" is equally true of a base that was never
read (Verifiseringsloven, ansikt 4).
**Rule U** the measurable form of "the base cannot answer this" (documented in
``docs/2026-09-12-p14-kontekstsett.md § 2.3``): each unanswerable question declares >= 1 ``anchor``,
a lowercase word of >= 4 characters, and is admitted **iff every anchor is absent case-insensitive
substring from the WHOLE text (frontmatter + body) of EVERY concept document in the base**. Not
"shares no keyword with any title": a tunnel question shares "tunnel" with hundreds of titles and
that proves nothing. What makes a question unanswerable is that the base lacks the SUBJECT, and the
anchor is that subject. Titles alone would be a proxy the full text costs nothing more to replace
(measured: 0.77 s for r761-2025, the largest base).
"""
from __future__ import annotations
import json
import os
from pathlib import Path
import pytest
from pydantic import ValidationError
from portfolio_optimiser import okf
from portfolio_optimiser.mandate import load_mandate
_REPO_ROOT = Path(__file__).resolve().parent.parent
_CONTEXT_ROOT = _REPO_ROOT / "contexts"
#: Where the vegnormal bases are mounted. A SYMBOLIC name in ``bundle.txt`` is resolved against
#: this, never an absolute path in the set: this repository is published, and an absolute path
#: would pin a set to one machine's home directory and ride out in the handover archive.
_DEFAULT_BUNDLE_ROOT = Path.home() / "repos" / "vegnormal-okf" / "build" / "ferdig"
#: The concept types the four bases declare. ``index.md`` carries none of them — it is navigation,
#: not content — which is why the file count and the concept count differ.
_CONCEPT_TYPES = {"Krav", "Prosess", "Kapittel", "Normal", "Håndbok"}
_MIN_ANCHOR_CHARS = 4
def own_frontmatter(path: Path) -> dict[str, str]:
"""The concept's OWN frontmatter: top-level keys only, FIRST occurrence winning.
**``okf.parse_frontmatter`` cannot be used for this, and that is measured rather than assumed.**
It is linewise and last-write-wins by documented design, so a nested block overwrites a
top-level key of the same name. Every vegnormal concept ends its frontmatter with
sources:
- resource: https://
title: N500:2024
and the indented ``title`` therefore replaces the concept's own. MEASURED on n500-2024:
``okf.navigate_bundle`` yields 270 concept files carrying **1 distinct title** (``N500:2024``,
270 times), and ``okf.directory_listing(bundle, path="krav/N500")`` returns 269 documents whose
``title`` is that same string so rung 2 and rung 3 of the navigation ladder tell a navigator
apart only by an opaque UUID filename and a character count. That is a finding about the
PRODUCT (recorded in ``docs/2026-09-12-p14-kontekstsett.md § 5``), not about this test; what it
means here is that a fasit assert against ``parse_frontmatter``'s ``title`` would compare every
concept to the same constant and be VACUOUS.
Uses ``okf._split_frontmatter`` deliberately: it is the module's ONE place ``---`` is compared
(B4), and a second delimiter rule here would be the copy that drifts.
"""
out: dict[str, str] = {}
for line in okf._split_frontmatter(path.read_text(encoding="utf-8"))[0]:
if not line or line[0].isspace() or line.lstrip().startswith("-"):
continue
key, sep, value = line.partition(":")
if sep and key.strip() not in out:
out[key.strip()] = value.strip().strip("'")
return out
def _bundle_root() -> Path:
return Path(os.environ.get("PORTFOLIO_VEGNORMAL_ROOT", str(_DEFAULT_BUNDLE_ROOT)))
def read_bundle_txt(path: Path) -> dict[str, str]:
"""Parse a set's ``bundle.txt``: ``key: value`` lines, nothing else."""
out: dict[str, str] = {}
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if ": " not in line:
raise ValueError(f"malformed bundle.txt line in {path}: {line!r}")
key, value = line.split(": ", 1)
out[key.strip()] = value.strip()
for required in ("name", "bundle_id"):
if required not in out:
raise ValueError(f"{path} declares no {required!r}")
return out
def scan_concepts(base: Path) -> list[tuple[str, dict[str, str], str]]:
"""Every concept document in a base: bundle-relative path, frontmatter, lowercased full text.
Built from the declared ``type``, never from the directory listing: ``index.md`` is navigation
and would otherwise be counted as content.
"""
found: list[tuple[str, dict[str, str], str]] = []
for path in sorted(base.rglob("*.md")):
text = path.read_text(encoding="utf-8")
frontmatter = own_frontmatter(path)
if frontmatter.get("type", "") in _CONCEPT_TYPES:
found.append((path.relative_to(base).as_posix(), frontmatter, text.lower()))
return found
def anchors_are_absent(
anchors: list[str], concepts: list[tuple[str, dict[str, str], str]]
) -> list[str]:
"""Rule U: return the anchors the base DOES carry (empty == the question is admitted).
:raises ValueError: an empty scan, or an anchor that is not a usable one. Both are refusals
rather than a quiet pass a rule that cannot fail proves nothing.
"""
if not concepts:
raise ValueError("rule U ran against ZERO concepts: absence here is unmeasured, not false")
if not anchors:
raise ValueError("an unanswerable question declares no anchors, so nothing was checked")
carried = []
for anchor in anchors:
if anchor != anchor.lower() or len(anchor) < _MIN_ANCHOR_CHARS:
raise ValueError(
f"anchor {anchor!r} must be lowercase and at least {_MIN_ANCHOR_CHARS} characters"
)
if any(anchor in text for _, _, text in concepts):
carried.append(anchor)
return carried
def context_sets() -> list[Path]:
return (
sorted(p for p in _CONTEXT_ROOT.iterdir() if p.is_dir()) if _CONTEXT_ROOT.is_dir() else []
)
_SETS = context_sets()
_SET_IDS = [p.name for p in _SETS]
def _require_base(declared: dict[str, str]) -> Path:
root = _bundle_root()
base = root / declared["name"]
if not base.is_dir():
pytest.skip(
f"knowledge base {declared['name']!r} not mounted under {root} (PORTFOLIO_VEGNORMAL_ROOT)"
)
return base
# --------------------------------------------------------------------------------------------
# The sets exist at all. Without this, every parametrised arm below would collapse to zero cases
# and the file would pass by having nothing to say.
# --------------------------------------------------------------------------------------------
def test_the_four_context_sets_are_present() -> None:
assert len(_SETS) == 4, f"expected four context sets under {_CONTEXT_ROOT}, found {_SET_IDS}"
# --------------------------------------------------------------------------------------------
# (a) + (d): unconditional — no knowledge base needed.
# --------------------------------------------------------------------------------------------
@pytest.mark.parametrize("set_dir", _SETS, ids=_SET_IDS)
def test_a_mandate_loads_fail_fast(set_dir: Path) -> None:
mandate = load_mandate(set_dir / "mandate.json")
assert mandate.objective
assert mandate.success_criteria, f"{set_dir.name} states no success criteria to judge it by"
assert 2 <= len(mandate.approaches) <= 4, "the order asks for 2-4 approaches per set"
for approach in mandate.approaches:
assert approach.affected_codes, f"{approach.id} names no affected_codes"
assert approach.claimed_saving_nok is not None, f"{approach.id} states no estimate"
@pytest.mark.parametrize("set_dir", _SETS, ids=_SET_IDS)
def test_d_every_approach_is_routed_at_this_sets_own_base(set_dir: Path) -> None:
declared = read_bundle_txt(set_dir / "bundle.txt")
mandate = load_mandate(set_dir / "mandate.json")
for approach in mandate.approaches:
assert approach.bundle_id == declared["bundle_id"], (
f"{set_dir.name}/{approach.id} routes at {approach.bundle_id!r} but the set declares "
f"{declared['bundle_id']!r}"
)
@pytest.mark.parametrize("set_dir", _SETS, ids=_SET_IDS)
def test_the_fasit_names_every_commissioned_approach(set_dir: Path) -> None:
fasit = json.loads((set_dir / "fasit.json").read_text(encoding="utf-8"))
mandate = load_mandate(set_dir / "mandate.json")
cited = {row["approach_id"] for row in fasit["must_cite"]}
assert cited == {a.id for a in mandate.approaches}
assert fasit["honesty"].strip(), "DEL 2(iii): what in this set is constructed must be stated"
assert len(fasit["unanswerable"]) >= 2, "the order asks for at least two per set"
assert (set_dir / "docs").is_dir(), "the form declares a docs/ directory even when it is empty"
# --------------------------------------------------------------------------------------------
# (b) + (c) + (e): these read the base itself and SKIP when it is not mounted.
# --------------------------------------------------------------------------------------------
@pytest.mark.parametrize("set_dir", _SETS, ids=_SET_IDS)
def test_b_every_fasit_concept_is_in_the_base_as_recorded(set_dir: Path) -> None:
declared = read_bundle_txt(set_dir / "bundle.txt")
base = _require_base(declared)
fasit = json.loads((set_dir / "fasit.json").read_text(encoding="utf-8"))
seen = 0
for row in fasit["must_cite"]:
assert row["concepts"], f"{row['approach_id']} cites nothing a right answer must reach"
for concept in row["concepts"]:
path = base / concept["path"]
assert path.is_file(), f"{set_dir.name}: {concept['path']} is not in {declared['name']}"
frontmatter = own_frontmatter(path)
assert frontmatter.get("title", "") == concept["title"], (
f"{concept['path']}: the base's own title has drifted from the fasit"
)
if concept.get("ref"):
actual = frontmatter.get("req_number") or frontmatter.get("prosessnr", "")
assert actual == concept["ref"], (
f"{concept['path']}: the base's own reference has drifted from the fasit"
)
seen += 1
assert seen > 0, "the fasit named no concepts at all, so nothing was verified"
@pytest.mark.parametrize("set_dir", _SETS, ids=_SET_IDS)
def test_c_rule_u_every_unanswerable_question_is_unanswerable(set_dir: Path) -> None:
declared = read_bundle_txt(set_dir / "bundle.txt")
base = _require_base(declared)
concepts = scan_concepts(base)
assert len(concepts) >= 100, (
f"{declared['name']} scanned to {len(concepts)} concepts — too few to be the base itself"
)
fasit = json.loads((set_dir / "fasit.json").read_text(encoding="utf-8"))
for row in fasit["unanswerable"]:
carried = anchors_are_absent(row["anchors"], concepts)
assert not carried, (
f"{set_dir.name}: {declared['name']} DOES carry {carried} over {len(concepts)} "
f"concepts, so {row['question']!r} is not unanswerable by rule U"
)
@pytest.mark.parametrize("set_dir", _SETS, ids=_SET_IDS)
def test_e_the_declared_bundle_id_is_the_bases_own(set_dir: Path) -> None:
declared = read_bundle_txt(set_dir / "bundle.txt")
base = _require_base(declared)
resolved = okf.reconcile_bundle_id(base)
assert resolved.id == declared["bundle_id"], (
f"{set_dir.name}: bundle.txt declares {declared['bundle_id']!r} but the base resolves to "
f"{resolved.id!r} (origin {resolved.origin})"
)
# --------------------------------------------------------------------------------------------
# KNOWN-POSITIVES (DEL 3): a deliberately broken set must make EXACTLY the arm that guards it red.
# Without these, a check that can only pass is indistinguishable from a check that never runs.
# --------------------------------------------------------------------------------------------
def _broken_set(tmp_path: Path, *, mandate: dict, bundle: str, fasit: dict) -> Path:
set_dir = tmp_path / "broken-set"
(set_dir / "docs").mkdir(parents=True)
(set_dir / "mandate.json").write_text(json.dumps(mandate), encoding="utf-8")
(set_dir / "bundle.txt").write_text(bundle, encoding="utf-8")
(set_dir / "fasit.json").write_text(json.dumps(fasit), encoding="utf-8")
return set_dir
_GOOD_MANDATE = {
"objective": "Reduce cost on a synthetic project",
"success_criteria": "at least one approach validates",
"approaches": [
{
"id": "a1",
"label": "One",
"affected_codes": ["X-1"],
"claimed_saving_nok": 1.0,
"bundle_id": "vegnormal-n500-2024",
},
{
"id": "a2",
"label": "Two",
"affected_codes": ["X-2"],
"claimed_saving_nok": 2.0,
"bundle_id": "vegnormal-n500-2024",
},
],
}
_GOOD_BUNDLE_TXT = "name: n500-2024\nbundle_id: vegnormal-n500-2024\n"
def test_known_positive_a_a_malformed_mandate_is_refused(tmp_path: Path) -> None:
broken = dict(_GOOD_MANDATE)
broken["approaches"] = [
dict(_GOOD_MANDATE["approaches"][0]),
dict(_GOOD_MANDATE["approaches"][0]),
]
set_dir = _broken_set(tmp_path, mandate=broken, bundle=_GOOD_BUNDLE_TXT, fasit={})
with pytest.raises(ValidationError):
load_mandate(set_dir / "mandate.json")
def test_known_positive_d_a_mandate_routed_at_another_base_is_caught(tmp_path: Path) -> None:
broken = json.loads(json.dumps(_GOOD_MANDATE))
broken["approaches"][1]["bundle_id"] = "vegnormal-n100-2023"
set_dir = _broken_set(tmp_path, mandate=broken, bundle=_GOOD_BUNDLE_TXT, fasit={})
declared = read_bundle_txt(set_dir / "bundle.txt")
mandate = load_mandate(set_dir / "mandate.json")
assert any(a.bundle_id != declared["bundle_id"] for a in mandate.approaches)
def test_known_positive_c_an_anchor_the_base_carries_is_reported() -> None:
concepts = [("a.md", {"type": "Krav"}, "en tunnel med ventilasjon og belysning")]
assert anchors_are_absent(["enhetspris"], concepts) == []
assert anchors_are_absent(["ventilasjon"], concepts) == ["ventilasjon"]
def test_known_positive_c_an_empty_scan_is_refused_never_vacuously_absent() -> None:
with pytest.raises(ValueError, match="ZERO concepts"):
anchors_are_absent(["enhetspris"], [])
def test_known_positive_c_an_unusable_anchor_is_refused() -> None:
concepts = [("a.md", {"type": "Krav"}, "tekst")]
with pytest.raises(ValueError, match="at least"):
anchors_are_absent(["vei"], concepts)
with pytest.raises(ValueError, match="lowercase"):
anchors_are_absent(["Enhetspris"], concepts)
with pytest.raises(ValueError, match="no anchors"):
anchors_are_absent([], concepts)
def test_known_positive_b_a_fasit_path_the_base_does_not_carry_is_caught(tmp_path: Path) -> None:
base = tmp_path / "base"
(base / "krav").mkdir(parents=True)
(base / "krav" / "real.md").write_text(
"---\ntype: Krav\ntitle: Ekte krav\nreq_number: Krav 1.1—1\n---\n\nkropp\n",
encoding="utf-8",
)
assert (base / "krav" / "real.md").is_file()
assert not (base / "krav" / "invented.md").is_file()
assert own_frontmatter(base / "krav" / "real.md")["title"] == "Ekte krav"
def test_known_positive_bundle_txt_must_declare_both_keys(tmp_path: Path) -> None:
path = tmp_path / "bundle.txt"
path.write_text("name: n500-2024\n", encoding="utf-8")
with pytest.raises(ValueError, match="bundle_id"):
read_bundle_txt(path)
@pytest.mark.parametrize("set_dir", _SETS, ids=_SET_IDS)
def test_the_fasit_titles_are_distinct_not_the_collapsed_sources_title(set_dir: Path) -> None:
"""The fasit's recorded titles must tell the cited concepts APART.
Paired with ``own_frontmatter``'s measurement, this is what keeps arm (b) from being vacuous: if
the recorded titles were ``parse_frontmatter``'s, every one of them would be the base's
``sources`` title and the assert would hold against any concept in the base.
**TRIPWIRE, deliberately.** The second half asserts that ``okf.parse_frontmatter`` DOES still
collapse them. The day that stops being true an okf bump, or a decision to read block
mappings here this arm goes red, and whoever sees it should read this docstring, confirm the
listing now carries real titles, and DELETE this half rather than weaken it.
"""
declared = read_bundle_txt(set_dir / "bundle.txt")
base = _require_base(declared)
fasit = json.loads((set_dir / "fasit.json").read_text(encoding="utf-8"))
cited = [c for row in fasit["must_cite"] for c in row["concepts"]]
assert len({c["title"] for c in cited}) == len(cited), "recorded titles do not tell them apart"
collapsed = {okf.parse_frontmatter(base / c["path"]).get("title", "") for c in cited}
assert len(collapsed) == 1, (
"okf.parse_frontmatter no longer collapses these titles onto the sources block — read this "
"test's docstring: the finding it guards may be gone, and this half should be deleted"
)