docs(s7b): forslaget ble ALDRI lest fra validator-input.json - symptomet stemmer, diagnosen ikke
Ordrens fase 1: mal hvor SavingsProposal-feltene kommer fra i dag, og hvilke som kan avledes deterministisk fra mandatet, den deriverte baselinen eller den rutede tilnaermingen. PREMISSET ER FELT FOER NOE BLE BYGGET PAA DET. Symptomet er bekreftet - en base uten validator-input.json nekter med FileNotFoundError foer foerste modellkall. Men forslaget har aldri blitt lest fra den fila: SavingsProposal konstrueres av generate._parse_ir fra MODELLENS svar. Fila er en fasit ved siden av kjoerestien. Det som blokkerer er at den er en paakrevd inngangsbetingelse for prosjekt- IDENTITETEN. Skillet avgjoer at S7b er TO soemmer, ikke en. NEVNEREN: tre kallsteder leser IR-projeksjonen, ikke ett - run.py:453 (hver bundle-kjoering), verdicts.py:507 (kun naar storen er ikke-tom) og run.py:1662, dispatcherens rutingsnoekkel, som bevisst ikke tar project_id fordi den leser den derfra. En ingestert base kan derfor ikke rutes i det hele tatt. MAALT som IKKE i veien: derive_cost_baseline trenger ingen validator-input.json (3 linjer ut av MAJOR-4-fixturen, 2 ut av en syntetisk base uten fila; det uprisede skjemaet nekter fortsatt i sin helhet). Forankringen er paa plass; det som mangler er en kandidat aa forankre. TABELLEN tvinger fram tre ting fase 2 maa avgjoere: anslaget og kostkodene finnes IKKE paa Approach eller Mandate i dag (maalt: fire hhv. fire felt, null tallfelt); measure er ikke bare prosa men METHOD_CAPS-oppslagsnoekkelen, saa en label treffer aldri metode-cap-en; og tom assumptions gjoer Monte Carlo inert (P10 == P50 == P90) mens persentiler fortsatt printes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4e7efd7d1f
commit
19badcd0fe
1 changed files with 154 additions and 0 deletions
154
docs/2026-09-03-forslag-fra-mandat.md
Normal file
154
docs/2026-09-03-forslag-fra-mandat.md
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
# Hvor kommer forslaget fra? — måling før S7b
|
||||||
|
|
||||||
|
**Dato:** 2026-09-03 · **Ordre:** `20260903T014107Z-72282048-from-.claude` (S7b-forberedelse), fase 1
|
||||||
|
**Status:** måling. Ingen kode endret av dette dokumentet.
|
||||||
|
|
||||||
|
Bruksscenarioet ordren stiller opp er «dokumenter + konkret oppgave → forslag». Ordren beskriver
|
||||||
|
hindringen slik: *forslaget leses fra en håndskrevet `validator-input.json`*. Dette dokumentet måler
|
||||||
|
den påstanden mot koden før noe bygges på den, og legger fram tabellen ordren ber om: hvert felt i
|
||||||
|
`SavingsProposal` → hvor det kommer fra i dag → om det kan avledes deterministisk fra mandatet, den
|
||||||
|
deriverte kostbaselinen eller den rutede tilnærmingen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Premisset er felt: symptomet stemmer, diagnosen ikke
|
||||||
|
|
||||||
|
Ordrens symptom er **bekreftet**. En kunnskapsbase uten `validator-input.json` nekter, og nekten kommer
|
||||||
|
før første modellkall:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ run_project('P1', docs_dir=<base>, bundle_dir=<base>, live_dry_run=True)
|
||||||
|
FileNotFoundError: IR projection not found in bundle: 'validator-input.json'
|
||||||
|
```
|
||||||
|
|
||||||
|
Diagnosen stemmer derimot ikke. **Forslaget har aldri blitt lest fra den fila.** Målt i kilden:
|
||||||
|
|
||||||
|
* `SavingsProposal` konstrueres av `generate._parse_ir` fra **modellens svar**
|
||||||
|
(`generate.py`, `generate_via_llm` → `_fetch_parsed` → `_parse_ir`). Ingen kodesti leser et forslag
|
||||||
|
fra `validator-input.json`.
|
||||||
|
* `run._project_from_bundle` (`run.py:444`) leser IR-projeksjonen til nøyaktig **to** ting: en
|
||||||
|
fail-fast av kjøringens `project_id` mot basens, og et fallback for prosjektnavnet — navnet hentes
|
||||||
|
primært fra `type: project`-konseptets `title`-frontmatter, og IR-en brukes ikke til det i det hele
|
||||||
|
tatt.
|
||||||
|
|
||||||
|
At `validator-input.json` er en komplett håndskrevet `SavingsProposal` (den er det — se
|
||||||
|
`shared/examples/tunnel-hauglia/validator-input.json`) gjør den til en **fasit ved siden av**
|
||||||
|
kjørestien, ikke til kjørestiens inngang. Det som faktisk blokkerer, er at fila er en **påkrevd
|
||||||
|
inngangsbetingelse for prosjekt-identiteten**.
|
||||||
|
|
||||||
|
Skillet er ikke pedanteri: det avgjør at S7b er **to sømmer, ikke én**.
|
||||||
|
|
||||||
|
| | Hva som er i veien | Hva som må til |
|
||||||
|
|---|---|---|
|
||||||
|
| **Søm 1 — prosjekt-identitet** | `load_ir_projection` er fail-fast påkrevd på tre kallsteder | fravær tolereres; en fil som FINNES nekter fortsatt ved avvik |
|
||||||
|
| **Søm 2 — kandidat-kilde** | den eneste kandidat-kilden er `generate_via_llm` | en deterministisk kilde ved siden av, eksplisitt bestilt |
|
||||||
|
|
||||||
|
### 1.1 Nevneren for søm 1: tre kallsteder, ikke ett
|
||||||
|
|
||||||
|
```
|
||||||
|
src/portfolio_optimiser/run.py:453 _project_from_bundle (prosjekt + fail-fast)
|
||||||
|
src/portfolio_optimiser/run.py:1662 run_mandate_across_bundles (dispatcherens rutingsnøkkel)
|
||||||
|
src/portfolio_optimiser/verdicts.py:507 bundle_candidate_features (ExpeL-nøkkelen)
|
||||||
|
```
|
||||||
|
|
||||||
|
Alle tre er målt til å reise samme `FileNotFoundError` mot en base uten fila. De er ulike i når de
|
||||||
|
fyrer:
|
||||||
|
|
||||||
|
* **`run.py:453`** fyrer på hver bundle-kjøring — dette er nekten ordren observerte.
|
||||||
|
* **`verdicts.py:507`** fyrer bare når storen er ikke-tom (Steg-1-folden er gated på det), altså først
|
||||||
|
når basen har lært noe. En kjøring uten dommer merker den ikke.
|
||||||
|
* **`run.py:1662`** er den som betyr mest for S7b: multi-base-dispatchen tar bevisst **ingen**
|
||||||
|
`project_id`-parameter, fordi hver bases prosjekt leses fra DEN basens egen IR-projeksjon
|
||||||
|
(CLAUDE.md, multi-base-raden). En ingestert base uten fila kan derfor ikke rutes i det hele tatt —
|
||||||
|
og etter S7a-3 finnes det en åpenbar erstatning: basens **erklærte `bundle_id`**.
|
||||||
|
|
||||||
|
### 1.2 Det som IKKE er i veien
|
||||||
|
|
||||||
|
`derive_cost_baseline` trenger ingen `validator-input.json`. Målt mot den samme base som nektet over,
|
||||||
|
og mot MAJOR-4-fixturparet:
|
||||||
|
|
||||||
|
| base | utfall |
|
||||||
|
|---|---|
|
||||||
|
| syntetisk to-linjers base (uten IR-projeksjon) | `A1: 100 × 50`, `A2: 20 × 250` |
|
||||||
|
| `tests/fixtures/k2-prisskjema-SYNTETISK` | `21.1: 1250 × 850`, `24.2: 48000 × 42.5`, `36.1: 4 × 187500` |
|
||||||
|
| `tests/fixtures/k2-prisskjema-uprisert-SYNTETISK` | `CostBaselineDerivationError` (uprisert skjema nekter i sin helhet) |
|
||||||
|
|
||||||
|
Forankringen er altså allerede på plass for et ingestert korpus. Det som mangler, er en kandidat å
|
||||||
|
forankre.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Tabellen: felt → kilde → skjønn
|
||||||
|
|
||||||
|
`SavingsProposal` har fem felt (målt: `['project_id', 'measure', 'affected_items',
|
||||||
|
'claimed_saving_nok', 'assumptions']`).
|
||||||
|
|
||||||
|
| felt | hvor det kommer fra i dag | (a) mandatet | (b) `derive_cost_baseline` | (c) rutet approach | skjønn? |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `project_id` | modellens svar, med `project.id` som default (`_parse_ir`) | — | `CostBaseline.project_id` — men den er et **påkrevd argument**, ikke en avledning | — | **NEI** — kjøringen vet allerede hvilket prosjekt den kjører |
|
||||||
|
| `measure` | modellens prosa | — | — | `Approach.label` (ev. + `description`), VERBATIM | **NEI** — men se § 2.1 |
|
||||||
|
| `affected_items` | modellen velger både koder og tall | — | `items[code] → (quantity, unit_cost)` for hver kode | approachen må **navngi kodene** | **JA** hvis kodene ikke er navngitt → må nekte, ikke gjette |
|
||||||
|
| `claimed_saving_nok` | modellens tall | **ingen plass i dag** | kan ikke avledes — baselinen er en KOSTNAD, ikke en besparelse | **ingen plass i dag** | **JA** → må komme fra eksperten, eller nekt ved navn |
|
||||||
|
| `assumptions` | modellens bånd | — | kan ikke avledes — én pris, ikke et bånd | valgfritt eksplisitt | **JA** → tomt er lovlig, men se § 2.2 |
|
||||||
|
|
||||||
|
Målt: `Approach` bærer `['id', 'label', 'description', 'bundle_id']` og `Mandate` bærer
|
||||||
|
`['objective', 'approaches', 'allow_own_proposals', 'success_criteria']`. **Ingen av dem har et
|
||||||
|
tallfelt.** De to feltene tabellen krever av eksperten — anslaget og kostkodene — finnes altså ikke,
|
||||||
|
og må legges til i `mandate.py`.
|
||||||
|
|
||||||
|
### 2.1 `measure` er ikke bare prosa — den er en oppslagsnøkkel
|
||||||
|
|
||||||
|
`validate_proposal` stage 5 slår opp `METHOD_CAPS[proposal.measure]` (`validator.py:248`). Registeret
|
||||||
|
har i dag én oppføring, `energy_efficiency` → 0,15. En `measure` satt til ekspertens `label` treffer
|
||||||
|
den derfor **aldri**, med mindre labelen ordrett er `energy_efficiency`.
|
||||||
|
|
||||||
|
Det er ikke en defekt, men det er en grense som må sies: en mandat-avledet kandidat dømmes av den
|
||||||
|
generiske 30 %-cap-en (`MAX_SAVING_FRACTION`), ikke av en metode-spesifikk. Metode-cap-en er
|
||||||
|
opt-in-data (F8), og en mandat-vei som skal treffe den, må la eksperten navngi metoden — det er en
|
||||||
|
egen beslutning, ikke noe denne veien kan avlede.
|
||||||
|
|
||||||
|
### 2.2 Tom `assumptions` gjør den stokastiske falsifisereren inert
|
||||||
|
|
||||||
|
`validator._monte_carlo` faller tilbake på `item.unit_cost` for hver kode uten bånd. Uten bånd i det
|
||||||
|
hele tatt er alle samples identiske og **P10 == P50 == P90**. Det er nøyaktig faren
|
||||||
|
`generate._ASSUMPTIONS_WIRE_NODE` ble skrevet for å unngå på LLM-stien.
|
||||||
|
|
||||||
|
Konsekvensen for en mandat-avledet kandidat: stage 3 rapporterer fortsatt persentiler, men de bærer
|
||||||
|
ingen usikkerhet. Kandidaten dømmes reelt av stage 0 (avstemming mot baselinen), stage 2/4b (CBC-solve
|
||||||
|
mot 30 %-cap-en) og pydantics `claimed ≤ total`. **Dette skal stå som en uttalt ærlighets-grense**, ikke
|
||||||
|
skjules bak at persentiler blir printet.
|
||||||
|
|
||||||
|
### 2.3 Hva som gjør en mandat-kandidat ikke-triviell å gjøre grønn
|
||||||
|
|
||||||
|
Kopieres `affected_items` fra den deriverte baselinen, avstemmer stage 0 med 0 % avvik ved
|
||||||
|
konstruksjon. Det er verdt å si høyt, fordi det betyr at en test som bare sjekker «validatoren kjørte»
|
||||||
|
er grønn ved konstruksjon — repoets vakuøs-gate-klasse.
|
||||||
|
|
||||||
|
Den levende beskrankningen er anslaget: `MAX_SAVING_FRACTION = 0.30`. Et anslag over 30 % av de
|
||||||
|
berørte linjenes total blir **avvist av den ordinære gaten**, uten at kandidat-veien vet noe om det.
|
||||||
|
Det er den skarpe diskriminatoren en test må bruke for å vise at kandidaten går *gjennom*
|
||||||
|
`validate_proposal` og ikke rundt den.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Hva som følger av dette
|
||||||
|
|
||||||
|
Fase 2 av ordren bygger på tre beslutninger tabellen tvinger fram:
|
||||||
|
|
||||||
|
1. **Anslaget og kostkodene bor på `Approach`, ikke på `Mandate`.** `mandate.settle` sier selv at
|
||||||
|
tilnærminger er ALTERNATIVER og aldri summeres, så ett tall på mandatnivå ville vært tvetydig over
|
||||||
|
N tilnærminger. `Mandate`-docstringen nekter å bære *målet* (det eier `contracts.GoalContract`) —
|
||||||
|
et per-kandidat-anslag er en annen ting enn et mål for kjøringen, og skillet må skrives ned der
|
||||||
|
feltet innføres, ellers leses det som nøyaktig den driften modulen forbyr.
|
||||||
|
2. **Hver ny nekt subklasser `ValueError`** — `MandateRoutingError`/`BundleIdMismatch`/
|
||||||
|
`CostBaselineDerivationError`-presedensen — så den lander på CLI-ens nekt-tuppel og hostings
|
||||||
|
400-arm, aldri krasj-kanalen.
|
||||||
|
3. **`allow_own_proposals` har ingen deterministisk motpart.** En «egen» kandidat krever en modell. Den
|
||||||
|
raden kan derfor ikke fylles, og skal rapporteres som `not_evaluated` med en grunn ved navn —
|
||||||
|
`ApproachOutcome`s egen regel: en rad som utelates er umulig å skille fra en tilnærming ingen
|
||||||
|
bestilte. Å nekte hele kjøringen ville vært feil, siden feltet defaulter til `True` og hvert
|
||||||
|
eksisterende mandat bærer det.
|
||||||
|
|
||||||
|
**Ærlighets-grenser i denne målingen:** ingen levende K2-fil er lest (fixturene er syntetiske, som i
|
||||||
|
MAJOR-4); `run.py:1662`-konsekvensen for dispatchen er lest i kilden, ikke kjørt mot en base uten
|
||||||
|
IR-projeksjon; og tabellen beskriver hva som KAN avledes, ikke at en ekspert vil fylle feltene godt.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue