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