portfolio-optimiser/CLAUDE.md
Kjell Tore Guttormsen 426ccb0ad6 feat(4d): hostet inngang — main.py wrapper rundt run_project på én asyncio-løkke
To målinger avgjorde formen FØR koden: (1) hosting-pakkas InvocationsHostServer
finnes kun i bygg som krever agent-framework-core>=1.13.0 (treet låser 1.9.0;
eneste 1.9-kompatible bygg er en forlatt alfa som importerer mcp udeklarert),
(2) et gjenbrukt bygget workflow er single-use på 1.9.0 (kall-serie [2,0,0] —
rundetaket persisterer; ferskt objekt per kall er ren kontroll). Derfor spikens
§5-fallback: hosting.py serverer kontrakten (8088/PORT, /readiness,
/invocations, SIGTERM→0) selv, stdlib asyncio på ÉN løkke — aldri as_agent()
(gatene ligger utenfor grafen), aldri tråder (NG1-guarden fanget første utkast
med ThreadingHTTPServer; asyncio-formen består den by construction).

Payload whitelistes på run_projects signatur — ukjente felt nektes ved navn
(400), aldri stille droppet; profile defaulter til azure kun her. ValueError →
400, alt annet → 500 {error_type, error}; Rejection er vellykket kjøring → 200.
outbox.outcome_payload ekstrahert som den ENE kopien av validated/rejected-
forgreningen (kø-(p)-regelen). azure.yaml validert GRØNN mot begge autoritative
skjemaer (jsonschema, hentet ferskt); ingen env:, ingen startupCommand (imagets
CMD er den ene kopien). Dockerfile: 3.12-slim-bookworm + git + uv==0.9.8 +
uv sync --frozen --no-dev; git archive <indeks-tre> | docker build
--platform linux/amd64 grønn på nøyaktig de stagede bytene.

Iron Law fulgt: testfila rød ved collection FØR modulen fantes. 835 passed /
4 skipped (fra 821), ruff+format+mypy rene. Seks mutasjoner mot HELE suiten,
alle røde på riktig test: detach felt-mappingen · dropp ukjente felt stille ·
flipp 400/500 · detach azure-defaulten · detach SIGTERM-handleren · detach
main.py-shimen (de to siste kun fanget av subprosess-testen, P4-presedensen).
Deploy IKKE utført — azd-steget er operatørens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEiiSGRShizKc771ZBa1iq
2026-08-13 23:31:02 +02:00

486 lines
47 KiB
Markdown

