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>
9.7 KiB
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:
SavingsProposalkonstrueres avgenerate._parse_irfra modellens svar (generate.py,generate_via_llm→_fetch_parsed→_parse_ir). Ingen kodesti leser et forslag fravalidator-input.json.run._project_from_bundle(run.py:444) leser IR-projeksjonen til nøyaktig to ting: en fail-fast av kjøringensproject_idmot basens, og et fallback for prosjektnavnet — navnet hentes primært fratype: project-konseptetstitle-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:453fyrer på hver bundle-kjøring — dette er nekten ordren observerte.verdicts.py:507fyrer 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:1662er den som betyr mest for S7b: multi-base-dispatchen tar bevisst ingenproject_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ærtebundle_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:
- Anslaget og kostkodene bor på
Approach, ikke påMandate.mandate.settlesier 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 eiercontracts.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. - Hver ny nekt subklasser
ValueError—MandateRoutingError/BundleIdMismatch/CostBaselineDerivationError-presedensen — så den lander på CLI-ens nekt-tuppel og hostings 400-arm, aldri krasj-kanalen. allow_own_proposalshar ingen deterministisk motpart. En «egen» kandidat krever en modell. Den raden kan derfor ikke fylles, og skal rapporteres somnot_evaluatedmed 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 tilTrueog 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.