portfolio-optimiser/CLAUDE.md
Kjell Tore Guttormsen bb4807aff7 fix(1b): regelen gjelder HVER betalt arm - fan-out-testen var fortsatt ugatet [skip-docs]
Forrige commit skrev en CLAUDE.md-invariant om at en betalt test far sin egen
opt-in, men gatet bare den NYE testen. test_portfolio_live.py passerer ingen
client_factory og er derfor selv en betalt kjoring - den fyrte pa
to-variabel-paret fra et bart `uv run pytest`. Invarianten var altsa halvt
usann den dagen den ble skrevet, som er nyaktig Fase 3-klassen (en pastand
flaten gjor om seg selv uten dekning).

MALT etter fiksen, med begge Foundry-vars satt og UTEN PORTFOLIO_LIVE_FULL_RUN:
  test_foundry_profile_live   PASSED   (det billige trinnet, med vilje pa to vars)
  test_portfolio_live         SKIPPED  (var betalt og ugatet for dette)
  test_full_run_live          SKIPPED

Ovrige oppfolgingspunkter fra review:
- assert_full_run_contract far ekte type hints (repoets "type hints overalt";
  mypy src ser ikke tests/, sa den ville rotnet ubemerket).
- Maleprotokollen SS6 far oppsettet STATE peker pa - model-map-formen og de fire
  export-linjene. Pekeren lovet noe seksjonen ikke hadde.

869 passed / 5 skipped, uendret. ruff+format+mypy rene.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GF7va4cpRiuf79kTzAi3vW
2026-08-14 20:12:47 +02:00

66 KiB

portfolio-optimiser

Note for visitors — what this file is. This is the working agreement between the repository and the AI coding agent that builds it (the Claude Code convention), and it is written in Norwegian because that is the maintainer's working language. It doubles as the repository's invariant ledger: each block below records a design decision, the measurement that forced it, and the test that turns red when the decision is undone.

