From 234e84c8a9c881b11ca7ba5e0edf492e3f04e3f0 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 3 Jul 2026 12:50:01 +0200 Subject: [PATCH] =?UTF-8?q?docs(rapport):=20S11=20=E2=80=94=20sammenlignin?= =?UTF-8?q?gsrapport=20begge=20stacker=20p=C3=A5=20pinned=20db86e15?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-07-03-sammenligningsrapport.md | 300 ++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 docs/rapport/2026-07-03-sammenligningsrapport.md diff --git a/docs/rapport/2026-07-03-sammenligningsrapport.md b/docs/rapport/2026-07-03-sammenligningsrapport.md new file mode 100644 index 0000000..19aaadb --- /dev/null +++ b/docs/rapport/2026-07-03-sammenligningsrapport.md @@ -0,0 +1,300 @@ +# 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 rev-parse '0f8af04:shared'` | reproduce | +| 2 | Delt kjerne = 13 filer, katalogene identiske | `diff -rq /shared /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 pytest -p no:cacheprovider -rs` | reproduce | +| 8 | Søsken ruff/mypy grønne (mypy strict via pyproject) | `uv run --frozen --directory 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 /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 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 status --porcelain` (rent) | reproduce | +| 14 | Konfig-diff feilet↔vellykket: 7 filer, +253/−25 | `git -C 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 +```