portfolio-optimiser/CLAUDE.md
Kjell Tore Guttormsen 63eec917d2 feat(4b): AZURE-profilen leser miljøet sitt, ikke operatørens laptop
Endepunktet løses som første ikke-tomme av vårt eget
PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT og Foundrys injiserte
FOUNDRY_PROJECT_ENDPOINT — vårt vinner, fallbacken lar samme image kjøre
hostet uten ekstra wiring. Presedensen gjelder verdier, ikke deklarasjoner.
Credential velges av samme miljø: AzureCliCredential lokalt,
ManagedIdentityCredential når FOUNDRY_HOSTING_ENVIRONMENT er satt, fordi
containeren ikke har noen Azure CLI. Ikke DefaultAzureCredential — Learns
MAF-veiledning navngir den spesifikke credentialen for å unngå probing.

Load-bearing målt mot hele suiten, fire mutasjoner alle røde + grønn
kontroll: detach credential-valget · presence i stedet for truthiness ·
detach fallbacken · snu presedensen. Fail-fast-testen var vakuøs først —
vårt variabelnavn inneholder det injiserte som delstreng.

De fire åpne azure.yaml-valgene lukket mot de to JSON-skjemaene og ført i
docs/2026-08-13-fase4-azure-yaml-valg.md. Ingen azure.yaml skrevet (4d).

821 passed / 4 skipped. Ruff + format + mypy rene.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jk8tauXXAojNKC7Tzq7ziF
2026-08-13 22:27:21 +02:00

44 KiB

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 (§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.
  • 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.
  • 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).
  • 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.)