portfolio-optimiser/docs/2026-09-03-forslag-fra-mandat.md
Kjell Tore Guttormsen 19badcd0fe 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>
2026-09-03 20:23:49 +02:00

9.2 KiB
Raw Blame History

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 ValueErrorMandateRoutingError/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 — ApproachOutcomes 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.