The context sets, the packaged knowledge bases and the example bundles are replaced by one fictitious example set about IT operations in an invented organisation: three context sets (serverrom-2027, driftsavtale-2027 and the two-base drift-og-avtale-2027), two synthetic knowledge bases under src/portfolio_optimiser/data/kunnskapsbaser and two example bundles under src/portfolio_optimiser/data/bundles. Numbers, codes and structural values in tests and fixtures are kept; names, ids and wording change. Dated measurement documents that only recorded runs on the replaced material are deleted. Gate figures measured on the new set are not comparable with earlier ones. The exclusion gate from the previous commit is green: 0 tracked files hit outside the shared/ subtree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
162 lines
9.7 KiB
Markdown
162 lines
9.7 KiB
Markdown
# 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
|
||
`src/portfolio_optimiser/data/bundles/driftssenter-kjoling/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.
|
||
|
||
### 3.1 Målt underveis, rapportert, IKKE fikset (utenfor ordren)
|
||
|
||
`--mandate` står **ikke** i `main()`s `report_forbidden`-partisjon. Report-modus returnerer FØR
|
||
kjøre-dispatchen, så `--report --ledger X --mandate Y` **dropper kommisjonen i stillhet** i stedet
|
||
for å nekte — nøyaktig den gap-klassen F4 målte på `--plan-review`, på et flagg som er eldre enn
|
||
denne ordren. `--proposals-from-mandate` er lagt inn i partisjonen (og gatet av M11); `--mandate`
|
||
selv er ikke rørt, fordi den er utenfor det ordren ba om.
|
||
|
||
**Æ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.
|