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:
parent
fed69790ac
commit
45edbf5957
19 changed files with 1282 additions and 0 deletions
44
CLAUDE.md
44
CLAUDE.md
|
|
@ -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` på `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.
|
||||||
|
|
||||||
|
|
|
||||||
2
contexts/fv412-dekkefornyelse-2027/bundle.txt
Normal file
2
contexts/fv412-dekkefornyelse-2027/bundle.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
name: n200-2024
|
||||||
|
bundle_id: vegnormal-n200-2024
|
||||||
7
contexts/fv412-dekkefornyelse-2027/docs/README.md
Normal file
7
contexts/fv412-dekkefornyelse-2027/docs/README.md
Normal 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.
|
||||||
82
contexts/fv412-dekkefornyelse-2027/fasit.json
Normal file
82
contexts/fv412-dekkefornyelse-2027/fasit.json
Normal 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."
|
||||||
|
}
|
||||||
36
contexts/fv412-dekkefornyelse-2027/mandate.json
Normal file
36
contexts/fv412-dekkefornyelse-2027/mandate.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
2
contexts/gate-nordvik-2027/bundle.txt
Normal file
2
contexts/gate-nordvik-2027/bundle.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
name: n100-2023
|
||||||
|
bundle_id: vegnormal-n100-2023
|
||||||
7
contexts/gate-nordvik-2027/docs/README.md
Normal file
7
contexts/gate-nordvik-2027/docs/README.md
Normal 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.
|
||||||
82
contexts/gate-nordvik-2027/fasit.json
Normal file
82
contexts/gate-nordvik-2027/fasit.json
Normal 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."
|
||||||
|
}
|
||||||
36
contexts/gate-nordvik-2027/mandate.json
Normal file
36
contexts/gate-nordvik-2027/mandate.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
2
contexts/kontrakt-sorasen-2027/bundle.txt
Normal file
2
contexts/kontrakt-sorasen-2027/bundle.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
name: r761-2025
|
||||||
|
bundle_id: vegnormal-r761-2025
|
||||||
7
contexts/kontrakt-sorasen-2027/docs/README.md
Normal file
7
contexts/kontrakt-sorasen-2027/docs/README.md
Normal 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.
|
||||||
82
contexts/kontrakt-sorasen-2027/fasit.json
Normal file
82
contexts/kontrakt-sorasen-2027/fasit.json
Normal 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."
|
||||||
|
}
|
||||||
38
contexts/kontrakt-sorasen-2027/mandate.json
Normal file
38
contexts/kontrakt-sorasen-2027/mandate.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
2
contexts/tunnel-hauglia-2027/bundle.txt
Normal file
2
contexts/tunnel-hauglia-2027/bundle.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
name: n500-2024
|
||||||
|
bundle_id: vegnormal-n500-2024
|
||||||
7
contexts/tunnel-hauglia-2027/docs/README.md
Normal file
7
contexts/tunnel-hauglia-2027/docs/README.md
Normal 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.
|
||||||
91
contexts/tunnel-hauglia-2027/fasit.json
Normal file
91
contexts/tunnel-hauglia-2027/fasit.json
Normal 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."
|
||||||
|
}
|
||||||
36
contexts/tunnel-hauglia-2027/mandate.json
Normal file
36
contexts/tunnel-hauglia-2027/mandate.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
321
docs/2026-09-12-p14-kontekstsett.md
Normal file
321
docs/2026-09-12-p14-kontekstsett.md
Normal 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/` | 0–n 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
|
||||||
|
«0–n» 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.
|
||||||
398
tests/test_context_sets_loadbearing.py
Normal file
398
tests/test_context_sets_loadbearing.py
Normal 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"
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue