Two commands are now part of the install surface a fresh clone gets from `uv sync`: `portfolio-optimiser` (run:main) and `portfolio-optimiser-demo` (simulation:main). Deliberately two of five main()s — costsim/hitl/preflight stay module-invoked; every name here is a name the freeze has to carry. Pinned against the INSTALLED distribution's metadata, not the TOML: a [project.scripts] line that has never been synced is a claim, not a command. Measured: stdout is byte-identical across both invocation forms. stderr (P4 pkt. 2), the session's open decision, resolved by measurement rather than by preference. Damped: the round-cap notice only, via a filter on the emitting logger, keyed on the message and installed by main() — never at import, so a library consumer keeps its own logging config. NOT damped: the two ExperimentalWarnings. They fire while the package __init__ imports run -> agent_framework, always before simulation's own imports and under both invocation forms, so silencing them would mean filtering warnings inside the library package on every consumer's behalf; they are pinned in pkt. 3 instead. A console-script wrapper was rejected for a second reason: the two forms would then write different stderr, and a byte-fasit would pin the command rather than the program. stderr 6 -> 4 lines. A first implementation wrapped simulation's own agent_framework import in a scoped mute. Measurement showed it can never fire — the package __init__ has already imported agent_framework by then — so it was removed rather than left as a green-but-dead seam. Load-bearing MEASURED against the whole suite, five mutations all red + green control: remove [project.scripts] · typo the target · detach the main() call · make the filter drop everything · install the filter at import time. The typo mutation also felled a test: the resolve-assert re-checked the expected constant against itself, and now resolves what the distribution actually installs. 775 -> 785 passed / 4 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C2bxLcCRguxXzpM4priTMn
36 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 (
mypyder mulig). Pydantic for validering/IR. rufffor lint+format.pytestfor 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
FunctionTooler 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 fraingest_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 avportfolio-optimiser-commons(source of truth, R1 realisert 2026-07-03; publisert iopen/2026-08-04 —commons-remoten peker fortsatt påktg/og virker uendret). Synk er pull-only: endringer committes i commons og hentes medgit subtree pull --prefix=shared commons main --squash. ALDRIgit subtree pushfra konsument — re-split lekker hele konsument-historikken inn i commons (observert + opprydd 2026-07-03). Seshared/README.md.
Kommandoer
- Sync:
uv sync— installerer to konsoll-kommandoer:portfolio-optimiser(CLI,run:main) ogportfolio-optimiser-demo(offline-beviset,simulation:main).python -m-formene virker uendret og er byte-identiske på stdout (målt). Bevisst KUN to av femmain()—costsim/hitl/preflighter operatørverktøy, ikke produktets inngang, og hvert navn her er et navn frysen må bære. Pinnet avtests/test_console_entry_points.pymot den INSTALLERTE distribusjonens metadata, ikke mot TOML-en: en[project.scripts]-linje som aldri eruv 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 delteshared/-kjernen er ren stdlib — nullagent_framework/mcp-import, så samme bundles konsumeres uendret av begge stacker (D7-portabel). Håndhevet avtests/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 itest_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_bundlefø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_resolveer den ENESTE inn-/ut-av-bundle-testen (fail-closed) — den erstattet den pensjonerte «separator = utenfor bundelen»-heuristikken, som forvekslet dybde med escape. Manglendeindex.mder feil KUN i bundle-rota (navigasjon følger lenker, aldri katalog-enumerering). Rendering er FLAT uansett dybde; nestedeindex.mder 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_fileer repoets ene authoring-primitiv som materialiserer en konseptfil fra CALLER-oppgitt frontmatter, og avviser derfor det KOMPLETTE eierskaps-stempelet (generated: true+ingest_manifest) medIngestStampError— 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. IngestErrormå overleve anyio-task-gruppene (kø-(x), 2026-08-03):stdio_clientogClientSessioner hver sin task group, og anyio pakker ALT som forlater en av dem i enBaseExceptionGroup. Derfor nåddestdio_call_tools egne feil (mcp_tool_error,mcp_non_text_content) kalleren som en exception group — aldri somIngestError, som er typen hele Door A fanger og switcher på viacode._unwrap_ingest_errorpakker ut og re-raiser den eide feilen; alt annet re-raises URØRT (innsnevring, aldri blanket re-raise). Duck-typet på.exceptions, fordiexcept*/ExceptionGrouper 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_serverkan 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 · detachinitialize()· gjør feilkoden generisk · detachisError-grenen · endre ett byte av bodyen.- MCP-timeouten må komponeres MED anyios eget cancel scope, ikke
asyncio.wait_forutenfra (kø-(z), 2026-08-03): STATE-premisset ("en utypetTimeoutErrorre-raises urørt") var FEIL — målt mot en EKTE hengende server gaasyncio.wait_for(run(), timeout=...)aldri enTimeoutErrori det hele tatt; den kansellererrun()UTENFRA strukturen anyio selv eier (stdio_client/ClientSession), og de to kansellerings-mekanismene komponerer ikke — målt utfall var enanyio.BrokenResourceErrorinni enBaseExceptionGroup(en bakgrunns-reader-task mistet skrive-enden midt i nedrigging). Fiksen eranyio.fail_after(timeout_seconds)NESTET INNI begge task-gruppene, der anyio rigger ned sin egen struktur rent og raiser en renTimeoutError. Innsnevringen dekker IKKE bare typen: builtinTimeoutErrorer ogsåsocket.timeout(≥3.10) ogasyncio.TimeoutError(≥3.11), så et ubetingetexcept TimeoutErrorville mislabelt en HVILKEN SOM HELSTTimeoutErrorsommcp_timeout— reviewet FØR commit (advisor) fant nøyaktig dette. Retteslen eranyio.CancelScope.cancelled_caught: kun scopet som faktisk traff SIN EGEN deadline tjenermcp_timeout-koden, ellers re-raises urørt (speiler_unwrap_ingest_errors eierskaps-regel). Ingen levende utløser finnes i dag for en "fremmed"TimeoutErrorpå denne stien — MÅLT: MCPs eget per-request read-timeout (ClientSession.send_request) konverterer sinanyio.fail_aftertilMcpErrorFØR den når oss, og en tool som raiserTimeoutErrorserver-side blir et ordinærtisError-resultat (samme som enhver annen tool-exception) — begge verifisert empirisk, ikke antatt. Diskriminatoren er likevel pinned med en syntetisk test (raiser fraStdioServerParameters-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 tilasyncio.wait_for· relabel koden · detachcancelled_caught-gaten (behold kun scope-presence).anyiopromotert fra transitiv (viamcp) til deklarert direkte dep (pyproject.toml) — modulen importerer den nå direkte. - 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_projectoverflater begge debatt-deltakere viaoutput_from=agentsog overstyrer utfallet til en checker-kilde-Rejection). Gaten er opt-in-reject (fail-open ved manglende markør), ogprovenance.validator_decisionforblir ærlig — den speiler KUN validatoren, aldri checkeren (de to falsifisererne blandes aldri). Load-bearing:tests/test_checker_gate_loadbearing.pyblir rød ved BEGGE detach-punkt (revertoutput_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 ytremax_attempts-løkke er ikke lenger blind — validatorens forrigeRejection.reasonmates 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.pyblir 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_llmreturnererGenerationResult(outcome, refinements)— ikke lenger bareValidatedProposal | Rejection. Før dette forbrukte løkka hverRejectioninternt (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.refinementsbærer KUN avvisninger som faktisk ble matet tilbake i et senere forsøks prompt — ved uttømt budsjett ER den siste avvisningenoutcome, 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_roundstår, oglastdriver fortsatt prompten alene (prompt-veksten er uendret).run.pyakkumulerer på tvers av_evaluate-kallene, så_evaluate_mandateer urørt;RunResult.refinementser defaultet (coverage-presedensen), og med mandat er den KONKATENERT på tvers av tiltak, ikke nøklet per tiltak (uttalt ærlighets-grense).scripted_factorytar nåstr | reply_selectorper 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øringload_verdicts_from_dir→store.addmerger 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_projectpersisterer 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, kontrastokf.load_ir_projections fail-fast);idleses verbatim, re-mintes aldri.write_verdicter den offentlige authoring-primitiven (persona/test + framtidig Steg 8), men wires IKKE inn irun_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_verdictdet fra RAW output-laget inn i kontekst-laget (OKF-bundelen) som entype: verdict-konseptfil, navigerbar av neste kjøringsseed_store_from_bundle. Gaten er fail-closed: en ikke-godkjent dom (decision ∉ {approved, approved_with_adjustment}) raiserPromotionRefusedog skriver/linker INGENTING — kun menneske/persona-godkjent kunnskap når wikien, aldri rå agent-output (selv-forurensning). Provenance-stemplet (hvem/eksperiment/når;timestamper påkrevd keyword, ingen wall-clock-default → deterministisk). OKF-skriveren bor iokf.pyog er ren stdlib (D7-portabel, MAF-fri — håndhevet avtest_okf_is_maf_free); navigasjon følger KUN index-cross-links, såpromote_verdictlinker filen iindex.mdvia en NØYTRAL label (ellers lekker signalet inn iindex_summary→bundle_contextutenom gaten). R4 = valgfri+gated:promote_verdicter en offentlig opt-in-primitiv, wires IKKE inn irun_project(speilerwrite_verdict— systemet leser; gaten/personaen promoterer). Ærlighets-grenser: promotert fil er MINIMAL (læringssignal kun somdescription/body-prosa, reproduserer ikke seedens strukturerterealization_rateo.l.); id = læringsnøkkel, så to godkjenninger om samme kandidat deler filnavn (last-write-wins, somwrite_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årlink_in_indexdetaches); promotert signal holdes ute avbundle_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_bundleleser hvertype: verdict-fils EGNE strukturelle felt fra frontmatter (affected_codes/measure_type/claimed_saving_nok); mangler de, faller nøklingen tilbake tilbundle_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 raiserVerdictFrontmatterErrori stedet for å slås sammen med bundle-kandidaten — sammenslåingen ville myntet en nøkkel som tilhører INGEN av kandidatene. Validering, ALDRI reparasjon (speilerwrite_concept_file); den tolerante hopp-over-regelen hører til RAW-innboks-laget.claimed_saving_nokparses medjson.loads— SAMME literal-regel IR-projeksjonen gikk gjennom — og skrives tilbake somstr()av råverdien, fordi_mint_idhasher den (30000≠30000.0; en normaliserende skriver ville splittet én kandidats signal på to id-er).promote_verdictskriver 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 feltenepromote_verdictskriver · 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å hvertaffected_itemmotCostBaseline(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 medquantity/unit_costutenfortolerance(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 fraproject.cost_items(alltid — estimatet ER prosjektet), bundle-stien KUN når bundelen shippercost-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 somread_spend). F8: metode-cap-en slås opp iMETHOD_CAPS-registeret (måletype→brøk, injiserbart), ikke motenergy_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+PortfolioMeterer ÉN ledger over hele porteføljepasset (og — seedet avread_spend— på tvers av pass), mens per-runBudget/TokenMeterer uendret. Taket har tre tenner, med hver sin jobb: (1) oppstartsnekt — en rest som ikke kan finansiere én kjøring raiserBudgetRefusedFØ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 iBudgetMiddleware— 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_stoper et EGET felt, aldristop_reason: et mål-stopp er suksess, dette er ressurs-utmattelse — å slå dem sammen ville gjort «vi stoppet» uleselig.record/checker SPLITTET iPortfolioMeterfordi 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_spendraiser på korrupt innhold (kontrast det tolerante RAW-inbox-laget — å lese korrupt som null ville gitt tilbake et allerede brukt budsjett), ogwrite_spendtar et PÅKREVDstamputen wall-clock-default (byte-determinisme, speilerpromote_verdict).portfolio_meterogmeter_factoryer 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ørread_spendtolerant. BudgetExceededs tre felt beskriver ÉN og samme ledger (kø-(y)):kind/limit/observeder ett strukturert stopp-event, ogobservedvar det udefenderte tredje feltet — MÅLT: fire av fem raise-steder (TokenMeter.charge,tick_round, og BEGGE armene iexhausted()) kunne rapportere hvilken som helst verdi uten at 621 tester merket det; barePortfolioMeter.checkvar dekket. Fellen som skjulte det:spikes/_harness.py:41har sin EGEN kopi avBudgetExceeded/TokenMeter, såspikes/test_harness.pysobserved-assert dekker IKKE den shippede modulen — produksjonenstick_roundhadde null direkte test.exhausted()er det ENESTE stedet som VELGER ledger (S3.4-guarden), så en refusal som sierportfolio_tokensmens den rapporterer runets eget forbruk ville villedet enhver leser. Testene bygges medobserved != 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: femobserved-mutasjoner (inkl. kontrollen) + fire ekko-mutasjoner.- Pengetall kvantiseres i ÉN orden, fra ÉN kilde (kø-(p)):
ledger.to_oreer rammeverkets ENE NOK→øre-konvertering (Decimal, ROUND_HALF_UP), og den brukes per pengebeløp — deretter summeres HELTALL.run.pyimporterer 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.0sREPLIES-presedens). Før dette summerterun.pys mål-baselineProject.total_cost-FLOATS og kvantiserte totalen ÉN gang, mensSavingsLedgersummerte 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.005NOK er18000003øre kvantisert først, men18000001summert først (float-drift til180000.01499999998) — nok til å vippe et mål. Kvantiser-først valgt fordi hverCostItemER et beløp (S4.0 gjorde per-linjequantity/unit_costtil 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 irun.py· endre avrundingsmodus · larealizegå utenomto_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.simulationdriverrun_projectmed en SKRIPTET syntetisk chat-klient (ScriptedChatClientpåOpenAIChatCompletionClient— IKKE bareBaseChatClient, ellers no-op-erBudgetMiddleware) 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øresuv run python -m portfolio_optimiser.simulation. Load-bearing:tests/test_simulation_loadbearing.pyblir 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_loopkalterun_projectUTENverdict_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årverdict_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, ogtest_simulation_loadbearing.pys promoterings-assert ville stått GRØNN med promoteringen detached, altså blitt vakuøs.simulate_learning_loopraiser derforValueErrornårinbox_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_idhasher kandidat- featurene, så en myntet id kolliderer med den promoterte dommens, ogVerdictStore.adder first-write-wins per id. Load-bearing MÅLT (tests/test_step7_demo_inbox_loadbearing.py), tre mutasjoner røde + grønn kontroll: detachverdict_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 etScriptedCandidate-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ærerProject: {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 — raiserScriptedCandidateError; 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_keyMÅ være fraværende fra bundelen (ellers bærer forsøk 1s prompt den allerede).simulate_learning_looptarproject_idved siden avbundle_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 · detachproject_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 undershared/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_bundleKOPIERER bundelen og legger fila til utenforshared/, og kjørestien (run.py→load_optional_cost_baseline) er da NØYAKTIG samme søm en levert bundle ville brukt. Retningen på avledningen er bærende: reservens tall er syntetiske, så manus-registeret er eneste grunnsannhet —baseline_from_scripted_candidateavleder 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 (ValueErrorellers): 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 erprovenanceet 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», ogENERGI-TOTAL-ELstå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 imain(), ALDRI ved import — en bibliotek-modul skal ikke omkonfigurere loggingen til en konsument. De toExperimentalWarning-linjene dempes IKKE: de fyrer mensportfolio_optimiser/__init__.pyimportererrun→agent_framework, altså alltid FØRsimulationsin 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: detachmain()-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. - 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 viaportfolio_optimiser.persona.load_persona_example(call-time, fail-fast), Claude-SDK- søskenet med sin egen loader mot samme JSON. Dette AV-STUBBER simuleringen: persona-dommen (decision- rationale + sporet markør) hentes nå fra artefaktet ved call-time, ikke en inline-literal — så
personaen er genuint konsumert og kan ikke råtne stille. Decision er binær (
approved/rejected—FeedbackContractrun-stien tar;approved_with_adjustmentavvises 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).
- 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 (
- 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.)