portfolio-optimiser/docs/rapport/2026-07-03-sammenligningsrapport.md

300 lines
20 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.

# Sammenligningsrapport — MAF og Claude Agent SDK på identisk delt kjerne (S11)
> **Hva dette er:** Programmets sluttleveranse (målbilde §1): en sammenligning av to uavhengige
> implementasjoner av samme metode — Microsoft Agent Framework-stacken (`portfolio-optimiser`) og
> Claude Agent SDK-søskenet (`portfolio-optimiser-claude`) — målt mot samme frosne protokoll
> ([sammenligningsprotokollen](../plan/2026-07-03-sammenligningsprotokoll.md), S3) på identisk
> delt kjerne (pinned commons-ref `db86e15`). Hver tallpåstand i rapporten har en rad i
> verifiseringsloggen (§9) med klasse `reproduce`, `recompute-from-artifact` eller
> `disclosed-non-reproducible`. Datert 2026-07-03.
## Hovedfunn
- **Begge stacker består ekvivalens-gaten (M1):** begge deterministiske validatorer reproduserer
golden-suitens besluttede felt på pinned ref, og Claude-sidens ene live-kjøring reproduserer
goldens persentiler bit-for-bit (§2). M1 er en gate begge må gjennom, ikke en differensierende
metrikk.
- **Begge stacker lukker læringssløyfa offline** (M2/M3, plumbing-bevis): MAF-siden via en kjørbar,
skriptet simulering (`LEARNING LOOP CLOSED`, exit 0), søskenet via testsuite-bevis
(187 tester grønne, deriblant to-runs- og promoterings-bevisene) (§3).
- **Kun Claude-siden har et live-bevis:** én API-kjøring (S10) validerte på første forsøk —
36 791 av 150 000 tokens, 2 av 12 runder, $0.127514. Den FØRSTE live-kjøringen feilet på
token-taket (162 250 tokens, $0.331506, exit 3) og rapporteres her med samme prominens,
med et datert protokoll-avviksnotat (§4).
- **Klarspråk om bevis-asymmetrien:** MAF-siden er aldri kjørt mot en ekte modell. Alt denne
rapporten sier om faktisk modellatferd gjelder utelukkende Claude-siden — og én enkelt kjøring
er et eksistensbevis: den viser at noe KAN skje under den konfigurasjonen, ikke at det VIL skje.
Den bindende erklæringen gjengis ordrett i §6.
- **Kvalitativt (M4 — case-studie-observasjoner, ikke rammeverks-egenskaper):** søskenet løser
samme metode med ~63 % av MAF-sidens kildevolum (1 696 mot 2 696 linjer; 15 mot 18 moduler) og
har mutasjonskontroller MAF-siden mangler; MAF-siden har et kjørbart offline-simuleringsbevis
søskenet ikke har. Ingen av observasjonene kan attribueres til rammeverkene (N=1 per rammeverk,
samme forfatter — se §5 og §7).
## 1. Bakgrunn og metode
**Målestokkens temporale status (presis):** Protokollen ble bundet 2026-07-03 (S3) FØR den delte
kjernen ble ekstrahert til `portfolio-optimiser-commons` (S4) og FØR søskenrepoet fantes (S5).
MAF-implementasjonen forelå derimot allerede da protokollen ble skrevet. Pre-bindingen beskytter
altså mot at Claude-siden former målestokken etter eget resultat — den beskytter IKKE mot at
målestokken er preget av MAF-sidens allerede eksisterende design. Denne residual-asymmetrien er en
erkjent svakhet og gjentas i §7.
**Interessekonflikt-erklæring:** Forfatteren er Anthropic-fokusert AI-rådgiver, og både begge
implementasjonene og denne rapporten er skrevet med Claude — modellen fra leverandøren av den ene
komparanden.
**Identisk kjerne, bevist:** Begge repo konsumerer den delte kjernen (13 filer: OKF-bundle,
`golden.json`/`validator-input.json`, expert-reviewer-persona, `method-spec.md`) som git-subtree
av commons-ref `db86e15`. Tre-hashen `fc58b0b956e5f9871b29810f17284250e794abd5` er verifisert
identisk FEM veier: `HEAD:shared` i begge repo, `db86e15^{tree}` i begge repo, og — temporalt —
`0f8af04:shared` i søskenet (shared-treet slik det sto i commiten som persisterte
S10-artefaktene). Den femte sjekken beviser at kjernen ikke endret seg mellom live-kjøringen og
denne sammenligningen; protokollens §1-krav om re-pin+re-kjør ved kjerneendring er dermed ikke
utløst. `diff -rq` mellom de to `shared/`-katalogene er tom, og `PORTFOLIO_SHARED_ROOT` var tom i
kjøringsmiljøet (ingen runtime-ompeking av MAF-sidens kjerne-resolver).
**Miljø:** MAF-repo på `695c7590…` (Python 3.12.13, `agent-framework-core`); søsken på
`0f8af04e…` (Python 3.14.0, `claude-agent-sdk`). Python-versjonsskewen er uproblematisk for M1:
begge validatorer bruker kun `random.Random(20260624).uniform(...)`, og golden-testene er grønne
på hver side. Alle kjøringer i S11 er offline (null API-forbruk); søskenrepoet ble behandlet
strengt read-only, bevist med identisk `git status --porcelain` før og etter (§9, rad 13).
## 2. M1 — validator-samsvar mot golden (ekvivalens-gate)
M1 rammes som **ekvivalens-gate**, ikke som differensierende metrikk, av to grunner:
1. **Golden er ikke nøytral mellom sidene.** `golden.json` ble generert av referanseprosedyren i
method-spec §7.2 (navngitt der som «Reference procedure (what generated the golden)»:
`nominal_feasible = 0.30 × Σ quantity·unit_cost`, Mersenne-Twister seed `20260624`,
512 samples, 10-kvantiler) — en prosedyre som stammer fra MAF-linjen. MAF-sidens
golden-samsvar er dermed i praksis selv-samsvar; kun Claude-sidens samsvar er en uavhengig
reproduksjon.
2. **Begge validatorer er ren Python** (MAF `validator.py:41`, søsken `validator.py:25` — samme
seed) — gaten måler metodetroskap, ikke rammeverkene.
Resultat på pinned ref, scopet til det ENE benchmark-bundelet `bygg-energi-mikro`:
- MAF: `tests/test_bygg_energi_mikro.py::test_validator_output_matches_golden` grønn
(i suiten: 157 passed, 4 skipped — skippene er de env-gatede live-testene
`tests/spikes/test_a_groupchat.py`, `tests/test_foundry_profile_live.py`,
`tests/test_local_profile_live.py`, `tests/test_portfolio_live.py`).
- Søsken: `tests/test_bygg_energi_mikro.py::test_decided_figures_match_golden` grønn
(i suiten: 187 passed, 0 failed).
- **Live-reproduksjon:** søskenets committede `runs/s10/run_result.json` reproduserer goldens
persentiler bit-for-bit — `p10 = 68543.08886748762`, `p50 = 95443.98966314227`,
`p90 = 121057.08845805985` (kommando i §9.1).
MAF-simuleringens tall er IKKE golden-tall og siteres aldri som M1-kilde: det skriptede forslaget
har ingen `assumptions`-bånd, så Monte Carlo-trekket degenererer (alle samples like;
p90 = nominal = 90 000). Det er et bevisst designvalg i et plumbing-bevis, ikke et avvik.
## 3. M2/M3 — offline-kategori (begge sider)
Offline-tallene er **designparametre valgt av forfatteren pluss boolske håndhevelses-utfall**
aldri målinger av modellatferd (skriptede stand-ins produserer dem). De sammenlignes kun med samme
kategori på motparten; live-tall står i egen tabell i §4 og blandes aldri inn her.
**Formforskjell (erklært):** MAF-sidens offline-bevis er en kjørbar modul
(`uv run python -m portfolio_optimiser.simulation` — to kjøringer adskilt av en promotering, over
ekte MAF-agenter med skriptet chat-klient). Søskenets offline-bevis ER testsuiten
(`test_step7_async_loop_loadbearing.py` + `test_step8_promotion_loadbearing.py` m.fl.) — det
finnes ingen kjørbar sim-modul der, og testsuiten emitterer følgelig ingen runde-/token-tall å
tabellere.
| Designparameter / håndhevelses-utfall (offline) | MAF (skriptet sim) | Søsken (testsuite-speil) |
|---|---|---|
| Syntetisk tokentelling per svar (designparameter) | 8 (`simulation.py:80`) | — (ingen sim-modul) |
| Debatt-tak (designparameter) | `max_rounds=3` | testene setter egne tak |
| Tak håndhevet? | JA — GroupChatOrchestrator kuttet debatten ved taket i begge runs (tracen viser «reached max_rounds=3; forcing completion» ×2) | JA — budsjett-/loop-tester grønne i suiten |
| Læringssløyfe lukket (fil-båret, promote → re-seed)? | JA — markøren `realiseringsgrad=0.79` fraværende i Run A-prompten, til stede i Run B-prompten; `LEARNING LOOP CLOSED`, exit 0 | JA — to-runs-beviset og promoterings-gaten grønne (del av de 187) |
| Refinement-sti (forrige avslagsgrunn inn i neste forsøk) detach-bevist? | JA — `tests/test_step5_refine_loadbearing.py` | JA — `tests/test_step5_refine_loadbearing.py` |
Simuleringens egen banner-tekst er del av ærlighetskontrakten og gjelder også denne rapporten:
den beviser plumbing, deterministisk ryggrad og at dataflyten lukkes — IKKE at en levende LLM
ville produsert forslaget eller dommen.
## 4. S10 — live-kategori (KUN Claude-siden)
Alt i denne seksjonen er live-observasjoner og gjelder utelukkende Claude-siden (jf. §6). Det ble
gjort TO live-kjøringer; begge rapporteres med samme prominens.
| Størrelse (live) | Kjøring 1 (feilet, 2026-07-03) | Kjøring 2 (fasit, 2026-07-03) |
|---|---|---|
| Utfall | Strukturert stopp: token-tak nådd | `validated` + checker `approve` |
| Tokens | 162 250 (taket 150 000 HOLDT — kjøringen ble stanset) | 36 791 av 150 000 |
| Runder | ikke bevart (ingen artefakter) | 2 av 12 |
| Genererings-forsøk | ikke bevart | 1 |
| Kost | $0.331506 | $0.127514 |
| Exit-kode | 3 | 0 |
| Modell | ikke bevart i artefakt | `claude-haiku-4-5-20251001` |
| Kilde | søskenets STATE.md-logg (prosa) — **disclosed-non-reproducible** | `runs/s10/*.json`, committet i `0f8af04` — recompute-from-artifact |
**Konfigurasjon (protokollens §4.3-krav):** defaultene i søskenets
`src/portfolio_optimiser_claude/run_s10.py` — bundle `bygg-energi-mikro`, `max-rounds 12`,
`max-tokens 150 000`, `max-budget-usd-per-call 0.25`, `max-debate-rounds 3`, `max-attempts 3`,
`top-k 3`.
**Datert protokoll-avviksnotat (2026-07-03):** Protokollens §4.5 lyder: *«Utfall re-rulles ikke:
feiler S10-kjøringen på innhold (avvist forslag, checker-REJECT, tak nådd), er DET resultatet som
rapporteres. Én re-kjøring er tillatt kun ved teknisk transportfeil (nettverk/auth), og
re-kjøringen loggføres eksplisitt.»* Kjøring 1 endte på «tak nådd» — etter ordlyden et
innholdsutfall, og re-kjørings-unntaket dekker kun transportfeil. Re-kjøringen var likevel
operatør-godkjent etter et post-mortem som attribuerte feilen til harness-/konfig-defekter, ikke
modellinnhold: konfig-lekkasje kapret checker-agenten, og en påtvunget preamble gjorde ren
JSON-output umulig (kilde: søskenets STATE-logg). Denne kategoriseringen («harness-feil, ikke
innholdsutfall») ble gjort ETTER utfallet og står ikke i protokollteksten. Rapporten behandler
derfor re-kjøringen som et erklært protokollavvik — det absorberes ikke stille — og rapporterer
kjøring 1 førsteklasses, jf. tabellen over.
**Konfig-diff mellom feilet og vellykket kjøring (publisert):**
`git -C /Users/ktg/repos/portfolio-optimiser-claude diff 0238507..7637c6f --stat` viser 7 filer
endret, 253 innsettinger, 25 slettinger (`artifacts.py`, `loop.py`, `run_s10.py`, `sdk_client.py`
+ tre testfiler), samlet i fiks-commiten `7637c6f` «fix(run): S10 del 2 — post-mortem:
stopp-artefakt, SDK-isolasjon, raw-JSON-direktiv». Endringene er harness-fikser (SDK-isolasjon
via `setting_sources=[]`, raw-JSON-direktiv, persistering av stopp-artefakter) — ikke justering
av modell, prompt-innhold eller tak for å tvinge frem et bedre utfall. Takene var identiske i
begge kjøringer. Leseren kan selv vurdere diffen; kommandoen står i §9.
**Asymmetrisk artefakt-retensjon (erklært):** Den vellykkede kjøringen etterlot fire committede
JSON-artefakter; den feilede etterlot KUN prosa i STATE-loggen (persistering av stopp-artefakter
ble bygget først i post-mortem-fiksen). Tallene for kjøring 1 er derfor ikke-reproduserbare og
merket deretter. **Fremadrettet retensjonsregel:** enhver live-kjøring skal persistere sine
artefakter uansett utfall (søskenets `persist_stop_artifacts` håndhever nå dette).
**Hva kjøring 2 beviser (eksistensbevis):** under §4.3-konfigurasjonen produserte modellen ett
forslag som passerte den deterministiske validatoren og checker-gaten på første forsøk, innenfor
takene. For et forretningspublikum: én kjøring viser at dette KAN skje — den sier ingenting om
hvor ofte det vil skje.
## 5. M4 — kvalitative case-studie-observasjoner
Alt i denne seksjonen er observasjoner av ÉN implementasjon per rammeverk, skrevet av samme
forfatter med samme AI-assistent, der spec-en i tillegg ble ekstrahert FRA MAF-implementasjonen
(S2/S3). Prechelt (2000) fant at variansen mellom programmerere overstiger variansen mellom
språk; med N=1 per rammeverk kan ingen forskjell under attribueres rammeverket. MAF-sidens
spec-troskap er dessuten nær-tautologisk (spec-en ble skrevet fra den), og søskenet hadde
annenmanns-fordel (implementert mot en ferdig spec med fasit). Observasjonene står som
case-studie-funn, ikke rammeverks-egenskaper.
**Kildevolum (ferske tall, kommandoer i §9):**
| Observasjon | MAF | Søsken |
|---|---|---|
| LOC i `src/` (`wc -l`, alle `.py`) | 2 696 | 1 696 |
| Antall moduler (`.py`-filer i `src/`) | 18 | 15 |
| Største moduler | `verdicts.py` 517, `run.py` 453, `simulation.py` 294 | `loop.py` 328, `experience.py` 171, `artifacts.py` 152 |
En del av differansen er formforskjellen fra §3: MAF-sidens kjørbare sim-modul (294 linjer)
finnes ikke i søskenet, der beviset bor i testsuiten. Differansen forsvinner ikke om den
trekkes fra.
**Spec-troskap (mot method-spec §12-tabellen):** begge suiter er grønne på pinned ref, inkludert
golden- og kontrakts-testene som håndhever §12-feltene. MAF-siden har i tillegg en egen
spec-integritetstest (`tests/test_method_spec_loadbearing.py`); søskenet har ingen tilsvarende
fil — dets spec-troskap er bevist via golden-/kontrakts-/persona-testene.
**Load-bearing-dekning (detach-bevis; ærlig asymmetri begge veier):**
- MAF: 9 load-bearing-testfiler (checker-gate, method-spec, persona, shared-root, simulering,
steg 1/5/7/8).
- Søsken: 7 load-bearing-testfiler (checker-gate, persona, steg 1/5/7/8) + `test_sdk_isolation.py`.
- **Søskenet har det MAF mangler:** fire mutasjonskontroller i golden-testen
(`test_changed_quantity_diverges`, `test_changed_assumption_band_diverges`,
`test_expected_actual_is_rate_times_modelled`, `test_learning_surface_is_outside_validator_reach`)
og et persistert `attempts`-felt i run-artefakten.
- **MAF har det søskenet mangler:** kjørbar offline-simulering med eget detach-bevis
(`test_simulation_loadbearing.py`) og spec-integritetstesten.
**Utvikleropplevelse (én-rater-observasjon, ingen rubrikk):** Begge stacker lot metoden
implementeres med type-hints, mypy (søskenet i strict-modus via pyproject) og samme
testdisiplin. Dette er én forfatters erfaring fra to sekvensielle implementasjoner og har ingen
utsagnskraft utover det.
## 6. Liveness-asymmetri-erklæringen (ordrett fra protokollens §3)
> **Liveness-asymmetri-erklæring:** De to stackene har ulik bevis-liveness, valgt av kostnadshensyn (D6). MAF-siden kjøres ALDRI mot en ekte modell; dens bevis er et skriptet offline-bevis av plumbing, deterministisk ryggrad og lukket læringssløyfe. Claude-siden kjører ÉN minimal ekte API-kjøring (S10) som programmets eneste genuine modell-atferdsbevis. Sammenligningen påstår derfor ALDRI noe om modellatferd på MAF-siden; enhver live-observasjon gjelder kun Claude-siden og merkes slik der den siteres.
## 7. Begrensninger (threats to validity)
1. **N=1 implementasjon per rammeverk, samme forfatter og samme AI-assistent** — ingen observert
forskjell kan attribueres rammeverkene (Prechelt-poenget i §5).
2. **Ett benchmark-bundle** (`bygg-energi-mikro`) — M1-ekvivalensen er kun bevist for dette
bundelet.
3. **Interessekonflikt** — forfatter og verktøy er knyttet til leverandøren av den ene
komparanden (§1).
4. **Residual temporal asymmetri** — protokollen predaterer søskenet, men ikke
MAF-implementasjonen (§1); målestokken kan være preget av MAF-designet.
5. **Én live-kjøring, én re-kjøring etter erklært protokollavvik** — live-kategorien er et
eksistensbevis med et deklarert §4.5-avvik (§4); den feilede kjøringens tall er
ikke-reproduserbare (kun STATE-prosa).
6. **Bevis-liveness er asymmetrisk by design** (§6) — offline- og live-kategoriene er holdt i
separate tabeller, og ingen live-påstand finnes om MAF-siden.
## 8. Konklusjon
Begge implementasjonene består ekvivalens-gaten på identisk delt kjerne og lukker læringssløyfa i
offline-kategorien; metoden er altså realiserbar på begge rammeverk med deterministisk validator
som blokkerende gate. Claude-siden har i tillegg programmets eneste live-bevis: én kjøring som
validerte på første forsøk innenfor takene — et eksistensbevis, etter én feilet kjøring som
rapporteres med samme vekt. De kvalitative forskjellene (mindre kildevolum og mutasjonskontroller
hos søskenet; kjørbart simuleringsbevis og spec-integritetstest hos MAF-siden) er
case-studie-observasjoner fra én forfatter og overføres ikke til påstander om rammeverkene.
Skriptede stand-ins er merket som stand-ins overalt hvor de forekommer.
## 9. Verifiseringslogg
Klasser: **reproduce** = kommandoen kan kjøres på nytt og skal gi samme svar;
**recompute-from-artifact** = tallet avledes av committede artefakter (live-tall re-kjøres aldri,
jf. §4.2/§4.5); **disclosed-non-reproducible** = kilden er erklært og ikke-reproduserbar.
Kommandoer kjøres fra MAF-repoets rot (`/Users/ktg/repos/portfolio-optimiser`); søskenet er
`/Users/ktg/repos/portfolio-optimiser-claude`.
| # | Påstand | Kommando / kilde | Klasse |
|---|---|---|---|
| 1 | Tre-hash `fc58b0b956e5f9871b29810f17284250e794abd5` identisk ×5 (inkl. temporal S10-sjekk) | `git rev-parse 'HEAD:shared' 'db86e15^{tree}'` i begge repo + `git -C <søsken> rev-parse '0f8af04:shared'` | reproduce |
| 2 | Delt kjerne = 13 filer, katalogene identiske | `diff -rq <MAF>/shared <søsken>/shared` (tom) + `find shared -type f \| wc -l` | reproduce |
| 3 | `PORTFOLIO_SHARED_ROOT` tom i kjøringsmiljøet | `echo "[$PORTFOLIO_SHARED_ROOT]"``[]` | reproduce |
| 4 | Python-versjoner 3.12.13 / 3.14.0 | `uv run --frozen python -V` i hvert repo | reproduce |
| 5 | MAF-suite: 157 passed, 4 skipped (navngitt i §2) | `uv run --frozen pytest -rs` | reproduce |
| 6 | MAF ruff/format/mypy grønne (18 kildefiler) | `uv run ruff check .` + `uv run ruff format --check .` + `uv run mypy src` | reproduce |
| 7 | Søsken-suite: 187 passed (inkl. steg 7/8-loadbearing) | `PYTHONDONTWRITEBYTECODE=1 uv run --frozen --directory <søsken> pytest -p no:cacheprovider -rs` | reproduce |
| 8 | Søsken ruff/mypy grønne (mypy strict via pyproject) | `uv run --frozen --directory <søsken> ruff check .` + `… mypy src` | reproduce |
| 9 | MAF-sim: løkke lukket, exit 0, markør False→True | `uv run --frozen python -m portfolio_optimiser.simulation; echo EXIT=$?` | reproduce |
| 10 | S10-fasit: 36 791/150 000 tokens, 2/12 runder, attempts 1, $0.127514, `claude-haiku-4-5-20251001` | `cat <søsken>/runs/s10/{usage,run_result,provenance}.json` | recompute-from-artifact |
| 11 | S10-persentiler == golden bit-for-bit | python-kommando i §9.1 | reproduce |
| 12 | `runs/s10/` har nøyaktig én commit (`0f8af04`) | `git -C <søsken> log --format="%H %cI %s" -- runs/s10/` | reproduce |
| 13 | Read-only-bevis: søskenets `git status --porcelain` identisk før/etter S11-kjøringene | BEFORE-/AFTER-fangst i evidence; `git -C <søsken> status --porcelain` (rent) | reproduce |
| 14 | Konfig-diff feilet↔vellykket: 7 filer, +253/25 | `git -C <søsken> diff 0238507..7637c6f --stat` | reproduce |
| 15 | Kjøring 1: 162 250 tokens, exit 3, $0.331506, ingen artefakter | søskenets `STATE.md`, logglinje «S10, live-kjøring 1» | disclosed-non-reproducible |
| 16 | LOC/moduler: 2 696/18 (MAF), 1 696/15 (søsken) | `find src -name '*.py' -exec wc -l {} +` og `find src -name '*.py' \| wc -l` i hvert repo | reproduce |
| 17 | §3-erklæringen gjengitt ordrett i §6 | python-kommando i §9.2 | reproduce |
| 18 | Ingen statistiske ord i rapporten | `grep -inE 'typis[k]\|gjennomsnittli[g]\|stabil[t]' docs/rapport/2026-07-03-sammenligningsrapport.md` → 0 treff (exit 1) | reproduce |
### 9.1 M1 bit-for-bit-kommandoen (rad 11)
```bash
python3 - <<'EOF'
import json, pathlib
g = json.loads(pathlib.Path("shared/examples/bygg-energi-mikro/golden.json").read_text())["validator"]
r = json.loads(pathlib.Path("/Users/ktg/repos/portfolio-optimiser-claude/runs/s10/run_result.json").read_text())["outcome"]
for k in ("p10", "p50", "p90"):
assert g[k] == r[k], (k, g[k], r[k])
print("OK: golden == run_result (bit-for-bit p10/p50/p90)")
EOF
```
### 9.2 Verbatim-kommandoen (rad 17, whitespace-normalisert sammenligning)
```bash
python3 - <<'EOF'
import re, pathlib
proto = pathlib.Path("docs/plan/2026-07-03-sammenligningsprotokoll.md").read_text()
block = re.search(r"> \*\*Liveness-asymmetri-erklæring:\*\*.*?(?=\n\n## )", proto, re.S).group(0)
decl = "\n".join(l.lstrip("> ").rstrip() for l in block.splitlines())
report = pathlib.Path("docs/rapport/2026-07-03-sammenligningsrapport.md").read_text()
assert decl.replace("\n", " ") in " ".join(report.split()), "asymmetry declaration NOT verbatim"
print("OK: declaration verbatim")
EOF
```