You need none of it to use the framework — start with the README. It is published anyway, because the reasoning behind a decision is worth more than the decision, and because a rule kept out of sight is a rule that drifts without anyone noticing.

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 (§15 implementeringsregister), plan. 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 (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_tools 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_errors 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.pyhosting.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.servers 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_portfolios 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_projects 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_projects egen default forblir LOCAL). Feilmapping ærlig: ValueError (pydantic-kontrakter subklasser den) → 400, BudgetExceeded → 429 (EGEN rad under, 14.08), 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). Container-innpakningen (Dockerfile/azure.yaml) ER FJERNET 14.08 — se python-only-invarianten under; resten av denne raden står, for main.py startes nå direkte (python main.py). 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). Chunked request-bodies støttes ikke, og under CPU-bundne strekk (CBC-solven) står readiness — uttalt, ikke skjult.
  • Whitelisten må komponere med den EKTE run_project, og artefaktene gates som RÅ TEKST (Fase 4e): alle 4d-testene ga invoke en stand-in som sluker **kwargs, så whitelisten kunne navngi et felt run_project ikke tar — eller sende samme argument to ganger — uten at én test merket det, mens en levende container svarte 500. Sømmen er run._default_factory, ikke payloaden: client_factory NEKTES av whitelisten med vilje (en kaller av en hostet agent skal aldri velge serverens modellklient), så å patche factory-defaulten er eneste injeksjonspunkt flaten etterlater (samme argument test_run_cli_loadbearing gjør for main()). Testen sender HVERT whitelistet felt og asserterer dekningen mot _ALLOWED_FIELDS, så et felt lagt til senere ikke kan gli forbi uøvet. Profilen er LOCAL, ikke den hostede defaulten: AZURE-armen slår opp et Foundry-deployment-navn i modell-mappet FØR noen klient bygges (run.py stempler provenance med det), så den kan ikke fullføre offline — containeren trenger altså PORTFOLIO_MODEL_MAP eller et utfylt data/model_map.json, ikke bare et endepunkt (målt her, ikke antatt). Artefakt-halvdelen av denne raden er PENSJONERT 14.08 sammen med Dockerfile/azure.yaml (rå-tekst-gaten pinnet --platform linux/amd64 + ÉN kopi av startkommandoen; to av radens fem mutasjoner traff nettopp den). Whitelist-halvdelen står URØRT. Load-bearing MÅLT (tests/test_hosting_loadbearing.py), de tre gjenværende mutasjonene alle røde på riktig test og på INGEN annen: send project_id to ganger · whitelist et felt run_project ikke tar · fjern bundle_dir fra whitelisten.
  • Et tak som fyrer er IKKE en krasj — BudgetExceeded får sin EGEN kanal (429), og trippelen bæres som STRUKTUR (1b-køen, 14.08): prosjektets første levende kjøring døde på rounds limit=12 observed=13, og den hostede flaten svarte 500 {error_type, error} — altså nøyaktig det samme den sier når modell-endepunktet faller. Beslutningen er S3.4-invarianten anvendt på transporten: budget_stop ble holdt UTENFOR stop_reason fordi de to stoppene betyr motsatte ting, og å svare ressurs-utmattelse på krasj-kanalen gjør «det gikk ikke» uleselig på nøyaktig samme måte. IKKE 200, og det er dét som skiller den fra Rejection: en Rejection er en kjøring som KONKLUDERTE (og hører derfor i payloaden), mens et uttømt budsjett produserte ingen proposal i det hele tatt — en 2xx ville latt en automatisk kaller bokføre «analysert» for en kjøring som analyserte ingenting. 429 fordi betingelsen oppstår av en TILDELING (max_rounds/max_tokens er whitelistede request-felt, og å heve dem er kallerens egen botemiddel), aldri av en serverfeil — derfor 4xx, ikke 5xx. kind/limit/observed legges ut som felt, ALDRI str(exc) (kø-(y): de beskriver ÉN ledger, og «hvilket tak bandt, og hvor langt forbi» er hele det operative spørsmålet); error_type holdes UTE — den nøkkelen tilhører feilkanalen, og en kaller som switcher på dens tilstedeværelse skal ikke finne den her. budget_exhausted er IKKE foldet inn i outcome_type, og kunne ikke vært det: outbox.outcome_payload er den ENE kopien av den forgreningen og tar ValidatedProposal | Rejection, som en uttømt kjøring ikke har noen av. Ærlighets-grense, uttalt: ingen Retry-After — å vente endrer ingenting, botemiddelet er et større tak eller å akseptere stoppet, og en header som lover tid ville vært en løgn. Load-bearing MÅLT (tests/test_hosting_loadbearing.py), fem mutasjoner alle røde mot HELE suiten, hver med sin egen signatur + grønn kontroll 867/4: detach armen (2 røde) · flat streng i stedet for struktur (1 rød — struktur-testen ALENE, altså rir den ikke på status-asserten) · ekko limit som observed (1 rød) · utvid armen til Exception (6 røde, inkl. 400-armen) · stemple error_type på budsjett-kroppen (1 rød). 500-armens vitne ble byttet, ikke slettet: den eksisterende testen brukte BudgetExceeded som sin 500-prøve, så å bare legge til en ny arm ville etterlatt krasj-kanalen uten vitne — den bærer nå en ekte ikke-budsjett-RuntimeError, og er dét som holder den nye armen SMAL.
  • Påstander flaten gjør om SEG SELV gates som rå tekst, linjeforankret (Fase 3, A5): to påstander bodde i prosa der ingen test kunne se dem, og begge drev. (1) env.template sa at credential resolves via DefaultAzureCredential — den har ALDRI gjort det; gaten leser de klassene backends.py faktisk konstruerer fra selve tilordningslinja, ikke fra modulen, fordi kommentarene NAVNGIR DefaultAzureCredential fire ganger for å begrunne hvorfor den ikke brukes — en fil-bred substring-gate ville vært rød på nøyaktig den prosaen den beskytter (repoets 08-09-klasse, fjerde gang). (2) README-ens wheel-filnavn bærer versjonen bygget stempler på fila, så en versjonsbump ville stille etterlatt en publisert install-kommando som peker på en fil som ikke finnes. Hver positiv assert er paret med en KONTROLL på at det søkes etter noe som finnes — en ekstraktor som stille finner null lager en gate som bare kan bli grønn. Load-bearing MÅLT (tests/test_public_surface_claims_loadbearing.py) mot HELE suiten, begge røde på riktig test og på INGEN annen: gjeninnfør credential-påstanden (2 røde, 844 grønne) · la wheel-filnavnet drifte (1 rød, 845 grønne). Bumpen selv var den tredje målingen — pyproject 1.0.0 → 1.1.0 gjorde README-gaten rød alene, FØR README ble rettet. repo-standard-gaten kan IKKE verifisere denne fasen: den var OK/20 sjekker før arbeidet startet, og RELEASE-STALE er strukturelt blind for repo med null utgivelser (org-ops hovedbok #18). Bevisene er Forgejo-APIet, filinnholdet og ren-klon-kjøringen.
  • GOVERNANCE er en LENKE, aldri en kopi (org-ops D11): én kanonisk GOVERNANCE.md bor i repo-standard og hvert repo lenker den fra README. Å skrive vår egen ville gjort oss til kopi nr. 12 av en fil D11-bølgen holder på å rydde vekk. Bus-faktor 1 står uttalt i den kanoniske teksten, ikke i vår.
  • 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_llms 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_dirstore.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_projections 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_summarybundle_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 (3000030000.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.
  • BudgetExceededs 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.pys 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.pys 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.
  • En BETALT test får sin EGEN opt-in, og instrumentet bevises GRATIS (Fase 1b, siste trinn): tests/test_full_run_live.py kjører hele run_project-stien mot et ekte Foundry-deployment, og gates på fire ting — de to Foundry-variablene, PORTFOLIO_MODEL_MAP, og et TREDJE, distinkt PORTFOLIO_LIVE_FULL_RUN lest på truthiness (4b-invarianten). Den tredje variabelen er load-bearing, ikke pynt: test_foundry_profile_live.py (klient-probe) og test_portfolio_live.py (fan-out) gatet på nøyaktig SAMME to variabler, så å gjenbruke det paret ville betydd at en operatør som eksporterer dem for den BILLIGE ett-ords-proben også fyrer den dyre fullkjøringen — altså at måleprotokollens stige («bevis så mye som mulig før det dyre trinnet, så en feil er attribuerbar») kollapser til ett trinn. MÅLT: med begge Foundry-variablene satt SKIPPET den dyre, og den billige var grønn. Regelen gjelder HVER betalt arm, ellers er den ingen regel: test_portfolio_live.py passerer ingen client_factory og er derfor selv en betalt kjøring — den fyrte på to-variabel-paret fra et bart uv run pytest, og ble gatet på den TREDJE variabelen i samme slengen. Å la den stå ville gjort denne raden halvt usann den dagen den ble skrevet; en invariant som beskriver én av to armer er en påstand flaten gjør om seg selv uten dekning, som er nøyaktig Fase 3-klassen. Den billige klient-proben beholder to-variabel-gaten med vilje — den ER det billige trinnet. PORTFOLIO_MODEL_MAP er med av en annen grunn — attribusjon: uten den feiler kjøringen av en KONFIGURASJONS-årsak som ser ut som en modell-feil. Asserten bor i ÉN kopi (conftest.assert_full_run_contract, kø-(p)) og er smal med vilje: fraværet av {run_id}-parse-failures.json (økt 35-invarianten «filens tilstedeværelse er signalet») + at validator_decision avgjorde. En rejected BESTÅR — påstanden som felles er at det strukturerte skjemaet ER akseptert av det levende endepunktet, ikke at modellen resonnerer godt; å kreve validated ville vært en modell-dømmekraft-påstand ingen enkelt kjøring kan bære. Iron Law uten å betale to ganger: et betalt kall kan ikke kjøres rødt-så-grønt, så diskrimineringen bevises OFFLINE av tests/test_live_full_run_contract.py — to armer over samme helper (én parse-feil → kontrakten MÅ feile; alle parser → MÅ passere). Load-bearing MÅLT, to mutasjoner med hver sin distinkte signatur: detach artefakt-sjekken (T1 rød ALENE — kontrakten degraderer da til test_portfolio_live.pys len(runs)==1-klasse) · raise ubetinget (T2 rød ALENE — den motsatte vakuiteten, en live-test som bare kan bli rød). Det betalte kallet er MÅLINGEN, aldri beviset på at måleinstrumentet virker.
  • 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 (ScriptedChatClientOpenAIChatCompletionClient — 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.pys 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.pyload_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 runagent_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/rejectedFeedbackContract 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).
  • Den råe svarteksten fanges i en KALLER-EID SINK, ikke i en returverdi (Fase 1b, funn 1): generate._fetch_parsed kastet hvert uparsebart modellsvar i except: continue, så prosjektets første levende kjøring brant tolv runder på formatfeil og etterlot null tegn av det modellen faktisk sa — enhver videre betalt kjøring ville vært gjetning. HVOR teksten overflates er avgjort av en MÅLING, ikke av symmetri med Steg 5: meter.tick_round() raiser BudgetExceeded INNE i _fetch_parsed, og uten mandat fanger ingen den (run.pys ene except BudgetExceeded er mandat-armen) — så på nøyaktig den stien fangsten finnes for, RETURNERER generate_via_llm ingenting. Et felt på GenerationResult (Steg 5-formen) er derfor blindt for den, og et outbox-artefakt skrevet ETTER kjøringen likeså. Sinken speiler i stedet meter: en kaller-eid akkumulator løkka muterer, hvis innhold kalleren holder uansett hvordan løkka endte. Steg 5s «returverdi, ikke out-parameter» gjelder en verdi som NÅR kalleren; her gjør den ikke det, og å kopiere regelen blindt ville gjenoppbygd defekten ett lag opp. Artefaktet {run_id}-parse-failures.json skrives fra en finally, ikke except BudgetExceeded — enhver exception ut av genereringen ødelegger samme bevis, og en liste over exception-typer er en liste som blir foreldet. Teksten er VERBATIM (en forkortelse gjør beviset om til en parafrase), og fila skrives KUN når noe faktisk feilet, så dens tilstedeværelse ER signalet. Byte-determinisme påstås IKKE for dette ene artefaktet — innholdet er en levende modells prosa. Load-bearing MÅLT (tests/test_parse_failure_capture_loadbearing.py), seks mutasjoner alle røde mot HELE suiten + grønn kontroll 859/4: detach fangsten (3 røde) · flytt skrivingen ut av finally (1 rød, KUN budsjett-testen) · detach run-wiringen (2 røde, generate-testen grønn) · skriv artefaktet alltid (kontrollen + den eksisterende a5-inerthetstesten) · trunker teksten til 40 tegn (3 røde) · trunker til 100 tegn slik at sentinelen OVERLEVER (1 rød — verbatim-asserten alene, den skarpe diskriminatoren). Ærlighets-grense: _charge_usage kan raise FØR parse, og et svar tapt der er ikke en parse-feil og fanges ikke.
  • Proposeren får en GRAMMATIKK, og skjemaet er DERIVERT + fail-closed (Fase 1b, funn 1b): generate_via_llm sender options={"response_format": proposal_response_format()} på hvert genererings-kall. Formen er MÅLT, ikke valgt: ChatOptions.response_format tar type[BaseModel] | Mapping, og BEGGE profiler ærer den — LOCAL (OpenAIChatCompletionClient) sender en Mapping ordrett til Chat Completions, AZURE (FoundryChatClientRawFoundryChatClientRawOpenAIChatClient) konverterer SAMME envelope til Responses-APIets text.format. Klassen er AVVIST på bevis: gitt en klasse konverterer klienten med type_to_response_format_param, som (målt) emitterer minimum / exclusiveMinimum / minItems / prefixItems og et assumptions-node hvis additionalProperties er et SKJEMA — fire ting Azures publiserte subset utelukker (Learn: «Unsupported type-specific keywords» + additionalProperties: false i hvert objekt). Vår egen mapping er eneste måte å styre hva som når tråden. Å stripe beskrankningene koster ingenting: skjemaets jobb er FORM, validatorens jobb er VERDIER — minItems/gt=0 gjenreises av pydantic i _parse_ir og av validate_proposal. Skjemaet DERIVERES fra SavingsProposal (strict_json_schema), aldri håndskrevet: en andre kopi av en form som alt bor i ir.py drifter stille, og modellen ville fortsatt blitt bestilt for den gamle. assumptions KAN IKKE bare droppes, og det er en MÅLING: feltet er det ene uttrykksløse (fri-form map av 2-tupler), men validator._monte_carlo faller tilbake på item.unit_cost for hver kode uten bånd — uten bånd i det hele tatt er alle 512 samples IDENTISKE og P10 == P50 == P90. Den stokastiske falsifisereren ville gått inert mens den fortsatt rapporterte persentiler: repoets kardinalklasse (en gate som bare kan bli grønn). Derfor bærer WIRE-en et array av navngitte entries og _parse_ir folder det tilbake til IR-ens map — additivt, aldri erstatning (map-formen parser uendret; alle scriptede svar i suiten og golden-transkriptet bruker den). Sanitiseren er fail-closed (StructuredOutputUnsupported) på prefixItems/oneOf/allOf/fri-form map uten deklarert override — validering, ALDRI reparasjon (speiler write_concept_file). Prompt-linja «Respond with ONLY a JSON object» + parse-retry + funn-1-fangsten står URØRT: en leverandør som ignorerer response_format må fortsatt få beskjed, og backstoppen er poenget. Load-bearing MÅLT (tests/test_structured_output_loadbearing.py), seks mutasjoner alle røde + grønn kontroll 864/4: detach wiringen (1 rød) · detach sanitiseren (3 røde) · dropp assumptions fra skjemaet (1 rød) · fail-closed → stille reparasjon (1 rød) · detach normaliseringen (3 røde) · erstatning i stedet for tillegg (2 røde — T5 PLUSS golden-transkriptet, et uavhengig vitne). T3 ble skrevet VAKUØS først (repoets 08-09-klasse, sjette gang): den påsto å bli rød når assumptions forsvant fra skjemaet, men den scriptede klienten ignorerer skjemaet — påstanden ble bevist usann av M3 og testen fikk en DIREKTE assert på skjemaet. ÆRLIGHETS-GRENSE, UTTALT: ingen betalt kjøring er gjort, så at det emitterte skjemaet ER akseptert av det levende endepunktet er IKKE verifisert — testen beviser konformitet med det DOKUMENTERTE subsettet, ikke aksept. Ollamas oppførsel på response_format er likeledes uverifisert.
  • Overleverings-pakka ER git archive HEAD, aldri en kuratert kopi (Fase 5): scripts/make-handover-package.sh bygger én zip en ekstern organisasjon deployer uten å klone repoet. Tracked files only er hele eksponerings-kontrollenSTATE.md, *.local.md og .env er gitignorert, så de KAN ikke komme inn; et filter vedlikeholdt i skriptet ville vært den andre kopien av den regelen, og den andre kopien er den som drifter (kø-(p)). Mottakeren får altså HEAD selv. Versjonen LESES fra pyproject.toml — et hardkodet tall her ville råtnet ved neste bump nøyaktig som README-ens wheel-filnavn gjorde (Fase 3). DEPLOY.md ligger i treet og blir dermed med i arkivet av seg selv; den bærer mottakerens tre første spørsmål — hvem gjør hva (plattform-operatør / bestiller / fagperson), prosessen ende-til-ende, og hvorfor det ikke finnes et chat-grensesnitt (flaten er POST /invocations, og as_agent() er bevisst vraket fordi validator, baseline-forankring, checker-gate og ledger ligger UTENFOR grafen — et chat-lag ville rutet forespørsler rundt nøyaktig det som gjør svaret etterprøvbart). Den navngir også det 4e målte deploy-kravet som ingen rad hadde skrevet ned: pakket model_map.json bærer REPLACE-WITH-*, så uten PORTFOLIO_MODEL_MAP starter tjenesten, svarer på /readiness og feiler HVER invocation. Gaten er tests/test_handover_package_loadbearing.py, og DEPLOY.md-asserten er LINJEFORANKRET: PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT INNEHOLDER FOUNDRY_PROJECT_ENDPOINT, så en delstreng-assert på det injiserte navnet ville vært oppfylt av vårt eget (repoets 08-09-klasse, femte gang).
  • Overleveringen er KUN kjørbar Python, og fraværet er FJERNING — ikke filtrering (14.08, operatørdirektiv etter ekstern test): Dockerfile og azure.yaml er slettet fra TREET. Sømmen er valgt av den eksisterende invarianten, ikke av smak: pakka ER git archive HEAD, så å ekskludere filene fra arkivet ville krevd en kurerings-mekanisme (skript-filter eller export-ignore) — den andre kopien av «hva mottakeren får», fri til å drifte fra HEAD, altså nøyaktig kø-(p)-regelen raden over finnes for. Å beholde dem som «opt-in» ville ikke oppfylt direktivet i det hele tatt. Fjerning holder arkivet ukurert OG gjør fraværet til en egenskap ved HEAD, som er det eneste en gate kan måle. De to gatene som pinnet flaten er håndtert BEVISST, aldri stille svekket: 4e-rå-tekst-gaten (--platform linux/amd64 + ÉN kopi av startkommandoen) er SLETTET med et notat der den sto — en gate som pinner en fjernet flate kan bare bli grønn — og handover-gatens _REQUIRED_MEMBERS er ikke bare fratatt de to navnene, men erstattet av en POSITIV fraværs-assert; å kun slutte å KREVE dem ville gitt en gate som ikke kan skille «fjernet» fra «shippes fortsatt». Matchingen skjer på arkiv-MEDLEMSNAVN, ikke på prosa (dokumentene må kunne forklare at ingen image shippes — repoets 08-09-klasse, sjette gang), og dokument-gaten forbyr kommando-FRAGMENTER (docker build, azd deploy), ikke ordet. Startkommandoen har nå ÉN kopi igjen — DEPLOY.md-ens python main.py — og den navngir inngangen subprosess-testen faktisk kjører. Ærlighets-grense, uttalt: azd/hosted-agent-stien finnes ikke lenger i pakka; hvordan prosessen driftes er mottakerens valg. git archive HEAD leser HEAD, ikke arbeidstreet, så gaten er ekte men forsinket med én commit (funn 35). Load-bearing MÅLT (tests/test_handover_package_loadbearing.py).
  • 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.)