portfolio-optimiser/docs/2026-08-14-fase1b-forste-levende-kjoring.md
Kjell Tore Guttormsen 241b50d6c4 docs(1b): koordinatene byttet mot plassholdere - vei A, redigert framover
Operatørbeslutning 2026-08-18: vei A. Ressurs-, prosjekt- og vertsnavnene i
måleprotokollen fra første levende kjøring byttes mot repoets eksisterende
plassholder-form (<resource-group> / <resource> / <project>, samme som
DEPLOY.md og auth-oppskriften). Vei B (omskrive historikken) er avvist:
koordinatene nådde alt en tredjepart via 14:24-bygget, så B ville kostet en
force-push for en gevinst vi ikke kan bekrefte.

MÅLT, ikke antatt:
- 5 linjer bar koordinatene (10-12, 14, 172), ikke de 10 STATE påsto. Linje
  164-166 bærer kun `gpt-4-1-mini` - offentlig Azure-nomenklatur, står.
- Ingen gate pinner strengene: 0 treff i tests/ + src/ + scripts/, kjent-positiv
  kontroll samme kommandoform ga 5. En prosa-redaksjon kan ikke brekke en guard.
- Klasse 2 etter redaksjonen: 0 treff over hele treet (nevner 327 sporede filer),
  tre spørringsklasser hver med kjent-positiv kontroll som fyrte. De gjenværende
  AI-Services-vertene er testdummies (x./platform./injected), og GUID-en
  53ca6127-db72-4b80-b1b0-d745d6d5456d er Azures OFFENTLIGE built-in
  role definition id for Foundry User (verifisert mot Microsoft Learn), lik i
  hver tenant - ikke en koordinat.
- Regresjonsgaten: 869 passed / 5 skipped, likt referansen.

Dokumentets verdi står: dette er en måleprotokoll, og poenget er hva som ble
målt - ikke hvilken ressursgruppe det skjedde i. En linje under tabellen sier
høyt at navnene er plassholdert, så ingen leser tror <resource> var det
literale navnet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WVTZgMF94VZtNa7nGVtupF
2026-08-18 13:03:11 +02:00

247 lines
13 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.