# portfolio-optimiser
## Kontekst
Generisk, åpent Python-rammeverk på **Microsoft Agent Framework (MAF)** som finner kostnadsbesparelser INNI hvert prosjekt i en portefølje av uavhengige prosjekter. Multi-agent samarbeid genererer kandidat-tiltak; en obligatorisk deterministisk validator avgjør verdiene; fageksperter vurderer via HITL, og systemet lærer av dommene. Publiseres på Forgejo for alle som har MAF.
Bakgrunn og beslutninger: [research](docs/research/2026-06-23-prior-art-platform.md) (§15 implementeringsregister), [plan](docs/plan/2026-06-23-incremental-plan.md). Løpende state: `STATE.md` (local-only).
## Stack
Python ≥3.10. MAF (`agent-framework-core` 1.9.0). Pakkehåndtering: `uv`. To backend-profiler: Azure/Foundry (full) + lokal (fallback).
## Konvensjoner
- Type hints overalt (`mypy` der mulig). Pydantic for validering/IR.
- `ruff` for lint+format. `pytest` for test.
- Modell-valg som konfig (modell-map rolle→Foundry-deployment), ikke spredt i kode.
- Metode kodifiseres som **Agent Skill** (`agentskills.io`: `SKILL.md` + `scripts/` + `references/`).
- Datatilgang: in-process `FunctionTool` er default-sømmen i kjørestien. **MCP er wiret som opt-in i kjørestien** (`mcp_tools.py` + `--mcp-config`, Trekk B 2026-08-05): konkrete eksterne servere blir verktøy agentene kan kalle UNDER debatten. Uten konfig gjøres null nettverkskall og verktøylista er uendret. Tre regler er load-bearing: **allowlist er påkrevd** (tom liste ville latt motparten bestemme hva agentene får kalle), **hver server og hvert tillatte verktøy navngis i kunngjøringen før første kall** (også uten `--mandate` — ingen udeklarert egress), og `--live-dry-run` åpner **ingenting**. Egen søm fra `ingest_mcp.py` (kildedokumenter FØR kjøring, null-argument-tools) — samme protokoll, ulik jobb. `build_mcp_server` (`datasource.py`) er fortsatt kun demo. Data-source-konfig JSON-Schema-validert, fail-fast.
- `shared/` er en **git subtree** av [`portfolio-optimiser-commons`](https://git.fromaitochitta.com/open/portfolio-optimiser-commons) (source of truth, R1 realisert 2026-07-03; publisert i `open/` 2026-08-04 — `commons`-remoten peker fortsatt på `ktg/` og virker uendret). Synk er **pull-only**: endringer committes i commons og hentes med `git subtree pull --prefix=shared commons main --squash`. ALDRI `git subtree push` fra konsument — re-split lekker hele konsument-historikken inn i commons (observert + opprydd 2026-07-03). Se `shared/README.md`. Wheelen bærer treet som pakkede data siden Fase 4a — se invarianten under.
## Kommandoer
- Sync: `uv sync` — installerer to konsoll-kommandoer: `portfolio-optimiser` (CLI, `run:main`) og
`portfolio-optimiser-demo` (offline-beviset, `simulation:main`). `python -m`-formene virker
uendret og er byte-identiske på stdout (målt). Bevisst KUN to av fem `main()`
`costsim`/`hitl`/`preflight` er operatørverktøy, ikke produktets inngang, og hvert navn her er et
navn frysen må bære. Pinnet av `tests/test_console_entry_points.py` mot den INSTALLERTE
distribusjonens metadata, ikke mot TOML-en: en `[project.scripts]`-linje som aldri er `uv sync`-et
er en påstand, ikke en kommando.
- Test: `uv run pytest`
- Lint: `uv run ruff check .` + `uv run ruff format .`
- Type: `uv run mypy src`
## Arbeidsflyt (invarianter)
- **Rent teknisk rammeverk:** deployer eier DPIA/ROS/behandlingsformål. Bygg IKKE compliance-funksjoner — kun tekniske forutsetninger (lokal-only, provenance, ingen stille egress) + disclaimer.
- **90%-prinsipp:** bygg den generiske kjernen + tydelige extension points; jakt IKKE de siste 10 %.
- **Deterministisk validator er obligatorisk og blokkerende** — aldri valgfri plugin.
- **Framework-nøytral kontekst-søm:** OKF-bundle-navigasjon (`okf.py`) og den delte
`shared/`-kjernen er ren stdlib — null `agent_framework`/`mcp`-import, så samme bundles
konsumeres uendret av begge stacker (D7-portabel). Håndhevet av
`tests/test_okf.py::test_okf_is_maf_free`; importér aldri MAF inn i kontekst-laget.
- **OKF-navigert bundle-kontekst (ikke stuffing):** på bundle-stien bygges agent-lese-konteksten
ved å NAVIGERE bundelen (`okf.bundle_context`: index + frontmatter + cross-links, progressiv
disclosure) — aldri keyword-chunk-stuffing (målbilde §2/§4). `type: verdict`-laget ekskluderes
fra denne konteksten: tidligere dommer når hypotese-prompten KUN via den gatede ExpeL-folden.
Load-bearing: `test_bundle_context_excludes_verdict_layer` + empty-store-kontrollen i
`test_step1_expel_loadbearing.py` (realiseringssignalet lekker aldri inn via kontekst).
- **Navigasjons-kontrakten er hierarkisk, og escape — ikke dybde — er forbudt** (`method-spec`
§3 Steg 1): `navigate_bundle` følger cross-links REKURSIVT, dybde-først i først-sett-rekkefølge;
ledende `/` betyr **bundle-rot** (aldri filsystem-absolutt), alt annet er relativt til den
LENKENDE filas katalog; dedup skjer på **resolvert** sti (så `./a.md` == `a.md`, og sykler
termineres). `safe_resolve` er den ENESTE inn-/ut-av-bundle-testen (fail-closed) — den erstattet
den pensjonerte «separator = utenfor bundelen»-heuristikken, som forvekslet dybde med escape.
Manglende `index.md` er feil KUN i bundle-rota (navigasjon følger lenker, aldri katalog-enumerering).
Rendering er FLAT uansett dybde; nestede `index.md` er navigasjon, ikke innhold. Gaten er
commons-eide nav-goldens (`shared/examples/nav-golden-*/expected-read-context.md`, byte-nivå
fasit): `test_nav_golden_hierarchy_*` (positiv) + `test_nav_golden_escape_*` (negativ — en gate
som bare kan bli grønn beviser ingenting).
- **Kuraterte skrivere kan ikke forfalske ingest-stempelet** (`ingest-spec` §3): `write_concept_file`
er repoets ene authoring-primitiv som materialiserer en konseptfil fra CALLER-oppgitt frontmatter,
og avviser derfor det KOMPLETTE eierskaps-stempelet (`generated: true` + `ingest_manifest`) med
`IngestStampError` — mens hver halvdel alene er lovlig (kuratert innhold kan bære ett
provenance-felt). Validering, ALDRI reparasjon: ingenting skrives. Uten dette kunne en kuratert fil
bli stille slettet av en senere re-materialisering, som fjerner nøyaktig det som bærer stempelet.
- **`IngestError` må overleve anyio-task-gruppene (kø-(x), 2026-08-03):** `stdio_client` og
`ClientSession` er hver sin task group, og anyio pakker ALT som forlater en av dem i en
`BaseExceptionGroup`. Derfor nådde `stdio_call_tool`s egne feil (`mcp_tool_error`,
`mcp_non_text_content`) kalleren som en exception group — aldri som `IngestError`, som er typen
hele Door A fanger og switcher på via `code`. `_unwrap_ingest_error` pakker ut og re-raiser den
eide feilen; alt annet re-raises URØRT (innsnevring, aldri blanket re-raise). Duck-typet på
`.exceptions`, fordi `except*`/`ExceptionGroup` er 3.11+ og repoet er `>=3.10`. **Ingen
canned-tool-test kunne fanget dette** — de går aldri inn i en task group; defekten dukket opp
første gang koden faktisk ble kjørt. En MCP-server på ingest-stien må eksponere en
**null-argument-tool** (URL-en bærer begge koordinatene, tool kalles med `{}`), så
`datasource.build_mcp_server` kan IKKE serve den — `retrieve_cost_docs(query)` har et påkrevd
argument og returnerer et error-result. De to er separate sømmer med vilje. Load-bearing MÅLT
(`tests/test_ingest_golden_mcp.py`), fem mutasjoner alle røde: detach unwrappingen · detach
`initialize()` · gjør feilkoden generisk · detach `isError`-grenen · endre ett byte av bodyen.
- **MCP-timeouten må komponeres MED anyios eget cancel scope, ikke `asyncio.wait_for` utenfra
(kø-(z), 2026-08-03):** STATE-premisset ("en utypet `TimeoutError` re-raises urørt") var FEIL —
målt mot en EKTE hengende server ga `asyncio.wait_for(run(), timeout=...)` aldri en
`TimeoutError` i det hele tatt; den kansellerer `run()` UTENFRA strukturen anyio selv eier
(`stdio_client`/`ClientSession`), og de to kansellerings-mekanismene komponerer ikke — målt
utfall var en `anyio.BrokenResourceError` inni en `BaseExceptionGroup` (en bakgrunns-reader-task
mistet skrive-enden midt i nedrigging). Fiksen er `anyio.fail_after(timeout_seconds)` NESTET
INNI begge task-gruppene, der anyio rigger ned sin egen struktur rent og raiser en ren
`TimeoutError`. **Innsnevringen dekker IKKE bare typen:** builtin `TimeoutError` er også
`socket.timeout` (≥3.10) og `asyncio.TimeoutError` (≥3.11), så et ubetinget `except TimeoutError`
ville mislabelt en HVILKEN SOM HELST `TimeoutError` som `mcp_timeout` — reviewet FØR commit
(advisor) fant nøyaktig dette. Retteslen er `anyio.CancelScope.cancelled_caught`: kun scopet som
faktisk traff SIN EGEN deadline tjener `mcp_timeout`-koden, ellers re-raises urørt (speiler
`_unwrap_ingest_error`s eierskaps-regel). **Ingen levende utløser finnes i dag** for en
"fremmed" `TimeoutError` på denne stien — MÅLT: MCPs eget per-request read-timeout
(`ClientSession.send_request`) konverterer sin `anyio.fail_after` til `McpError` FØR den når oss,
og en tool som raiser `TimeoutError` server-side blir et ordinært `isError`-resultat (samme
som enhver annen tool-exception) — begge verifisert empirisk, ikke antatt. Diskriminatoren er
likevel pinned med en syntetisk test (raiser fra `StdioServerParameters`-konstruksjon, inni
fail_after-scopet men FØR noen task group), fordi defektklassen ellers ikke har en nåbar sti å
bevise den mot. Load-bearing MÅLT (`tests/test_ingest_golden_mcp.py`) mot HELE 625-suiten, fire
mutasjoner alle røde: detach hele oversettelsen · revert til `asyncio.wait_for` · relabel koden ·
detach `cancelled_caught`-gaten (behold kun scope-presence). `anyio` promotert fra transitiv
(via `mcp`) til deklarert direkte dep (`pyproject.toml`) — modulen importerer den nå direkte.
- **Door A-innholdsgaten kan IKKE bo i `materialize` — den bor rundt den (P2/S1.b, 2026-08-09):**
`materialize` er en REN delegasjon til det pinnede `llm_ingestion_okf` v0.3.2s
`materialize_bundle`, som stager i minnet og utfører sin EGEN disk-fase; **det finnes ingen
callback mellom de to**, så en gate plassert «i skrivepunktet» kunne bare kjørt ETTER at bytene
hadde landet — en opprydding, ikke en gate (planens premiss, felt ved måling FØR bygging).
`materialize_gated` er derfor: **kopier bundelen → materialiser inn i kopien → skann det som ble
generert → publiser eller forkast.** **Kopien er BÆRENDE:** bibliotekets §3 eierskaps-skann,
kollisjons-gaten mot kuratert innhold og §6 index-merge leser alle den EKSISTERENDE bundelen —
staging i en tom katalog mister alle tre og publiserer en bundle uten kuraterte naboer og deres
index-lenker (datatap forkledd som sikkerhetsfiks; MÅLT av kun ÉN test, 809 andre merket
ingenting). `materialize` forblir UGATET med vilje — fire golden-suiter pinner bytene, og en
kaller som vil ha gaten ber om den ved navn. **Utfall per BUNDLE, diagnostikk per DOKUMENT:**
delvis publisering ville etterlatt bundle + index som svarer til INTET manifest, men
`import_bundle` itererer forbi første avvisning så hvert funn rapporteres. **Trust følger ORIGIN,
aldri channel** (`Origin.EXTERNAL`/`Channel.AUTOMATIC` = UNTRUSTED) — ikke et av guardens to
`Policy`-preset: `PRESET_USER_UPLOAD` bærer `quarantine_default=True` som Door A ikke har.
**Den laveste dispositionen er `warn`, ikke `allow`** (`warn < quarantine_review < fail_secure`;
`allow` finnes ikke) — en gate skrevet mot `== allow` ville avvist hvert dokument noensinne.
**Funnene til `log.md` (OKF §7), ALDRI konsept-frontmatter** — der ville de brutt fire goldener.
Guarden shipper ingen `py.typed`: mypy-override ALENE gjør sømmen type-BLIND, så `_stamp_line` +
koersering stopper `Any` ved grensen. Load-bearing MÅLT
(`tests/test_ingest_content_gate_loadbearing.py`), fem mutasjoner alle røde + grønn kontroll:
detach gaten · la den fyre ETTER publisering · `Origin.INTERNAL` · tom staging-katalog ·
rapporter kun første avvisning. **Målingen felte en VAKUØS test først:** en hard injeksjon
scorer `fail_secure` under BEGGE tierene, så `Origin.INTERNAL`-mutasjonen lot alle tre
avvisningstestene stå grønne — beslutningen så dekket ut uten å være testet. Båndet der tieren
faktisk avgjør er høy-entropi-innhold (`quarantine_review` vs `warn`), og testen ble skrevet mot
nøyaktig det.
- **`shared/` leses som PAKKEDE DATA, med arbeidstreet som overstyring (Fase 4a):** wheelen bærer
en byte-identisk speiling av hele `shared/`-treet under `portfolio_optimiser/_shared/`
(hatchling force-include i `pyproject.toml`), og `shared_root()` løser ved KALL-tid i fast
rekkefølge: `PORTFOLIO_SHARED_ROOT` → arbeidstreets `shared/` når det finnes → pakket kopi.
**Arbeidstreet er autoritativt i en checkout** — det er dét som holder pull-only-subtree-
kontrakten og de byte-eksakte goldenene urørt (målt: goldens shasum-identiske før/etter, og
`shared/` selv urørt). Den pakkede kopien er dét som gjør wheel og container mulig uten klone
(målt før: 1.0.0-wheelen bar 58 filer, null under `shared/`; etter: 122, hvorav 64 under
`_shared/`, og sdist→wheel-kjeden bærer treet). Speilingen er ALDRI en redigert derivat —
byte-identitet er egenskapen som lar commons-goldenene fortsatt gate den pakkede kopien.
Load-bearing MÅLT (`tests/test_shared_packaged_data_loadbearing.py`, ekte `uv build` i
fixturen — pakkekonfigen er selv en søm), tre mutasjoner alle røde mot hele suiten: detach
fallbacken (1 rød) · detach force-include (3 røde) · snu rekkefølgen (1 rød — ordnings-testen
var grønn før fiksen; dens kontroll på at pakket kopi FINNES er det som gjør flippen målbar).
- **AZURE-profilen leser MILJØET sitt ved kall-tid, ikke operatørens laptop (Fase 4b):** endepunktet
løses som første IKKE-TOMME av `_ENDPOINT_ENVS` — vårt eget `PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT`
FØRST, deretter Foundrys injiserte `FOUNDRY_PROJECT_ENDPOINT`. **Vårt vinner** (det er dét enhver
doc, recipe og test setter, så en eksport av det er en bevisst handling; en plattformverdi som
stille overstyrte den ville vært uforklarlig utenfra), og fallbacken er dét som lar samme image
kjøre hostet uten ekstra wiring. **Presedensen gjelder VERDIER, ikke deklarasjoner** — et
eksportert-men-tomt eget navn faller igjennom i stedet for å skygge et ekte injisert inn i en
fail-fast. Feilmeldingen navngir BEGGE: operatøren i en container og operatøren på en laptop
leter etter hver sin variabel. Credential velges av samme miljø: `AzureCliCredential` lokalt
(konstruksjon henter INGEN token — `az login` er operatørens manuelle steg),
`ManagedIdentityCredential` når `FOUNDRY_HOSTING_ENVIRONMENT` er satt, fordi containeren ikke har
noen Azure CLI og plattformen mynter den en egen Entra-identitet ved deploy. **Ikke
`DefaultAzureCredential`:** Learns egen MAF-veiledning sier «prefer a specific credential such as
`ManagedIdentityCredential` to avoid unintended credential probing» — probing ville vandret en
kjede som ikke KAN lykkes der, og gjort en konfigfeil om til en treg en. Markøren leses på
**truthiness, ikke presence**: en eksportert tom verdi er et shell-uhell, ikke et hosting-signal.
Klienten eksponerer INGEN credential-attributt (målt), så testene observerer via en
`FoundryChatClient`-recorder — med én UPATCHET arm, ellers ville de kun bevist at vi sender
*noe* som heter `credential`. Load-bearing MÅLT
(`tests/test_hosted_backend_loadbearing.py`), fire mutasjoner alle røde mot hele suiten + grønn
kontroll: detach credential-valget · presence i stedet for truthiness · detach fallbacken · snu
presedensen. **Fail-fast-testen ble skrevet VAKUØS først** (repoets 08-09-klasse):
`PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT` INNEHOLDER `FOUNDRY_PROJECT_ENDPOINT`, så asserten på det
injiserte navnet var oppfylt av vårt eget; den fjerner nå vårt navn før den sjekker.
- **Hostet inngang er en WRAPPER rundt `run_project` på ÉN asyncio-løkke (Fase 4d):** `main.py`
`hosting.py` serverer hosting-kontrakten (port 8088/`PORT` på truthiness, `GET /readiness`,
`POST /invocations`, SIGTERM → exit 0) med stdlib asyncio — ALDRI `as_agent()` (validator,
baseline-forankring, checker-gate og ledger ligger UTENFOR grafen, spike §5) og ALDRI tråder
(NG1-guarden: `http.server`s trådvariant ville lagt samtidige kjøringer på OS-tråder der
S3.3-resonnementet ikke holder; samtidige invocations interleaver som koroutiner — samme modell
som `run_portfolio`s bølger, og `/readiness` svarer mens en kjøring venter på modell-I/O, målt).
**Formen er MÅLT, ikke valgt:** hosting-pakkas `InvocationsHostServer` finnes kun i bygg som
krever core>=1.13.0 (treet låser 1.9.0; eneste 1.9-kompatible bygg er en forlatt alfa med defekt
metadata — importerer `mcp` udeklarert), og et gjenbrukt bygget workflow er SINGLE-USE på 1.9.0
(målt kall-serie [2, 0, 0] — rundetaket persisterer i objektet, så gjenbruk gir TOMME kjøringer;
ferskt objekt per kall er ren kontroll). Payloaden whitelistes på `run_project`s signatur —
ukjente felt NEKTES ved navn (400), aldri stille droppet (valg-doc §0-fella anvendt på vår egen
flate); `profile` defaulter til `azure` KUN her (containeren har ingen lokal endpoint;
`run_project`s egen default forblir LOCAL). Feilmapping ærlig: `ValueError` (pydantic-kontrakter
subklasser den) → 400, alt annet → 500 `{error_type, error}` (speiler `RunFailure`), og en
`Rejection` er en VELLYKKET kjøring → 200 — det negative utfallet tilhører payloaden, aldri
transporten. `outbox.outcome_payload` er den ENE kopien av validated/rejected-forgreningen
(delt av fil-skriveren og HTTP-responsen — to kopier drifter, kø-(p)-regelen). `azure.yaml`
validert GRØNN mot begge autoritative skjemaer; ingen `env:` (redeklarer aldri
`FOUNDRY_PROJECT_ENDPOINT`), ingen `startupCommand` (imagets `CMD` er den ene kopien av
startkommandoen). `git archive <tree> | docker build --platform linux/amd64 -` grønn på
indeks-treet. Load-bearing MÅLT (`tests/test_hosting_loadbearing.py`), seks mutasjoner alle
røde mot hele suiten på riktig test: detach felt-mappingen · dropp ukjente felt stille · flipp
400/500 · detach azure-defaulten · detach SIGTERM-handleren · detach main.py-shimen (de to
siste fanges KUN av subprosess-testen — P4-presedensen). Deploy er IKKE utført (azd-steget er
operatørens); chunked request-bodies støttes ikke, og under CPU-bundne strekk (CBC-solven)
står readiness — uttalt, ikke skjult.
- **Stoppkriterier + budsjett-tak påkrevd ved oppstart** (fail-fast, aldri ubegrenset loop).
- **Group Chat maker-checker** som debatt-default (IKKE Magentic, som er eksperimentell).
- **To falsifiserere, samme kandidat (Steg 3/4, målbilde §2/§6):** den deterministiske validatoren
gater *tallene* (blokkerende), checkeren gater *resonnementet*. Checkeren avslutter turen med en
`VERDICT: APPROVE` / `VERDICT: REJECT — <grunn>`-linje; et eksplisitt avslag blokkerer et ellers
validert forslag (`run_project` overflater begge debatt-deltakere via `output_from=agents` og
overstyrer utfallet til en checker-kilde-`Rejection`). Gaten er opt-in-reject (fail-open ved
manglende markør), og `provenance.validator_decision` forblir ærlig — den speiler KUN validatoren,
aldri checkeren (de to falsifisererne blandes aldri). Load-bearing:
`tests/test_checker_gate_loadbearing.py` blir rød ved BEGGE detach-punkt (revert `output_from`,
eller fjern override). Checkeren «må faktisk gate, ELLER vi slutter å kalle det maker-checker».
- **Informert forbedring, bundet (Steg 5, målbilde §5/§7):** `generate_via_llm`s ytre
`max_attempts`-løkke er ikke lenger blind — validatorens *forrige* `Rejection.reason` mates inn i
neste forsøks prompt (`_build_messages(prior_rejection=...)`), så proposeren korrigerer i stedet
for å gjenta. Kun den mest-nylige falsifiseringen (`last`, ikke akkumulert), kun grunnen (aldri
forrige proposal-JSON), under EKSISTERENDE tak (`meter.tick_round` + `max_attempts` — ingen ny
løkke; «forbedre til god nok» uten tak er forbudt). Eneste *per-forsøk*-falsifiserer her er
validatoren; å seede generering med checker-*kritikken* er run-nivå og separat scoped (IKKE bygget
her) — så koden påstår ikke mer enn den gjør. Load-bearing:
`tests/test_step5_refine_loadbearing.py` blir rød når reason-injeksjonen detaches (utfallet
flipper aldri + reason-verbatim-asserten faller); kontrollen beviser at løkka forblir bundet.
- **Falsifiserings-historikken FORLATER generate-løkka som typet returverdi (Steg 5, del 2):**
`generate_via_llm` returnerer `GenerationResult(outcome, refinements)` — ikke lenger bare
`ValidatedProposal | Rejection`. Før dette forbrukte løkka hver `Rejection` internt (`last`) og
DROPPET den, så Steg 5 var det ene av åtte steg uten observerbart utfall. **Returverdi, ikke
out-parameter/callback:** en returnert verdi kan ikke bli stille tapt av en kaller som glemmer å
sende en samler, og mypy tvinger hvert kallsted til å ta stilling. **`refinements` bærer KUN
avvisninger som faktisk ble matet tilbake** i et senere forsøks prompt — ved uttømt budsjett ER
den siste avvisningen `outcome`, den informerte ingenting, og å telle den med ville vært
dobbeltføring (en «samle alt»-implementasjon består den positive testen og faller på kontrollen).
Taket er URØRT: `max_attempts` + `meter.tick_round` står, og `last` driver fortsatt prompten alene
(prompt-veksten er uendret). `run.py` akkumulerer på tvers av `_evaluate`-kallene, så
`_evaluate_mandate` er urørt; `RunResult.refinements` er defaultet (`coverage`-presedensen), og
med mandat er den KONKATENERT på tvers av tiltak, ikke nøklet per tiltak (uttalt ærlighets-grense).
`scripted_factory` tar nå `str | reply_selector` per rolle, så simuleringens proposer korrigerer
seg innholds-nøklet uten en andre scriptet kropp. Load-bearing MÅLT
(`tests/test_step5_history_loadbearing.py`), fire mutasjoner: detach returneringen · samle-alt ·
detach run-wiringen · reverter simuleringens proposer til konstant svar.
- **Lang/async fil-løkke (Steg 7, målbilde §3/§7):** `run_project(verdict_dir=...)` er den lange
tilbakemeldings-tidsskalaen — en ekspert/persona dropper en verdict-fil (vanlig JSON, RAW-laget
per §10 R2) i en inbox-mappe ETTER en kjøring, og en separat, senere kjøring `load_verdicts_from_dir`
`store.add` **merger** den inn FØR Steg-1-folden (ingen endring i folden), så dommen når neste
hypotese. **Rolledeling (§3, ufravikelig):** systemet LESER mappa; eksperten/personaen SKRIVER den
`run_project` persisterer ALDRI sin egen fangede dom tilbake (det er outbox/Steg 8). Merge, aldri
erstatt (`run_portfolio`-tråding intakt); tolerant last (manglende mappe / fremmede / halvskrevne
filer hoppes over, ikke raises — RAW-lag, kontrast `okf.load_ir_projection`s fail-fast); `id` leses
verbatim, re-mintes aldri. `write_verdict` er den offentlige authoring-primitiven (persona/test +
framtidig Steg 8), men wires IKKE inn i `run_project`. Load-bearing:
`tests/test_step7_async_loop_loadbearing.py` — en dom droppet etter Run A MÅ nå Run B's prompt
(Run B bruker FERSK store → overføringen er fil-løkka, ikke in-memory-carryover); tom-inbox-kontroll
beviser kausalitet. Markør = realiseringsverdi som finnes ingen steder i bundelen (ikke frøets 0.82).
- **Gated wiki-promotering (Steg 8, målbilde §3/§6/§7):** når en ekspert/persona GODKJENNER et
utfall, løfter `verdicts.promote_verdict` det fra RAW output-laget inn i kontekst-laget (OKF-bundelen)
som en `type: verdict`-konseptfil, navigerbar av neste kjørings `seed_store_from_bundle`. **Gaten er
fail-closed:** en ikke-godkjent dom (`decision ∉ {approved, approved_with_adjustment}`) raiser
`PromotionRefused` og skriver/linker INGENTING — kun menneske/persona-godkjent kunnskap når wikien,
aldri rå agent-output (selv-forurensning). Provenance-stemplet (hvem/eksperiment/når; `timestamp` er
påkrevd keyword, ingen wall-clock-default → deterministisk). **OKF-skriveren bor i `okf.py` og er ren
stdlib** (D7-portabel, MAF-fri — håndhevet av `test_okf_is_maf_free`); navigasjon følger KUN
index-cross-links, så `promote_verdict` linker filen i `index.md` via en NØYTRAL label (ellers lekker
signalet inn i `index_summary``bundle_context` utenom gaten). **R4 = valgfri+gated:** `promote_verdict`
er en offentlig opt-in-primitiv, wires IKKE inn i `run_project` (speiler `write_verdict` — systemet
leser; gaten/personaen promoterer). Ærlighets-grenser: promotert fil er MINIMAL (læringssignal kun som
`description`/body-prosa, reproduserer ikke seedens strukturerte `realization_rate` o.l.); id =
læringsnøkkel, så to godkjenninger om samme kandidat deler filnavn (last-write-wins, som `write_verdict`)
— wikien vokser én kuratert fil per distinkt kandidat, ikke per dom-hendelse. Load-bearing-trio
(`tests/test_step8_promotion_loadbearing.py`): gaten avviser ikke-godkjent dom (RØD uten gate); godkjent
dom er navigerbar (RØD når `link_in_index` detaches); promotert signal holdes ute av `bundle_context`
(RØD når en beskrivende index-label lekker det inn). Index-RMW er ikke-atomisk (enprosess-MVP).
- **En dom nøkles på SIN kandidat, ikke bundelens ene IR-projeksjon (S3.2):** `seed_store_from_bundle`
leser hver `type: verdict`-fils EGNE strukturelle felt fra frontmatter (`affected_codes` /
`measure_type` / `claimed_saving_nok`); mangler de, faller nøklingen tilbake til
`bundle_candidate_features` — så hver pre-S3.2-seed står uendret (fallbacken er BÆRENDE: fjernes
den, brekker step1-suiten ved collection). Uten dette kollapset en bundle med dommer om flere
kandidater dem på ÉN nøkkel, og en dom om kandidat B scoret perfekt strukturell match mot
kandidat As query. **ALLE TRE felt eller ingen:** en delvis erklæring raiser
`VerdictFrontmatterError` i stedet for å slås sammen med bundle-kandidaten — sammenslåingen ville
myntet en nøkkel som tilhører INGEN av kandidatene. Validering, ALDRI reparasjon (speiler
`write_concept_file`); den tolerante hopp-over-regelen hører til RAW-innboks-laget.
`claimed_saving_nok` parses med `json.loads` — SAMME literal-regel IR-projeksjonen gikk gjennom —
og skrives tilbake som `str()` av råverdien, fordi `_mint_id` hasher den (`30000``30000.0`; en
normaliserende skriver ville splittet én kandidats signal på to id-er). `promote_verdict` skriver
de tre feltene, så en promotert dom ikke utgir seg for målbundelens kandidat; nøkkelen er
signal-fri, så Steg-8s no-leak-egenskap står. **Semantikken er bestemt HER** — commons' seeding-regel
(`method-spec` §3 Steg 1) hadde ikke kommet; D7-speiling forblir ÅPEN. Load-bearing MÅLT
(`tests/test_step32_multicandidate_loadbearing.py` + `test_step8_promotion_loadbearing.py`), fem
mutasjoner alle røde: detach per-dom-nøklingen · detach feltene `promote_verdict` skriver · gjør en
delvis/uparsebar nøkkel tolerant · normaliser magnituden ved skriving · fjern fallbacken (kontroll).
- **Den deterministiske gaten er FORANKRET i prosjektets faktiske kostbaseline (S4.0, F3/F8):** før
S4.0 resonnerte HVER stage kun om tall forslaget selv oppga, så en internt konsistent hallusinasjon
klarerte hele gaten. `validate_proposal(..., baseline=...)` avstemmer nå hvert `affected_item` mot
`CostBaseline` (`ir.py`) i en **stage 0 — FØR løseren** (billigst, og den eneste som skiller en
oppdiktet linje fra en ekte; å bruke en CBC-solve på tall som ikke tilhører prosjektet er arbeid på
et krav som uansett ikke kan valideres). To uavhengige avvisninger: ukjent kostkode, og ekte kode
med `quantity`/`unit_cost` utenfor `tolerance` (default 5 %, **konfig**) relativt til BASELINE-verdien.
Validering, ALDRI reparasjon — forslaget avvises, aldri stilltiende korrigert til baselinen.
**Argumentet er VALGFRITT** (`None` = pre-S4.0-oppførsel), men begge run-stier SETTER det: road-stien
fra `project.cost_items` (alltid — estimatet ER prosjektet), bundle-stien KUN når bundelen shipper
`cost-baseline.json` (`load_optional_cost_baseline`) — en pre-amendment-bundle er legitimt
uforankret, og det er dét som holder commons-goldenene byte-identiske. **Toleransen stopper ved
fravær:** en baseline som FINNES men er malformed raiser på BEGGE loaderne (å lese korrupt som
«ingen baseline» ville gitt en uforankret gate i forkledning — samme resonnement som `read_spend`).
**F8:** metode-cap-en slås opp i `METHOD_CAPS`-registeret (måletype→brøk, injiserbart), ikke mot
`energy_efficiency`-literalen — en andre metode er nå data, ikke en redigering av validatoren.
Format- og toleranse-semantikken er bestemt LOKALT (commons-amendmentet D-A pkt. 2 kom aldri, som i
S3.2); **D7-speiling ÅPEN.** Load-bearing MÅLT (`tests/test_s40_cost_baseline_loadbearing.py`), seks
mutasjoner alle røde: detach avstemmings-stagen · detach magnitude-toleransen · detach road-wiringen ·
detach bundle-wiringen · ignorer det injiserte cap-registeret · gjør den valgfrie loaderen tolerant.
- **Globalt token-tak håndheves FØR kall, aldri bare etterpå (S3.4, F10):** `PortfolioBudget` +
`PortfolioMeter` er ÉN ledger over hele porteføljepasset (og — seedet av `read_spend` — på tvers
av pass), mens per-run `Budget`/`TokenMeter` er uendret. Taket har tre tenner, med hver sin jobb:
(1) **oppstartsnekt** — en rest som ikke kan finansiere én kjøring raiser `BudgetRefused` FØR noe
lastes (et pass som har råd til null prosjekter er en caller-feil, ikke et resultat);
(2) **wave-assembly** — et prosjekt som ikke kan finansieres blir ALDRI STARTET, og passet stopper
strukturert (`budget_stop` + `stopped_early`, fullførte runs bevart). Aldri-startet er poenget:
et ufinansiert prosjekt som bare avbrytes har allerede kostet kall. Fordi hele bølgen sjekkes mot
SAMME før-bølge-rest, **reserverer** admission hver members krav — ellers overforplikter en bølge
av k taket med inntil k kjøringer; (3) **pre-call-guard** i `BudgetMiddleware` — et kall resten
ikke kan betale for NEKTES i stedet for å gjøres (post-charge-sjekken består: ekte usage kjennes
først etterpå, så guarden stopper NESTE kall, aldri det som er i lufta). `budget_stop` er et EGET
felt, aldri `stop_reason`: et mål-stopp er suksess, dette er ressurs-utmattelse — å slå dem sammen
ville gjort «vi stoppet» uleselig. `record`/`check` er SPLITTET i `PortfolioMeter` fordi tokens
leverandøren allerede har fakturert må nå ledgeren selv når samme charge bryter run-taket.
Spend-fila er vår EGEN regnskapstilstand: `read_spend` raiser på korrupt innhold (kontrast det
tolerante RAW-inbox-laget — å lese korrupt som null ville gitt tilbake et allerede brukt budsjett),
og `write_spend` tar et PÅKREVD `stamp` uten wall-clock-default (byte-determinisme, speiler
`promote_verdict`). `portfolio_meter` og `meter_factory` er gjensidig utelukkende — en
factory-meter er ubundet, så begge sammen ville gitt et pass som SER capped ut uten å være det.
Load-bearing MÅLT (`tests/test_portfolio_budget_loadbearing.py` + `tests/test_budget.py`), seks
mutasjoner alle røde: detach wave-sjekken · detach pre-call-guarden · detach bølge-reservasjonen ·
sjekk run-taket før global kreditering · detach oppstartsnekten · gjør `read_spend` tolerant.
- **`BudgetExceeded`s tre felt beskriver ÉN og samme ledger (kø-(y)):** `kind`/`limit`/`observed`
er ett strukturert stopp-event, og `observed` var det udefenderte tredje feltet — MÅLT: fire av
fem raise-steder (`TokenMeter.charge`, `tick_round`, og BEGGE armene i `exhausted()`) kunne
rapportere hvilken som helst verdi uten at 621 tester merket det; bare `PortfolioMeter.check`
var dekket. **Fellen som skjulte det:** `spikes/_harness.py:41` har sin EGEN kopi av
`BudgetExceeded`/`TokenMeter`, så `spikes/test_harness.py`s `observed`-assert dekker IKKE den
shippede modulen — produksjonens `tick_round` hadde null direkte test. `exhausted()` er det
ENESTE stedet som VELGER ledger (S3.4-guarden), så en refusal som sier `portfolio_tokens` mens
den rapporterer runets eget forbruk ville villedet enhver leser. **Testene bygges med
`observed != limit`**: ved nøyaktig-uttømt sammenfaller de to, og en test skrevet der kan ikke
skille dem — den ville passert på en implementasjon som ekkoet taket tilbake som forbruket.
Ingen defekt funnet i verdiene selv (kontrast (x)/(p)): trippelen var koherent alle fem steder,
gapet var rent dekning. Load-bearing MÅLT (`tests/test_budget.py`), ni mutasjoner alle røde:
fem `observed`-mutasjoner (inkl. kontrollen) + fire ekko-mutasjoner.
- **Pengetall kvantiseres i ÉN orden, fra ÉN kilde (kø-(p)):** `ledger.to_ore` er rammeverkets ENE
NOK→øre-konvertering (Decimal, ROUND_HALF_UP), og den brukes **per pengebeløp — deretter summeres
HELTALL**. `run.py` importerer den; aldri en egen kopi (to kopier av en penge-konvertering drifter,
og en driftet kopi setter de to sidene av en mål-sammenligning på hver sin skala — S4.0s
`REPLIES`-presedens). Før dette summerte `run.py`s mål-baseline `Project.total_cost`-FLOATS og
kvantiserte totalen ÉN gang, mens `SavingsLedger` summerte per-kandidat-heltall — og de to ordenene
møttes i nøyaktig ETT punkt: `_goal_limit_if_reached`, der et prosent-mål avgjør om et
porteføljepass stopper tidlig. **Målt divergens:** tre linjer à `60000.005` NOK er `18000003` øre
kvantisert først, men `18000001` summert først (float-drift til `180000.01499999998`) — nok til å
vippe et mål. **Kvantiser-først valgt fordi hver `CostItem` ER et beløp** (S4.0 gjorde per-linje
`quantity`/`unit_cost` til validatorens grunnsannhet), og fordi heltallsaddisjon er assosiativ →
rekkefølge-uavhengig under D-D-bølgemodellen, som float-folden ikke er. **To kallsteder, ikke ett:**
portefølje- og per-prosjekt-baselinen er separate, og en fiks på bare den ene OVERLEVDE hele suiten
(målt). Load-bearing MÅLT (`tests/test_money_quantization_loadbearing.py`), fem mutasjoner alle
røde: detach portefølje-baselinen · detach per-prosjekt-baselinen · gjeninnfør en privat kopi i
`run.py` · endre avrundingsmodus · la `realize` gå utenom `to_ore`. **Ærlighets-grense:**
`sum_claimed_saving_nok` (`run.py:_aggregate`) er BEVISST urørt — et float-NOK-rapportfelt som
aldri kvantiseres og aldri sammenlignes mot ledgeren, altså utenfor ordens-defekten.
- **Kostnadsdisiplin:** utvikle primært på lokal profil (gratis); Foundry/Azure (privat tenant finnes) kun til målrettet, minimal verifisering; billigste modeller + små syntetiske data + harde token-tak. Ingen tunge test-kjøringer.
- **Offline simulering = primært metode-bevis (kostnadsdrevet, erstatter §11.8):** operatøren kjører
IKKE MAF mot ekte modell (verken Azure/Foundry eller Ollama — API for begge repoene er for kostbart
privat). `portfolio_optimiser.simulation` driver `run_project` med en SKRIPTET syntetisk chat-klient
(`ScriptedChatClient``OpenAIChatCompletionClient` — IKKE bare `BaseChatClient`, ellers no-op-er
`BudgetMiddleware`) over to kjøringer adskilt av en promotering, og viser at læringssløyfa lukkes:
Run A's godkjente persona-dom (markør fraværende fra bundelen) → `promote_verdict` → re-seed → Run B's
hypotese-prompt bærer markøren (tom-wiki-kontroll på Run A beviser kausalitet). **Ærlighet (§1,
ufravikelig):** beviser plumbing + deterministisk ryggrad + at dataflyten lukkes — IKKE at en levende
LLM ville produsert forslaget/dommen (skriptede stand-ins). Den genuine modell-atferd-sammenligningen
lever på Claude-SDK-siden (minimal API-kjøring). Skriptet klient = MAF-side stillas, IKKE delt
(`shared/` forblir framework-nøytralt). Kjøres `uv run python -m portfolio_optimiser.simulation`.
Load-bearing: `tests/test_simulation_loadbearing.py` blir RØD når promoteringen detaches.
- **Demoen KJØRER begge tidsskalaer, og de bæres av HVER SIN markør (P1/S1.a):** Steg 7-linja sa
«lang fil-løkke», men `simulate_learning_loop` kalte `run_project` UTEN `verdict_dir` — dommen kom
som funksjonsargument (`verdict_input`, den KORTE i-kjøring-fangsten). Nå skriver en ekspert en
faktisk fil (`write_verdict`) i en innboks MELLOM kjøringene, og Run B får `verdict_dir=`.
**Hvorfor en ANDRE markør og ikke persona-dommen gjennom innboksen:** Steg 7 (innboks) og Steg 8
(promotering) er to ULIKE mekanismer som begge ender i Run B's hypotese-prompt — med én delt
markør kunne hver av dem båret den alene, og `test_simulation_loadbearing.py`s promoterings-assert
ville stått GRØNN med promoteringen detached, altså blitt vakuøs. `simulate_learning_loop` raiser
derfor `ValueError` når `inbox_marker == marker`. Innboksen ligger VED SIDEN AV bundle-kopien,
aldri inni: en dom-fil inne i bundelen når neste kjøring som navigerbar kontekst, som er en annen
mekanisme i denne sin forkledning. Sentinel-`id` (aldri myntet) — `_mint_id` hasher kandidat-
featurene, så en myntet id kolliderer med den promoterte dommens, og `VerdictStore.add` er
first-write-wins per id. Load-bearing MÅLT (`tests/test_step7_demo_inbox_loadbearing.py`), tre
mutasjoner røde + grønn kontroll: detach `verdict_dir=` · la Run B lese en TOM mappe · sett
innboks-markøren til en verdi som FINNES i bundelen.
- **Det skriptede manuset nøkles på PROSJEKT-ID-en, og det er MÅLT:** `scripted_proposer(candidates)`
bygger simuleringens proposer fra et `ScriptedCandidate`-register, så et nytt prosjekt er en
data-oppføring (demo-uke-plan §4 risiko 2) — ikke et andre håndskrevet manus. **Hvorfor ikke
kostkode/tiltaksnavn:** to prompt-former når selectoren — debatt-prompten bærer hele
bundle-konteksten, mens genererings-prompten (`generate._build_messages`) bærer
`Project: {id} - {name}` pluss *debatt-outputen* som kontekst, altså selectorens EGET tidligere
svar. Kostkode og tiltaksnavn står derfor i genererings-prompten kun fordi manuset selv la dem
der; å nøkle på dem ville nøklet manuset på sin egen output. Prosjekt-ID-en er den ene
identifikatoren BEGGE former bærer og som RAMMEVERKET stempler. **Validering, ALDRI reparasjon:**
null treff — eller mer enn ett — raiser `ScriptedCandidateError`; et default-svar ville besvart et
uregistrert prosjekt med et ANNET prosjekts tall, som på skjermen er umulig å skille fra en riktig
kjøring, og en tvetydig blob er et DATA-problem som skal falle på generalprøven, ikke avgjøres av
register-rekkefølgen. `flip_key` MÅ være fraværende fra bundelen (ellers bærer forsøk 1s prompt
den allerede). `simulate_learning_loop` tar `project_id` ved siden av `bundle_dir`. Load-bearing
MÅLT (`tests/test_content_keyed_script_loadbearing.py`), fem mutasjoner alle røde + grønn
kontroll: detach nøklingen · én global flip-key · fallback ved ukjent prosjekt · første-treff ved
tvetydighet · detach `project_id`-argumentet. **Flip-key-testen ble skrevet om under målingen**
første form asserterte på FØRSTE register-oppføring, der «den matchede kandidatens nøkkel» og
«`candidates[0]`s nøkkel» sammenfaller; den kunne ikke skille de to implementasjonene.
- **Demoen kjører FORANKRET, og baselinen DERIVERES fra manuset (P4 pkt. 0):** før dette regnet
validatoren i demoen kun på tall forslaget selv oppga — S4.0-forankringen aktiveres bare når
kunnskapsbasen shipper `cost-baseline.json`, og ingen bundle under `shared/` har den. Reserven kan
aldri få fila DER (pull-only subtree + kriterium 8 krever goldenene byte-uendret), men det er en
*plasserings*-begrensning: `materialize_anchored_bundle` KOPIERER bundelen og legger fila til
utenfor `shared/`, og kjørestien (`run.py``load_optional_cost_baseline`) er da NØYAKTIG samme
søm en levert bundle ville brukt. **Retningen på avledningen er bærende:** reservens tall er
syntetiske, så manus-registeret er eneste grunnsannhet — `baseline_from_scripted_candidate`
avleder i KODE, aldri en andre håndskrevet kopi av de samme tallene (to kilder drifter, og drift
er nøyaktig det 10 %-prøven modellerer). På GO-dagen snus retningen (plan P3 b: registeret skrives
FRA levert fil). Begge skriptede svar må oppgi SAMME kostlinjer (`ValueError` ellers): var de
ulike, ville hypotese #1 blitt avvist av stage 0 istedenfor av P90 — samme REJECTED-linje på
skjermen, annen mekanisme. **Forankringen er usynlig i alt annet stdout** (målt: eneste diff mot
uforankret er KUNNSKAPSBASE-blokka), derfor printes den erklærte baselinen, og derfor er
`provenance` et PÅKREVD argument til `_baseline_lines` — kallstedet som velger bundelen er det
eneste som vet hvor tallene kom fra. Load-bearing MÅLT
(`tests/test_anchored_reserve_loadbearing.py`), fem mutasjoner røde + grønn kontroll: detach
main-wiringen · detach fil-skrivingen · la filnavnet drifte · detach to-svars-enigheten · returner
et literal i stedet for det avledede. **Målingen felte TESTEN først** (samme klasse som 08-06):
«ingen kostbaseline erklært» INNEHOLDER «kostbaseline erklært», og `ENERGI-TOTAL-EL` står allerede
i Steg 2-linja — begge assertene overlevde detach-mutasjonen. De to grenene deler nå ingen ordlyd.
- **Demoens stderr: rund-taket dempes, `ExperimentalWarning`-paret PINNES (P4 pkt. 2):** målt 08-09
var stderr seks linjer. `quiet_expected_round_cap_notice()` dropper KUN
«reached max_rounds=…; forcing completion» — en hendelse demoen selv provoserer (maker/checker
kjører til taket) — via et filter på den EMITTERENDE loggeren (`ROUND_CAP_LOGGER`, lest ut av MAFs
kilde). Logger-filtre gjelder kun loggeren posten ble logget GJENNOM; en forfars filtre konsulteres
aldri. Filteret installeres i `main()`, ALDRI ved import — en bibliotek-modul skal ikke
omkonfigurere loggingen til en konsument. **De to `ExperimentalWarning`-linjene dempes IKKE:** de
fyrer mens `portfolio_optimiser/__init__.py` importerer `run``agent_framework`, altså alltid FØR
`simulation` sin egen importblokk, under BEGGE kjøreformer — så å dempe dem ville krevd et
warnings-filter inne i bibliotekpakken, dvs. at rammeverket bestemmer hva MAF får si til enhver
konsument. En wrapper bak konsoll-kommandoen ble avvist av en andre grunn: da ville de to
kjøreformene skrevet ULIK stderr, og en byte-fasit ville pinnet kommandoen i stedet for programmet.
**Dempingen er smal ved konstruksjon** — nøklet på meldingen, ikke loggeren — nettopp så pkt. 3-pinnen
fortsatt kan felles av en NY advarsel. Load-bearing MÅLT
(`tests/test_demo_stderr_quiet_loadbearing.py`), fem mutasjoner røde + grønn kontroll: detach
`main()`-kallet (subprosess-testen er ENESTE som fanger det — de tre filter-testene installerer
filteret selv) · la filteret droppe alt · installer ved import · pluss de to entry-point-mutasjonene.
- **Demo-transkriptet er sjekket inn som fasit, og masken er SPANN-avgrenset (P4 pkt. 3):**
kriterium 6 er selv-identitet — to kjøringer av en REGREDERT demo er like enige som to kjøringer av
en riktig, så fasiten må forlate prosessen. `tests/golden/demo-transcript.stdout` er stdout ORDRETT
(målt byte-identisk over kjøringer OG i fersk klon), og er derfor også demoens abortsti: feiler
live-kjøringen, ER fila transkriptet. `…​.stderr` er normalisert på nøyaktig to MÅLTE miljø-spann —
`site-packages`-prefikset og temp-katalogen bak `(arbeidskopi: …)`, der `po-sim-`-prefikset holdes
SYNLIG fordi det er en egenskap ved programmet (`mkdtemp(prefix=…)`), ikke ved miljøet; pinnet
stderr er fire linjer. **Masken må ikke kunne vokse:** P4 pkt. 2 betalte for at en NY advarsel
fortsatt når stderr, og en normalisering som maskerte hele linjer ville opphevet det i ett trekk —
derfor er `test_normalisation_does_not_mask_a_new_warning` kontrollen som forbyr det (MÅLT: en
droppende normaliserer med fasiten regenerert under seg holder BEGGE likhets-testene grønne og
felles kun av kontrollen). `PYTHONIOENCODING` pinnes, ellers måler sammenligningen operatørens
locale i stedet for programmet. **Regenerering er en beslutning, aldri rydding** — fasiten kan ikke
bevise sin egen kjøring, den pinner outputen P3-kriteriene ble målt mot.
- **Frø-setningen AVLEDES fra kjøringen (P4 pkt. 4):** demoen sier høyt hvor Kjøring B's tidligere
dommer kommer fra, og splitten (`_verdict_origin_line`) regnes ut — linja rett over printer allerede
antallet, så en håndskrevet «én av tre» ville vært den andre kopien som drifter (samme regel som
pkt. 0-baselinen), og ville blitt sagt uendret etter at en framtidig bundle shipper en ANDRE frøsatt
dom. Klassifisereren er de to markørene demoen alt sporer; den hviler på at den frøsatte dommen
bærer INGEN av dem, som MÅLES på levert bundle. **Planens forhåndsskrevne ordlyd var FEIL mot
levert innhold** («én av de TO») — målt henter Kjøring B TRE: én fulgte med kunnskapsbasen, to er
demoens egne (én per tidsskala). Load-bearing MÅLT
(`tests/test_p4_honesty_sentences_loadbearing.py` + golden-transkriptet), fem mutasjoner alle røde
+ grønn kontroll: ett byte i en stdout-linje · detach dempingen · over-normaliser stderr · literal
splitt (fanget av INGENTING i 800 tester bortsett fra skille-testen) · detach frø-setningens print.
- **Delt ekspert-persona som Agent Skill (§8, framework-nøytral):** ekspert-reviewer-personaen bor i
`shared/skills/expert-reviewer/` (`SKILL.md` + `references/example-verdict.json`) og er den ENE
delte artefakten begge stacker instansierer reviewer-en fra. `shared/` forblir REN DATA — MAF-siden
leser den via `portfolio_optimiser.persona.load_persona_example` (call-time, fail-fast), Claude-SDK-
søskenet med sin egen loader mot samme JSON. Dette AV-STUBBER simuleringen: persona-dommen (decision
+ rationale + sporet markør) hentes nå fra artefaktet ved call-time, ikke en inline-literal — så
personaen er genuint konsumert og kan ikke råtne stille. **Decision er binær** (`approved`/`rejected`
`FeedbackContract` run-stien tar; `approved_with_adjustment` avvises der, bor kun i bundle-seedens
frontmatter + promoterings-gaten); realiseringskorreksjonen lever i rationale-prosaen, ikke et tredje
enum. SKILL.md-prosaen nevner ALDRI en konkret framework (maf-guarden er import-formet). Load-bearing-
trio (`tests/test_persona_skill_loadbearing.py`): struktur+framework-nøytralitet (RØD på framework-
import), eksempelet er gyldig pipeline-input inkl. `FeedbackContract` (RØD på skjema-/kontrakt-drift,
på en throwaway-kopi — aldri den git-tracked fixturen), og sim-ens markør følger artefakt-fila (RØD i
det øyeblikk personaen re-inlines).
- **STATE.md er local-only** (gitignored). Voyage session-state er efemert; STATE.md er kanonisk kontinuitet.
- Prosess: Voyage-plugin (`/trekbrief → /trekplan → /trekexecute → /trekreview`) per større fase.
## Communication patterns
When linking to local files in responses, use named markdown links — `[Human-friendly name](file:///absolute/path)`, never bare `file://` URLs or autolinks `<file://...>`, always absolute paths (never `~/` or relative), one bullet per file when there are several. (Bare `file://` URLs render only the first as clickable across multiple lines; named links stay independently clickable.)