portfolio-optimiser/docs/2026-09-09-p8-forankringstilbudet.md
Kjell Tore Guttormsen 455d611660 feat(run,generate): a run says what its delivered input can ground, before it spends an attempt [skip-docs]
P7 is right and landed, but re-measuring it exposed a consequence no row stated: with the gate
live, 29 of 29 cost codes in 13 of 13 delivered proposals fall across the three free recordings
(PM's denominator; 14 of 14 in 8 proposals on the PARSEABLE one -- the five blobs that separate
the numbers are refused by pydantic's `claimed <= total` and never reach stage 0b). All 29 were
invented, so the gate is right; but a gate that always refuses is as useless as one that never
does.

The cause is that the PROMPT asks for something the input cannot supply. `_build_messages`
requires each affected_item to "restate a cost line as the project's price schedule already
carries it", while K2's delivered input carries 9 occurrences / 2 distinct code-shaped tokens --
`SHA-01`/`SHA-10`, both document numbers off a page footer -- and `derive_cost_baseline` refuses
the base outright. There is no cost line in it to restate.

`GroundingOffer(chars, identifiers, cost_lines)` reports it. The PAIR is the diagnosis: "50
identifiers, 0 cost lines" says what neither number says alone. A REPORT, never a gate -- it
blocks nothing, because a blocking requirement IS `--require-cost-baseline` (F4/D-3, opt-in,
untouched), and `_ground_against_input` is untouched.

The callsite is MEASURED, not chosen: `generate.py` composes the grounding per attempt, after
`await _fetch_parsed`, so a report there could only speak once an attempt had been paid for;
`run.py` binds both halves above the `--live-dry-run` cut and before the first `debate.run`, so
the FREE trip says it. `delivered` is bound ONCE and the same variable feeds the report and
`_evaluate`; the report composes THROUGH `_grounding_text`, the gate's own composer.

A pattern is admissible here and not in the gate, and that is the difference between a report and
a falsifier: an unknown form is a token left uncounted -- an under-count, never a false rejection.
The forms are transcribed from the measurement; bare numbers are excluded with the number
(46 394 / 2 117 in K2). `grounding_offer_notice` is the ONE renderer and is silent when the run
CAN anchor -- omission, never an empty row.

Load-bearing MEASURED (tests/test_grounding_offer_loadbearing.py, 12 arms), nine mutations all
red against the WHOLE suite + green control 1570/5 (from 1558/5, strict superset, 0 removed) and
the golden byte-unchanged (shasum -a 1 of the CONTENT = ea8c534773acdbe41ae68f2c55724d69aaf8be4f).

Measurement: docs/2026-09-09-p8-forankringstilbudet.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 17:50:02 +02:00

227 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# P8 — den leverte inputen bærer ingen kostlinjer, så ingen forankret proposal kan oppstå
Ordre `20260909T142951Z-365478643-from-.claude`, økt 110. HEAD ved start: `277bb95`.
Alt i dette dokumentet er målt **gratis** — null modellkall, **NOK 0,00**.
## § 0 Hva som ER målt og hva som IKKE er det
**Målt:** hva de tre gratis opptakene faktisk leverte og hvor mange av kodene P7-gaten feller;
hva den LEVERTE inputen i hvert korpus po har liggende kan tilby som lovlig `affected_item`-kode;
og at rapporten som nå bygges sier null der tilbudet er null og positivt der det er positivt.
**Ikke målt:** at rapporten endrer noe LEVENDE. Ingen betalt kjøring er gjort, og ingen er
bestilt. At en modell som får se linja oppfører seg annerledes er ikke vist — samme klasse som
structured-output-grensen. Rapporten er dessuten skrevet på grunnlag av tre opptak fra **ÉN**
modell og **ETT** deployment.
**Ikke bygget, med vilje:** prosa-skanningen (operatørbeslutning A) og
`--require-cost-baseline` som default (operatørbeslutning, F4/D-3). Begge står uendret.
## § 1 Premissene (i)(viii) — hver verifisert selv
| # | Premiss (PM) | Mitt utfall |
|---|---|---|
| (i) | HEAD `277bb95`, `git ls-remote origin main` = `eb41374`, upushet 2 | **BEKREFTET** ordrett |
| (ii) | To utrackede: presentasjons-HTML (parallell økt) + `scratchpad/` | **BEKREFTET**, intet tredje |
| (iii) | 1558 passed / 5 skipped, ruff rent, mypy rent (37 filer), golden `ea8c534…` | **BEKREFTET** alle fire |
| (iv) | 13 forslag / 29 koder / 29 ugrunnet | **BEKREFTET på PMs nevner, med et avvik — se § 2** |
| (v) | K2 leverer 9/2 distinkte kodeformede (`SHA-01`/`SHA-10`, dokumentnumre); N100 0 kodeformede, 17/8 kravnumre | **BEKREFTET ordrett** |
| (vi) | Ved uttømte forsøk returneres siste `Rejection`, ingen krasj | **BEKREFTET**: `generate.py` returnerer `GenerationResult(outcome=last_ruling, …)` etter forsøksløkka |
| (vii) | `--require-cost-baseline` er opt-in og skal ikke bli default | **RESPEKTERT**, urørt |
| (viii) | (A) og (B) er operatørens | **RESPEKTERT**, ikke avgjort her |
## § 2 Konsekvensen — og AVVIKET mot PMs tall
PMs tall er reprodusert nøyaktig **på PMs nevner**, men den nevneren teller blobs som aldri
NÅR P7-gaten. Begge tall er sanne om hver sin ting, og begge gir 100 %:
| Opptak | Rå JSON-blobs | Koder | Ugrunnet | Parsebare til IR | Koder | Ugrunnet |
|---|---|---|---|---|---|---|
| `p6-Aopen-records.json` | 2 | 4 | **4** | 2 | 4 | **4** |
| `Aopen-records.json` | 8 | 22 | **22** | 3 | 7 | **7** |
| `p4-n100-free-records.json` | 3 | 3 | **3** | 3 | 3 | **3** |
| **Totalt** | **13** | **29** | **29 (100 %)** | **8** | **14** | **14 (100 %)** |
De fem blobene som skiller tallene avvises av en **annen og TIDLIGERE falsifiserer**
pydantics `claimed_saving_nok <= affected items' total` — og når derfor aldri stadium 0b.
Det er ikke en svakhet i PMs måling; det er to nevnere om to ulike hendelser. Grunnlaget er
det **mest sjenerøse** som finnes: unionen av ALLE prompter i opptaket. Selv der er ingen kode
grunnet.
Et instrument-forbehold, skrevet ned fordi det først ga feil svar: en rå
`SavingsProposal.model_validate_json` avviser **alle 13**, fordi svarene bærer WIRE-formen av
`assumptions` (et array) som `_normalise_assumptions` folder tilbake. En måling som stoppet der
ville rapportert 0 forslag — null fordi instrumentet ikke kunne lese formen, ikke fordi formen
manglet.
## § 3 Årsaken — prompten ber om noe inputen ikke kan levere
`generate._build_messages` sier ordrett: «Each entry in `affected_items` must restate a cost line
as the project's price schedule already carries it». Målt på record 0 (turen som bar bundelen):
* **K2** (P6: 96 567 tegn; S7c: 145 281 tegn) → **9 forekomster / 2 distinkte** kodeformede, og
begge er `SHA-01` / `SHA-10`. Konteksten er entydig: `Oppdragsnr.: 52308329 Dokumentnr.: SHA-01
Versjon: 01` — en sidefot i en SHA-plan — og `Dokumentnavn: SHA-10 - … Restrisikorapport`.
**Dokumentnumre, ikke kostlinjer.**
* **N100** (10 569 tegn) → **0** kodeformede, men **17 forekomster / 8 distinkte** kravnumre på
`Krav X.Y.Z—N`-form (em-dash U+2014).
Modellen ble bedt om å gjengi en kostlinje som ikke finnes, og fabrikkerte den. Dette rimer med
funn 4 (økt 107): K2s prisskjema bærer ingen mengdefortegnelse, 0 av 92 rader navngir
kode/mengde/enhetspris.
## § 4 Tilbudet per korpus — målt med den SHIPPEDE funksjonen
Teksten er komponert nøyaktig som `run_project` komponerer P7s grunnlag
(`"\n".join([context, bundle_grounding])`, deretter gjennom `generate._grounding_text`).
«Kostlinje» er po sin EGEN definisjon (`okf.derive_cost_baseline`, MAJOR-4) — ikke en ny
heuristikk oppfunnet her.
| Korpus | Konsepter | Grunnlag (tegn) | Distinkte identifikatorer | Kostlinjer | `derive_cost_baseline` |
|---|---|---|---|---|---|
| K2 s7c + `payload-open` | 630 | 1 991 597 | 50 | **0** | REFUSED |
| K2 s7c + `payload-default` | 630 | 1 970 415 | 50 | **0** | REFUSED |
| K2 s7a2 (peker-armen, P6) | 629 | 1 894 500 | 50 | **0** | REFUSED |
| N100 `n100-2023` | 446 | 462 041 | 435 | **0** | REFUSED |
| N200 `n200-2024` | 1 133 | 1 500 962 | 982 | **0** | REFUSED |
| N500 `n500-2024` | 270 | 408 220 | 272 | **0** | REFUSED |
| `shared/veglys-fv-soer` | 5 | 30 038 | 3 | **1** | REFUSED (men fila finnes) |
| `shared/tunnel-hauglia` | 5 | 36 546 | 2 | **1** | REFUSED (men fila finnes) |
| `shared/bygg-energi-mikro` | 4 | 11 019 | 2 | **0** | REFUSED |
**Klassene, målt.** K2s 50 distinkte kodeformede er tegningsnumre (`B-20-00-00`, `F-20-00-01`,
`V-73-20-01-01`), dokumentnumre (`SHA-01`, `RIM-02`, `RIA-01`, `NOT-01`), stoff- og
standardreferanser (`PCB-7`, `PAH-16`, `DALI-2`) og forskriftsnumre
(`FOR-2011-12-06-1357`). **Ingen av dem er en kostkode.** N-korpusene bærer kravnumre og UUID-er
(N200: 2 290 distinkte UUID-er), ingen kostkoder.
**Er et forankret forslag i det hele tatt MULIG?** For **K2: NEI**, og tallet er 0 —
`derive_cost_baseline` nekter basen, og ingen av de 50 identifikatorene er en kostlinje. Bare
**2 av 50** når i det hele tatt prompten. For **N-korpusene: NEI** på kostlinje — en vegnormal
bærer ingen — men **JA** på identifikator: 435 / 982 / 272 distinkte kravnumre er sitérbare, og
8 av dem sto i N100-prompten.
**Rene tall er farligst og bæres videre uendret:** 46 394 forekomster / 2 117 distinkte i K2
(P7 § 2). Regelen er inert mot dem og feiler ÅPENT. Ikke bygget om på.
**Gammel default.** Alt over er målt på `K2-bundle-20260903`. okf melder (innboks
`20260909T134141Z`, `reply-expected: no`, lukket med `coord-done`) at deres default-bygg sluttet å
emittere den 2026-09-08, at gjeldende default er 436 konsepter / 832 filer, og at de leser
formfunnet som fortsatt stående fordi endringen treffer FORMEN, ikke innholdet. Ingen ny bundle er
bygget eller konsumert her — deres endring er ikke pushet, og en re-måling på den nye defaulten er
en senere, separat ordre.
## § 5 Hva som er bygget — en RAPPORT, ikke en gate
`generate.GroundingOffer(chars, identifiers, cost_lines)` + `generate.grounding_offer(...)`,
kalt fra `run.py`, rendret av `run.grounding_offer_notice`.
**Valget av kallsted, med grunnen (ordrens eget krav).** Begge kandidater ble lest først.
`generate.py` komponerer grunnlaget **per forsøk, ETTER `await _fetch_parsed(messages)`** — en
rapport derfra kan først tale når ett forsøk allerede er betalt, altså nøyaktig det ordren ber
den om å komme foran. `run.py` binder begge halvdeler ved `run.py:1228`, **over
`--live-dry-run`-kuttet og før første `debate.run`**. Valgt: **`run.py`**. Tellefunksjonen bor
likevel i `generate.py`, ved siden av `_grounding_text` den måler — å skille dem ville gitt to
steder å bli uenige på.
**De fire kravene:**
1. **BLOKKERER ikke.** En kjøring med null tilbud kjører som før. Et blokkerende krav ER
`--require-cost-baseline`, som premiss (vii) fredet.
2. **Måler den EKSAKTE teksten.** `grounding_offer` komponerer GJENNOM `_grounding_text`
samme funksjon P7s gate bruker — og `run.py` binder `delivered` **én gang** og gir samme
variabel til både rapporten og `_evaluate`. To komposisjoner av én tekst er fri til å være
uenige (kø-(p)); her er de identiske ved konstruksjon.
3. **Når utfallet operatøren leser.** `RunResult.grounding_offer`, `DryRunReport.grounding_offer`
og én linje på stdout i begge CLI-armene.
4. **Gjenbruker P7s sømmer.** Ingen ny domstype ved siden av `Rejection`/`ValidatedProposal`.
`_ground_against_input` er **URØRT**.
**Identifikator-formene er TRANSKRIBERT fra målingen i § 4**, ikke valgt: `[A-ZÆØÅ]{1,8}[-_]\d…`
(K2s 50) og `Krav X.Y.Z—N` (N-korpusenes dominerende form). At et mønster er tillatt HER og ikke i
`_ground_against_input` er selve skillet mellom en rapport og en gate: en form rapporten ikke
kjenner er et token den unnlater å telle, altså en **under-telling** — aldri en falsk avvisning.
**Rene tall er BEVISST utelatt, med tallet** (46 394 / 2 117): å telle dem ville gjort hver rapport
positiv og målingen inert — repoets kardinalklasse, en gate som bare kan bli grønn.
Rendereren er **ÉN**, og den tier når kjøringen KAN forankre en kostlinje — omisjon, aldri en tom
rad (`cost_baseline_notice`s regel). Den bærer **begge tall**, fordi paret er diagnosen:
«0 kostlinjer» alene leses som en gjentakelse av `cost_baseline_notice`, «50 identifikatorer» alene
leses som gode nyheter.
## § 6 Kjent-positiv og kontroll
**Kjent-positiven (bindende):** N100s input bærer `Krav 3.3.1—13` (em-dash U+2014; P7 premiss
(vii): 6 av 6 prompter). Rapporten sier **positivt tilbud** der, og armen er paret med en kontroll
på at strengen faktisk STÅR i fixturen — en rapport som fant null fordi den lette etter ingenting
ville ellers bestått.
**Kontrollen på samme materiale:** K2s to genererings-prompter, der tilbudet er **null i begge
tall**. Uten den beviser en grønn kjent-positiv ingenting.
**Fixturvalget, uttalt.** Testene leser P7s egne SPORede fixturer under
`tests/fixtures/p7-grounding/`. De leverte KUTTENE (10 kB N100, 96 kB K2) er ikke sporet: å
committe verbatim anbudstekst og standardtekst inn i et repo som publiseres på `open/` er en
publiseringsbeslutning som tilhører operatøren, ikke denne ordren. 8-/435-distinkt-tallene står
derfor i § 4 som MÅLINGER; det testene asserterer er EGENSKAPEN, på tekst repoet allerede
shipper.
## § 7 Mutasjonstabellen
Grønn kontroll **1570 passed / 5 skipped** (fra 1558/5; **+12 node-ider, 0 fjernet**, målt med
`comm` mot en liste bygget fra HEAD). Golden `demo-transcript.stdout` BYTE-UENDRET,
`shasum -a 1` av INNHOLDET = `ea8c534773acdbe41ae68f2c55724d69aaf8be4f` (ALDRI git-blob-id-en).
Hver mutasjon kjørt mot HELE suiten, én per kall, restaurert fra `scratchpad/p8/mut-backup/`
med `shasum -c`, aldri `git checkout`.
| # | Mutasjon | Røde |
|---|---|---|
| M1 | rapporten teller alltid null | **2** (kjent-positiven blant dem) |
| M2 | rapporten teller alltid positivt | **7** (K2-kontrollen først) |
| M3 | rapporten bygges fra `delivered` alene, ikke gjennom `_grounding_text` | **1** (krav (2)-armen alene) |
| M4 | rapporten når aldri `RunResult` | **3** |
| M5 | rapporten når aldri `DryRunReport` | **3** |
| M6 | rendereren skriver alltid linja | **2** (omisjonen er selv gatet, på begge flater) |
| M7 | detach dry-run-utskriften | **1** |
| M8 | detach fullkjørings-utskriften | **1** |
| M9 | rene tall telles som tilbud | **2** |
Ni mutasjoner, **alle røde**. Ingen grønn mutasjon, altså ingen uvitnet søm i denne leveransen.
## § 8 Honesty limits
* **Alt er målt OFFLINE** på tre opptak fra **ÉN modell** og **ETT deployment**. Ingen betalt
kjøring bekrefter at rapporten endrer noe levende, og at en modell som ser linja velger
annerledes er **ikke vist** (structured-output-grensens klasse).
* **Rapporten BLOKKERER ikke.** En kjøring med null tilbud kan fortsatt brenne tre forsøk på et
forslag som ikke kan bli forankret. Det er **VALGT**, ikke oversett: et blokkerende krav er
`--require-cost-baseline`, og F4/D-3 la den beslutningen hos operatøren.
* **Identifikator-formene er et mønster**, og et mønster kan mangle en form. Retningen er
under-telling, aldri falsk avvisning — men et korpus med en tredje form vil rapportere lavere
enn det tilbyr, til noen måler den formen.
* **`cost_lines` er `len(baseline.items)`**, lest av SAMME `baseline`-binding `_grounding_text`
tar som sin tredje kilde. Den gjentar altså forankringen som et ANTALL. Den står her fordi
paret er diagnosen, ikke fordi antallet er en ny kjensgjerning.
* **Portefølje-armen er BEVISST ikke wiret.** `run_portfolio` skriver ingen slik linje;
`bundle_id_notice`s avgjørelse, ikke `cost_baseline_notice`s. Asymmetrien står her fordi
stillhet om den er det eneste gale svaret.
* **Den hostede flaten er urørt.** Feltet er i ingen av hostings tre sett, så Fase 4es to
halvdeler står uendret.
* **Tilbudsmålingen er gjort på okf sin GAMLE default-bundle** (`K2-bundle-20260903`). En
re-måling på den nye (436 konsepter / 832 filer) er en senere, separat ordre.
* **(A) prosa-skanningen og (B) blindsone-valget forblir OPERATØRENS.** Målingen her informerer
(B) — den handler om **TILBUDET** i inputen, ikke om hvilket utdrag modellen **VELGER** — men
den avgjør den ikke.
## § 9 Reproduksjon
uv run pytest -q tests/test_grounding_offer_loadbearing.py
uv run python -m portfolio_optimiser.run BYGG-KONTOR-NORD \
--docs-dir shared/examples/bygg-energi-mikro \
--bundle-dir shared/examples/bygg-energi-mikro --live-dry-run
uv run python -m portfolio_optimiser.run VEGLYS-FV-SOER \
--docs-dir shared/examples/veglys-fv-soer \
--bundle-dir shared/examples/veglys-fv-soer --live-dry-run
Den første basen har ingen kostbaseline og skriver tilbudslinja; den andre har én og tier.
Måleskriptene ligger i `scratchpad/p8/` (utracket).