# Fase 1b — den første levende kjøringen (14. august 2026)
Dette dokumentet er et **måleprotokoll**, ikke en konklusjon. Alt under er kjørt; ingenting er utledet.
Der noe ikke er bevist, står det.
## 0. Hva som ble opprettet
| Ressurs | Verdi |
|---|---|
| Ressursgruppe | `<resource-group>` (eastus) |
| Foundry-ressurs | `<resource>``kind: AIServices`, `--allow-project-management` |
| Prosjekt | `<project>` |
| Deployment | `gpt-4-1-mini` (modell `gpt-4.1-mini`, versjon `2025-04-14`, GlobalStandard) |
| Prosjekt-endepunkt | `https://<resource>.services.ai.azure.com/api/projects/<project>` |
| Rolle | `Foundry User` (`53ca6127-…`) på **prosjekt**-scope |
Ressurs-, prosjekt- og vertsnavnene er byttet mot plassholdere (`<resource-group>` / `<resource>` /
`<project>`, samme form som `DEPLOY.md` og auth-oppskriften). Modellnavn, versjon og region er
offentlig Azure-nomenklatur og står som målt.
**`gpt-4o-mini` ble avvist av plattformen** med `ServiceModelDeprecating` — modellnavnet måtte måles
med `az cognitiveservices model list`, ikke hentes fra hukommelsen. Kommandoene er hentet fra
Microsoft Learn (`az cognitiveservices account create` / `account project create`), ikke formulert
fritt.
**Rotårsaken til at dette ikke fantes før** var prosedural, ikke teknisk: den påloggede identiteten
har vært **Owner på abonnementet** hele tiden, mens repoets egen state-fil hadde ført opprettelsen
opp som operatørens oppgave og samtidig sagt at operatøren aldri kjører kommandoer selv. Arbeidet
tilhørte ingen, og hver økt målte lydig på nytt at det ikke fantes.
## 1. Stigen — billigste trinn først
Disiplinen fra fase 1a: bevis så mye som mulig før det dyre trinnet, så en feil er attribuerbar.
| Trinn | Kommando | Utfall |
|---|---|---|
| 1 | `az account get-access-token --resource https://ai.azure.com` | exit 0 |
| 2 | `preflight --profile azure` (fra **utpakket overleveringspakke**) | `preflight OK (azure)` |
| 3 | `pytest tests/test_foundry_profile_live.py` (gatet triviell probe) | **1 passed, 5,02 s** |
| 4 | Full `run_project` mot levende modell | **RC=1 — `BudgetExceeded`** |
**Trinn 3 er prosjektets første levende modellkall noensinne.** Det beviser at auth, RBAC,
endepunkt-form og deployment-navn komponerer — og at en rød trinn 4 derfor *ikke* kan skyldes noen
av dem.
## 2. Trinn 4 — hva som faktisk skjedde
```
uv run python -m portfolio_optimiser.run BYGG-KONTOR-NORD \
--profile azure \
--docs-dir shared/examples/bygg-energi-mikro \
--bundle-dir shared/examples/bygg-energi-mikro \
--outbox-dir <tmp>/outbox --run-id live-001
```
stdout var **tom**. stderr bar to linjer som betyr noe:
```
GroupChatOrchestrator reached max_rounds=3; forcing completion.
portfolio_optimiser.budget.BudgetExceeded: budget exceeded: rounds limit=12 observed=13
```
Den første er forventet — maker/checker-debatten kjører til taket også i den offline demoen. Den
andre er funnet.
### Diagnosen, utledet av kode og ikke av flere betalte kjøringer
`generate._fetch_parsed` er en `while True` som kaller `meter.tick_round()` for **hvert** forsøk og
`continue`-er ved parse-feil. Rund-budsjettet i genereringsfasen er `max(max_rounds * 4, 4)` = **12**
(`run.py:546`). Hadde modellsvarene parset til IR-formen, ville tre validerings-forsøk kostet tre
runder. At alle tolv gikk med betyr at **de fleste svarene fra `gpt-4.1-mini` ikke lot seg parse**
taket ble brent på formatfeil, ikke på validator-avslag.
**Dette er et FUNN, ikke en diagnose som er ferdig.** Det som mangler for å lukke den, er den råe
svarteksten, og den finnes ikke i noen artefakt i dag (`--outbox-dir` skrev kun
`live-001-runconfig.json`). Å skaffe den krever en endring i koden, altså Iron-Law-arbeid — ikke en
rask omkjøring med høyere tak, som ville kostet penger og fortsatt ikke sagt hvorfor.
### En bieffekt som er verdt å uttale
`BudgetExceeded` forlot kjøringen som en **uhåndtert exception med traceback**, ikke som et
strukturert utfall. På den hostede flaten ville nøyaktig dette blitt `HTTP 500` — altså ville
ressurs-utmattelse (en normal, forventet tilstand) presentert seg for en ekstern kaller som en
serverfeil. Det er ikke rettet her; det er notert.
## 3. Hva som ER bevist, og hva som IKKE er det
**Bevist, målt:**
- Rammeverket når en levende Foundry-modell: auth, RBAC på prosjekt-scope, endepunkt-form,
deployment-oppslag og klientbygging virker.
- Kunnskapsbase-navigasjon, debatt-orkestrering og genererings-løkka kjører mot ekte modellsvar —
kjøringen døde *inne i* løkka, ikke før den.
- Preflight fra den **utpakkede overleveringspakka** er grønn mot et ekte prosjekt, både med vårt
eget endepunkt-variabelnavn og med plattformens injiserte.
**Ikke bevist:**
- At systemet produserer et **validert forslag** mot en levende modell. Det har det aldri gjort.
Kjøringen nådde aldri fram til validatoren med en parsebar kandidat.
- At `gpt-4.1-mini` er en egnet modell for proposer-rollen. Målingen peker mot at den ikke er det
uten endret prompting eller structured output — men én kjøring er én kjøring.
- Noe som helst om kostnad i drift. Denne kjøringen kostet noen få øre; det sier ingenting om en
reell portefølje.
## 4. Neste steg, i rekkefølge
1. **Fang den råe svarteksten** ved parse-feil (i dag forsvinner den i `except: continue`). Test
først — dette er en søm, ikke en logg-linje.
2. Vurder **structured output** mot Foundry for proposer-rollen, framfor å prompte fram JSON.
3. Vurder om `BudgetExceeded` skal bli et strukturert utfall i stedet for en traceback, særlig for
den hostede flaten.
4. Først når 1 og 2 er på plass: en ny bundet kjøring, og en gatet test som dekker **hele
`run_project`-stien** (den eksisterende gatede testen dekker kun klient-nivået).
**Rettelse til punkt 4, målt 14.08 (økt 40):** parentesen er upresis. `tests/test_portfolio_live.py`
fantes allerede og dekker `run_portfolio`-utfoldingen — altså hele `run_project`-stien — med samme
env-gate. Men den asserterer `len(result.runs) == 1`, og fordi `runs` og `failures` *partisjonerer*,
kan den kun skille «kjøringen fullførte» fra «kjøringen raiste». Den kan **ikke** skille et validert
forslag fra et avvist, og heller ikke en kjøring der svarene parset fra en der de ikke gjorde det.
Den bærer derfor ikke påstanden 1b skal felle. Punkt 4 står, men grunnen er en annen enn skrevet.
## 5. Pre-registrerte utfall (skrevet FØR kjøringen)
Denne kjøringen har ett formål: å felle den ene gjenstående ærlighets-grensen fra økt 37 — **at det
emitterte `response_format`-skjemaet ER akseptert av det LEVENDE endepunktet er uverifisert**;
testene beviser konformitet med Azures *dokumenterte* subset, ikke aksept.
**Instrumentet** er `conftest.assert_full_run_contract`, og diskriminatoren er et artefakt repoet
allerede eier: `{run_id}-parse-failures.json` skrives hvis og bare hvis et svar ikke lot seg parse
(økt 35). Artefaktets **fravær** ved siden av et `RunResult` beviser at hvert genererings-svar kom
tilbake i den bestilte formen. Kontraktens evne til å skille er bevist **offline og gratis**
(`tests/test_live_full_run_contract.py`, to mutasjoner med hver sin signatur: detach artefakt-sjekken
→ T1 rød alene; raise ubetinget → T2 rød alene). Det betalte kallet er *målingen*, ikke beviset på at
måleinstrumentet virker.
**Hva hvert utfall betyr — avgjort på forhånd:**
| Utfall | Betydning |
|---|---|
| Ingen parse-failure-artefakt + validatoren avgjorde (`validated` **eller** `rejected`) | **Ærlighets-grensen er felt.** Skjemaet ble akseptert av det levende endepunktet. En P90-avvisning er et *bestått* utfall — kjøringen KONKLUDERTE. |
| Parse-failure-artefaktet finnes | Skjemaet ble **ikke** honorert. Et FUNN, ikke et bevis — og denne gangen finnes den råe teksten (økt 35), så neste steg kan begrunnes i stedet for gjettes. |
| `BudgetExceeded` | Fortsatt ubevist, men artefaktet forklarer hvorfor. Samme form som 1a-kjøringen. |
**Taket heves IKKE.** `max_rounds`/`max_tokens` står på defaultene den første levende kjøringen døde
på: fyrer ledgeren igjen, er dét informasjon, og å heve taket ville brukt mer penger på en sti som
kanskje fortsatt er brukket.
**Gate-variabelen er en TREDJE, distinkt opt-in** (`PORTFOLIO_LIVE_FULL_RUN`, lest på *truthiness*).
Begge de eksisterende live-testene gater på nøyaktig `PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT` +
`PORTFOLIO_FOUNDRY_DEPLOYMENT`; å gjenbruke det paret ville betydd at en operatør som eksporterer de
to for å kjøre den **billige** ett-ords-proben også fyrer den dyre fullkjøringen — altså at stigen i
§1 kollapser til ett trinn. `PORTFOLIO_MODEL_MAP` er med i skip-betingelsen av en annen grunn:
uten den feiler kjøringen av en *konfigurasjons*-årsak som ser ut som en modell-feil.
## 6. Den bundne fullkjøringen — målt 14.08 kl. 19:54
### Oppsettet, reproduserbart
Model-mappet ligger **out-of-tree** med vilje: `data/model_map.json` i treet bærer
`REPLACE-WITH-FOUNDRY-DEPLOYMENT`-plassholdere, og tenant-spesifikke deployment-navn skal aldri
committes (B12). `PORTFOLIO_MODEL_MAP` peker på en fil operatøren eier:
```json
{
"local": {"default": "qwen3:4b", "proposer": "qwen3:4b", "checker": "qwen3:4b"},
"azure": {
"default": "gpt-4-1-mini",
"proposer": "gpt-4-1-mini",
"checker": "gpt-4-1-mini"
}
}
```
```bash
export PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT="https://<resource>.services.ai.azure.com/api/projects/<project>"
export PORTFOLIO_FOUNDRY_DEPLOYMENT="gpt-4-1-mini"
export PORTFOLIO_MODEL_MAP="/absolutt/sti/til/model_map.live.json"
export PORTFOLIO_LIVE_FULL_RUN=1 # BETALER — utelat den for alt annet enn trinn 4
```
Merk at `PORTFOLIO_LIVE_FULL_RUN` bevisst settes SIST og alene for det dyre trinnet: uten den er
trinn 2 og 3 gratis-nok til å kjøres fritt, og det er hele stigens poeng.
### Stigen
Stigen på nytt, samme disiplin som §1. Alt under er kjørt.
| Trinn | Kommando | Utfall |
|---|---|---|
| 1a | `az account get-access-token --resource https://ai.azure.com` (uten rør) | exit 0, `expiresOn 21:07:40` |
| 1b | `az ad signed-in-user show` (ekte Graph-kall, ikke lokal cache) | exit 0 — gmail-kontoen, ikke jobbkontoen |
| 2 | `preflight --profile azure` | `preflight OK (azure)` |
| 3 | `pytest tests/test_foundry_profile_live.py` | **1 passed, 5,48 s** |
| 3b | samme kall, med `test_full_run_live.py` samlet | **SKIPPED** — det tredje flagget holder stigen |
| 4 | `pytest tests/test_full_run_live.py` (`PORTFOLIO_LIVE_FULL_RUN=1`) | **1 passed, 22,87 s** |
Trinn 3b er verdt å uttale: den dyre testen hoppet over **selv med begge Foundry-variablene satt**.
Det er den empiriske bekreftelsen på at det tredje opt-in-flagget gjør jobben sitt design lover.
### Ærlighets-grensen ER felt
Outboksen inneholder `-proposal.json`, `-outcome.json`, `-runconfig.json` — og **ingen
`-parse-failures.json`**. Hvert eneste genererings-svar fra `gpt-4.1-mini` kom tilbake som et
parsebart objekt i den bestilte formen. Økt 37s uttalte grense — *«at det emitterte skjemaet ER
akseptert av det LEVENDE endepunktet er IKKE verifisert»* — er dermed **lukket ved måling**, ikke
ved resonnement. Kontrasten til den første levende kjøringen er hele funnet: der brant tolv runder
på formatfeil og etterlot null tegn; her feilet ingen.
`assumptions` kom tilbake som forventet (`{"EL-LIGHTING-OP-HR": [10.0, 13.0]}`), altså virker økt 37s
additive wire-form → IR-map-normalisering ende-til-ende mot et levende endepunkt. Det var
beslutningen som holdt den stokastiske falsifisereren fra å gå inert, og den er nå prøvd i felt.
### Utfallet: `rejected` — og det er et bestått utfall
```
outcome_type: rejected
reason: claimed saving 34500 exceeds P90 feasible 11488
checker_verdict: approve
validator_decision: rejected
token_usage: 15 306
```
Kjøringen KONKLUDERTE. Per §5s pre-registrering er dette det positive utfallet: validatoren ble nådd
med en parsebar kandidat og avgjorde. **Og de to falsifisererne skilte lag for første gang mot en
levende modell** — checkeren godkjente *resonnementet*, validatoren avviste *tallene*. Nøyaktig den
uavhengigheten `checker_verdict` holdes atskilt fra `provenance.validator_decision` for.
### FUNN som er viktigere enn den grønne testen: modellen fant opp en kostkode
Forslaget bar `code: "EL-LIGHTING-OP-HR"` (3 000 × 11,5). **Den koden finnes ikke noe sted i
kunnskapsbasen** (`grep` over hele bundelen: null treff). Kunnskapsbasen instruerer eksplisitt
mappingen `ENERGI-TOTAL-EL`, 300 000 kWh × 1,00 NOK — modellen konstruerte i stedet sin egen
kostlinje med en egen enhet.
Avvisningen var derfor **riktig, men skjedde på feil gate**: 30 %-cap-en fanget den på *magnitude*
(34 500 > P90 11 488), ikke stage 0 på *eksistens*. Grunnen er en kjent og uttalt egenskap, ikke en
defekt: S4.0-forankringen aktiveres på bundle-stien KUN når bundelen shipper `cost-baseline.json`,
og `bygg-energi-mikro` gjør ikke det (målt) — «en pre-amendment-bundle er legitimt uforankret».
Dette er akkurat den hallusinasjons-klassen S4.0 ble bygget for, observert i felt for første gang.
At den uforankrede gaten fanget den likevel er betryggende; at den fanget den på den dyre gaten
i stedet for den billige er en kø-post, ikke noe som endres her.
**Fortsatt ikke bevist:** at systemet produserer et *validert* forslag mot en levende modell. Denne
kjøringen avviste — korrekt, og med en begrunnelse som kan leses. Én kjøring er én kjøring, og
`gpt-4.1-mini`s egnethet for proposer-rollen er ikke avgjort av den.