diff --git a/.claude/projects/2026-06-26-fase3-portfolio-fanout/brief.md b/.claude/projects/2026-06-26-fase3-portfolio-fanout/brief.md new file mode 100644 index 0000000..fac7c95 --- /dev/null +++ b/.claude/projects/2026-06-26-fase3-portfolio-fanout/brief.md @@ -0,0 +1,202 @@ +--- +type: trekbrief +brief_version: "2.2" +created: 2026-06-26 +task: "Fase 3: orkestrator fan-out over N porteføljeprosjekter med state-isolasjon + delt læring" +slug: fase3-portfolio-fanout +project_dir: .claude/projects/2026-06-26-fase3-portfolio-fanout/ +research_topics: 0 +research_status: skipped +auto_research: false +interview_turns: 9 +source: interview +framing: refine +phase_signals: + - phase: research + effort: standard + - phase: plan + effort: high + model: opus + - phase: execute + effort: high + model: opus + - phase: review + effort: high + model: opus +--- + +# Task: Fase 3 — orkestrator fan-out over N porteføljeprosjekter + +> Generated by `/trekbrief` on 2026-06-26. +> This brief is the contract between requirements and planning. `/trekplan` +> reads it to produce the implementation plan. Every decision in the plan must +> trace back to content in this brief. + +## TL;DR + +Refine av den låste planen (Fase 3, §52-57): bygg en **sekvensiell fan-out-orkestrator** +som kjører N porteføljeprosjekter gjennom eksisterende `run_project`, **isolerer +eksekverings-state** per prosjekt men **deler én VerdictStore** (ExpeL akkumulerer over +porteføljen). Bevis at et nytt syntetisk prosjekt kjører ende-til-ende **kun via konfig** +(null kjerne-endring), med **load-bearing** isolasjons-test + extension-dok. + +## Intent + +Rammeverket beviser i dag metoden på ÉTT prosjekt (Fase 2 vertikal skive, +`run_project`). Kjerneløftet — en *generisk* porteføljeoptimerer — er ubevist +helt til rammeverket kjører over N uavhengige prosjekter uten kjerne-kodeendring +per prosjekt. Fase 3 generaliserer: en orkestrator fan-out'er over en konfigurert +portefølje, isolerer hvert prosjekts eksekverings-state (budsjett-meter, debatt, +retrieval-kontekst) så det er null state-bleed, samtidig som den tråder én delt +VerdictStore så læringsløkken (ExpeL) komponerer over porteføljen — en dom på ett +prosjekt informerer like forslag på andre. Å bevise at et splitter nytt syntetisk +prosjekt slippes inn kun via konfig er den konkrete falsifiseringen av +genericitets-påstanden (D4/D5). Dette låser opp Fase 4 (open-source) og D7 +Claude-SDK-søsken, som begge avhenger av en genuint generisk, konfig-drevet kjerne. + +## Goal + +En `run_portfolio`-orkestrator (navn avgjøres i plan) eksisterer som tar en +konfigurert liste prosjekt-id-er + én delt VerdictStore, kjører hvert gjennom +`run_project` **sekvensielt** med isolert eksekverings-state, og returnerer en +ordnet liste av per-prosjekt `RunResult` pluss et **tynt aggregat** (antall +validert/avvist, sum claimed saving, sum token-forbruk). Et nytt syntetisk +referanse-prosjekt lagt til **kun** i `reference_projects.json` (+ dets +docs-fixture) kjører gjennom porteføljen uten endring i noen kjerne-modul. +Referanse-domenet er pakket som dokumentert eksempel, med extension-point-dok for +«legg til egen metode / egen datakilde / egen modell-map». + +## Non-Goals + +- **Concurrent/parallell fan-out** (`asyncio.gather` / `ConcurrentBuilder`) — kun + sekvensiell; concurrency er et dokumentert extension point (90%-prinsipp). +- **Dynamisk/runtime prosjekt-topologi** — kun fast konfig-liste. +- **Rik porteflje-rapport** (HTML/markdown-dashboards) — kun tynt aggregat-struct. +- **Per-prosjekt isolert VerdictStore** — vi deler bevisst én store (kryss-prosjekt-læring). +- **Live Azure/Foundry-kjøring som gate** — offline syntetisk forblir default; ekte-profil-arm + forblir gated/skipped (Standing item SC9, uendret). +- **Compliance-funksjoner** (D3 — uendret invariant; deployer eier DPIA/ROS). + +## Constraints + +- **Ingen kjerne-modul-kodeendring for å legge til et nytt prosjekt** (det bærende + genericitets-kriteriet). Å legge til et prosjekt = JSON-konfig + docs-fixture, intet annet. +- **Per-prosjekt `docs_dir` MÅ være konfig-drevet** (et felt i prosjekt-konfigen, + resolvet av loaderen) — IKKE en hardkodet id→sti-mapping i orkestrator-kode. Ellers brytes + SC1 for det NESTE prosjektet. (Selve mekanismen å støtte per-prosjekt-`docs_dir` er en + engangs kjerne-endring under Fase 3-bygget; det er den marginale kostnaden ved å legge til + prosjekt N+1 *etterpå* SC1 måler.) +- **Kostnadsdisiplin (D6):** utvikle/verifiser på lokal/offline syntetisk profil; ingen tunge live-kjøringer. +- **Deterministisk validator forblir obligatorisk + blokkerende** (uendret). +- **MAF 1.9.0; `uv`; Python ≥3.10.** Eksisterende `run_project`-signatur er per-prosjekt-sømmen + (gjenbruk — IKKE fork/dupliser logikken). +- **STATE.md local-only** (uendret). + +## Preferences + +- **Gjenbruk `run_project` som-det-er** (det eksponerer allerede `store=` og + `client_factory=`-sømmene) — orkestratoren *komponerer* det, dupliserer ikke logikken. +- Match eksisterende modul-stil: `frozen` dataclasses for resultat-typer, type hints overalt, + ruff/mypy-rene. +- Tynt aggregat som frozen dataclass (f.eks. `PortfolioResult`) som speiler `RunResult`-stilen. + +## Non-Functional Requirements + +- Suite forblir grønn offline (syntetisk), ingen ny live-avhengighet. +- **Isolasjons-testen MÅ være load-bearing:** den FALLER hvis eksekverings-state + bleed-er mellom prosjekter (f.eks. delt meter) — ikke green-but-dead (Fase 2-lærdommen, + [[load-bearing-tests-quality]]). +- `uv run mypy src` ren; `uv run ruff check .` ren. + +## Success Criteria + +- **SC1 — Nytt prosjekt kun via konfig:** et nytt syntetisk prosjekt lagt til KUN i + `reference_projects.json` (+ docs-fixture, inkl. dets konfig-drevne `docs_dir`) kjører + gjennom orkestratoren og produserer et `RunResult` for det. Bevis: `git diff --name-only` + for tilføyelsen viser KUN endringer i `*.json` + docs-fixture + tester — null + `src/portfolio_optimiser/*.py`-kjernemodul. +- **SC2 — Fan-out over ≥2 prosjekter:** orkestratoren returnerer ett `RunResult` per + prosjekt + ett aggregat. Falsifiserbart: `uv run pytest tests/test_portfolio.py` exit 0, + asserterer `len(result.runs) == N` og at aggregatet summerer korrekt. +- **SC3 — State-isolasjon er load-bearing:** en test beviser at per-prosjekt + eksekverings-state (budsjett-meter) IKKE bleed-er; testen FALLER hvis orkestratoren + endres til å dele én meter over alle prosjekter (detach-bevis). Foretrukket: enkod + detach-en som en automatisk guard (inject en delt meter i orkestratoren, assert at + isolasjons-testen da blir rød) framfor kun en manuell reviewer-revert — manuelle + detach-bevis hoppes over under tidspress (Fase 2-feilmodus). +- **SC4 — Delt VerdictStore akkumulerer:** etter fan-out inneholder den delte storen + én verdict per prosjekt, og et senere prosjekts ExpeL-retrieval kan overflate en + tidligere prosjekt-dom for et likt forslag. Assert: `store.verdicts` har lengde N, + og `retrieved` er ikke-tom for en matchende feature i et senere prosjekt. (Retrieval keyer + på `_features_of(proposal)` (`run.py:185`) — se Open Questions for fixture-kravet.) +- **SC5 — Full suite grønn offline, ingen regresjon:** `uv run pytest` → alle nye tester + passerer OG dagens grønne baseline-antall holder (current + new), de 3 eksisterende skip + bevart, null nye failures; `uv run mypy src` ren; `uv run ruff check .` ren. (Baseline-tallet + hentes fra faktisk suite ved plan/execute-start — IKKE hardkodet gjetning.) +- **SC6 — Extension-dok eksisterer:** en doc-seksjon «legg til eget prosjekt / egen + datakilde / egen modell-map» som peker på konfig-sømmene; verifiserbart ved at fila finnes + og refererer `reference_projects.json` + modell-map. +- **SC7 — Begge profiler eksersert (offline):** en parametrisert test kjører orkestratoren + under `profile=local` OG `profile=azure` med den syntetiske `client_factory`, og asserterer + at begge produserer et `RunResult` (kontrakt-stien faktisk kjørt, ikke bare backend + instansiert). Ekte Azure-arm forblir `@pytest.mark.skip` (SC9 uendret). + +## Research Plan + +No external research needed — the codebase and this brief contain sufficient +context for planning. MAF-concurrency-mekanikk ble spiket i Fase 1 (Spike B) og +concurrency er bevisst utenfor scope (sekvensiell valgt); `run_project` eksponerer +allerede `store=` og `client_factory=`-sømmene planen trenger. + +## Open Questions / Assumptions + +- **[ASSUMPTION]** `run_project`s eksisterende `store=`-param tråder en delt VerdictStore + korrekt over sekvensielle kall (den `add`-er per kall; retrieval leser tidligere). Verifiser + i plan mot `run.py:184-193`. +- **[ASSUMPTION]** Hvert `run_project`-kall bygger en fersk meter + fresh_workflow internt + (`run.py:151-161`), så eksekverings-isolasjon er i stor grad iboende ved sekvensiell gjenbruk — + orkestratoren må unngå å introdusere delt muterbar state. Verifiser at meteren konstrueres per + kall (`run.py:151`). +- **[OPEN] docs_dir per prosjekt:** `run_project` tar én `docs_dir`. En portefølje av N + uavhengige prosjekter trenger naturlig hver sin docs-kilde. Løsningen er bundet av Constraints: + per-prosjekt-`docs_dir` MÅ være et konfig-felt (resolvet av loaderen), ikke en kode-mapping — + ellers brytes SC1. Plan ferdigstiller felt-navn + hvor det bor (i `reference_projects.json` + per prosjekt vs en egen portefølje-konfig). +- **[OPEN] Aggregat-felter:** eksakt sett (antall validert/avvist, sum claimed saving, sum + tokens) ferdigstilles i plan. +- **[OPEN] SC4 fixture-design:** for at et senere prosjekts retrieval skal overflate et + tidligere prosjekts dom, må de to syntetiske prosjektene være laget så den syntetiske klienten + gir OVERLAPPENDE features (retrieval keyer på `_features_of`: affected codes, measure_type, + claimed saving). Plan må designe fixturene for et reelt feature-match, ikke oppdage gapet ved test-tid. +- **[ASSUMPTION/PROVENANCE] Nytt syntetisk prosjekt er AI-forfattet domene-innhold.** SC1s nye + prosjekt (kostnadskoder, enheter, enhetspriser) er AI-foreslått; operatøren er ikke domene-ekspert + ([[user-not-domain-expert]]) og kan ikke fange domene-feil. Planen/fixturen markerer dataen som + AI-forslag / «ikke verifisert», konsistent med eksisterende `_note` i `reference_projects.json`. + +## Prior Attempts + +Ingen tidligere fan-out-forsøk. Fase 1 Spike B (`tests/spikes/test_b_footguns.py`, throwaway) +testet `ConcurrentBuilder` state-isolasjon — informerer isolasjons-bekymringen, men brukte en +ANNEN (concurrent) mekanisme enn det sekvensielle valget her. Lærdom fra Fase 2: +load-bearing-tester må FALLE når sømmen detach-es ([[load-bearing-tests-quality]]). + +## Metadata + +- **Created:** 2026-06-26 +- **Interview turns:** 9 +- **Auto-research opted in:** no +- **Source:** trekbrief interview + +--- + +## How to continue + +Manual (default) — 0 research-tema, så rett til plan: + +```bash +# Ingen /trekresearch (0 tema). +/trekplan --project .claude/projects/2026-06-26-fase3-portfolio-fanout + +# Deretter: +/trekexecute --project .claude/projects/2026-06-26-fase3-portfolio-fanout +```