portfolio-optimiser/docs/plan/2026-07-10-sesjonsplan-fase2-6.md

431 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Sesjonsplan — Fase 2 → offline-fullførings-grense (S2.0S5.3 · D-AD-E · M1M3)
> **Status:** GJELDENDE sesjonsplan. Forfattet som re-plan-forslag av kryssmodell-reviewen
> 2026-07-09 (Fable 5); **promotert til gjeldende plan 2026-07-10 på operatør-instruks.**
> Roadmapen (`2026-07-06-reell-kjoring-analyse-plan.md`) er revidert med kryssreferanser hit.
> Funn-referanser (F1F14, P1P6, U-1U-4) peker til [reviewen](../review-2026-07.md).
> **Beslutnings-sesjonene D-AD-E er fortsatt ÅPNE (operatør-kø):** bygg-sesjoner gated på dem
> starter ikke før beslutning foreligger; **S2.0/S2.1/S2.5 er byggbare NÅ** (ingen
> beslutnings-avhengighet). Endringer mot roadmapen er merket `[ENDRET]` / `[NY]`.
> Frosne dokumenter (målbilde, ingest-målbilde, method-spec) er IKKE endret — amendments
> skjer kun via D-A/D-B + commons (PULL-ONLY).
>
> Prinsipp: hver bygg-sesjon er én-sesjons-stor, offline, gratis (skriptede klienter), TDD
> (Iron Law), og Opus-eksekverbar med kun «Les STATE.md og følg instruksjonene». Kost/tenant/
> ekspert-gatede steg er **operatør-milepæler (M1M3)**, aldri autonome Opus-sesjoner.
---
## 1. Endringer mot roadmapen (oppsummert)
| Roadmap | Endring | Begrunnelse |
|---|---|---|
| Fase 2 «MCP wiret i kjørestien» | `[ENDRET]`**MCP-ingest-konnektor** (ny kildefamilie i manifestet), aldri kjøresti-wiring | P2: method-spec §3 forbyr query-time retrieval; CLAUDE.md-invariant |
| Fase 2 «live-kilde-herding, inkrementell re-ingest» | `[ENDRET]` → gated på beslutning **D-B**; default-scope er herding av http-konnektoren mot mock (timeouts/retry/feilkategorier), IKKE live kilde/inkrementell | P1/U-3: frossen ingest-målbilde §8/§11/§12 forbyr dette; bevisst amendment kreves |
| Fase 3 «concurrent fan-out» | `[ENDRET]` → krever beslutning **D-D** først (determinisme-modell) + fikse F1/F6 FØR concurrency | Concurrent==sekvensiell er i dag selvmotsigende (delt store, RMW-index) |
| Fase 4 | `[ENDRET]` → splittes i offline-byggbar del (S4.0S4.2, autonome sesjoner) + operatør-milepæler (M1/M2) | Mandat-skille |
| Fase 5 | `[ENDRET]` → forutsetter outbox (S2.1) som roadmapen ikke nevner | P4: output-laget (målbilde §3) er ubygget |
| — | `[NY]` S2.0 (portefølje-læringssløyfe), S2.1 (outbox), S2.5 (inbox-herding), S2.7 (validator-stramming), S4.0 (kostbaseline-forankring) | Injisert av funn F1, P4, F7/F11, F2, F3 |
| — | `[NY]` Beslutnings-sesjoner D-AD-E FØRST | §4-restene + spec-amendments må avgjøres før bygg |
**Funn → sesjon-mapping:** F1→S2.0 · F2→D-A+S2.7 · F3→D-A+S4.0 · F4→D-A (spec) · F5→D-A+S3.2 ·
F6→D-D+S3.3 · F7→S2.5 · F8→S4.0 · F9→D-E+S2.0(guard) · F10→S3.4 · F11→S2.5 · F12→S2.0 ·
F14→D-A (tekst) · P1→D-B · P4→S2.1 · P6→S4.1/S5.2/S5.3.
**Commons-regel (gjelder alle spec-amendments):** `shared/` er PULL-ONLY subtree. Endringer i
method-spec/ingest-spec/bundle-layout skrives i `portfolio-optimiser-commons` og pulles hit
(`git subtree pull --prefix=shared commons main --squash`). D7-søskenet må speile senere —
UTENFOR dette repoets sesjoner; flagges i hver berørt sesjon.
---
## 2. Beslutnings-sesjoner (FØRST — operatør + kort AI-brief, ingen kode)
### D-A — Spec-amendments i commons (method-spec)
- **Avgjør:** (1) validator nominal-gate + IR-invariant `low ≤ unit_cost ≤ high` (F2 — golden-
kompatibel, verifisert); (2) kostbaseline-avstemmings-stage (F3); (3) `/`-prefiksede
cross-links mappes til bundle-rot (F4/U-1); (4) per-verdict seeding-nøkkel (F5/U-4);
(5) tekst-justering «Magentic eksperimentell» → «planleggingstung/uprøvd utenfor
Magentic-One» (F14; kun begrunnelse, valget står).
- **Output:** godkjent/avvist per punkt + utkast til commons-endringer. Blokkerer: S2.7, S3.2, S4.0.
### D-B — Fase 2-scope vs frossen ingest-målbilde
- **Avgjør:** (1) amendes ingest-målbildet bevisst (live-kilde/inkrementell/§11), eller nedskopes
roadmap-B til mock-herding (P1/U-3 — anbefalt: nedskop nå, live utsettes til M3-forberedelse);
(2) §4.4 første live-kilde (anbefalt: **SQL/database**`read_sql` er allerede den mest herdede
konnektoren, read-only by construction); (3) dokument-konnektor-avgrensning: PDF-tekst via
`pypdf` (ny dep) nå, DOCX utsatt; (4) MCP-kildefamiliens nettverks-/subprosess-grant-form.
- **Output:** Fase 2-scope-erklæring + evt. målbilde-amendment. Blokkerer: S2.2, S2.3, S2.4.
### D-C — Vektor-store (§4.5)
- **Grunnlag (delegert research, kildeført i review §6):** ved 1005 000 dokumenter er ANN
unødvendig; eksakt søk er deterministisk og offline-testbart. Rangering: **1) numpy brute-force**
(BSD, universelle wheels, delt format .npy+jsonl er enklest for begge stacker), **2) sqlite-vec
0.1.9** (eksakt KNN, MIT/Apache, x86_64-wheel OK — men pre-v1.0, pin), 3) faiss-cpu (x86_64-wheel
krever macOS 15+ — sjekk `sw_vers` først). **LanceDB: dealbreaker** (ingen Intel-Mac-wheel);
ChromaDB/DuckDB-VSS: frarådet (tunge deps / eksperimentell VSS).
- **Avgjør:** valg (anbefalt: numpy) + delt lagringsformat med SDK-søskenet + embeddings-kilde for
ekte kjøring (Azure OpenAI embeddings vs lokal — kun konfig; tester bruker alltid fake-embedder).
- **Output:** beslutning D8. Blokkerer: S3.1.
### D-D — Concurrency- og feil-modell (Fase 3)
- **Avgjør:** (1) determinisme-modell for concurrent fan-out — anbefalt: **bølge-modell** (kjøringer
i en bølge ser samme store-snapshot; deterministisk merge-barriere mellom bølger; sekvensiell ==
bølgestørrelse 1); (2) én-skriver-regel eller fil-lås for index/ledger (F6); (3) feilpolicy ved
delvis feil midt i portefølje (anbefalt: collect-and-continue, per-run-feil i `PortfolioResult`,
aldri tap av fullførte runs — i dag kastes alt, review §3).
- **Output:** concurrency-kontrakt. Blokkerer: S3.3, S3.4.
### D-E — Mål-semantikk-rest + stack-paritet
- **Avgjør:** (1) prosent-måls baseline-kilde (F9: subset-avhengig i dag; bundle-prosjekter har
baseline 0) — anbefalt: eksplisitt `baseline_ore` i GoalContract/GoalConfig, aldri implisitt sum;
(2) §4.6 stack-paritet for den ekte kjøringen (MAF, SDK eller begge — operatørens kost-avveining;
liveness-asymmetri-erklæringen i sammenligningsprotokollen §3 består uansett).
- **Output:** semantikk-beslutning. Blokkerer: del av S2.0 (guard-form), M2-konfig.
---
## 3. Opus-sesjons-specs (offline, autonome)
> Felles for alle: Iron Law (failing test først); `uv run pytest` / `uv run ruff check .` /
> `uv run mypy src` grønne ved slutt; skriptede klienter (null modell-kost); commit per sesjon i
> repoets Conventional-Commit-stil; STATE.md overskrives ved sesjonsslutt.
### S2.0 `[NY]` Portefølje-læringssløyfe + ærlighets-fiks (F1, F12, P5, F9-guard)
- **Mål:** en dom på prosjekt k når prosjekt k+1s hypotese-PROMPT i `run_portfolio` — og ingen
docstring påstår mer enn koden gjør.
- **Scope:** per-prosjekt `bundle_dir` (valgfritt felt i `reference_projects.json`) +
`verdict_dir`-parameter tres gjennom `run_portfolio``run_project`. Percent-goal-guard:
eksplisitt feil (ValueError) når percent-mål settes og baseline er 0. CHANGELOG oppdateres
(Fase 1 + denne). IKKE bygges: concurrency, semantisk henting, outbox.
- **Avhengigheter:** ingen (første bygg-sesjon).
- **Filer:** `run.py`, `reference_domain.py`, `data/reference_projects.json` (+ evt. mini-bundle
fixture under `data/`), `tests/test_portfolio.py`, ny `tests/test_portfolio_learning_loadbearing.py`,
`CHANGELOG.md`.
- **TDD:** (1) RØD: test der prosjekt k får dom (via delt store, bundle-sti) og k+1s
genererings-prompt MÅ inneholde dommens id+markør — detach-punkt: fjern bundle_dir-trådingen i
run_portfolio → rød (i dag ER den rød — det er poenget). (2) RØD: empty-store-kontroll.
(3) RØD: percent-mål + baseline 0 → ValueError.
- **Nøkkelantakelser:** bundle-fixture kan gjenbruke `bygg-energi-mikro`-form uten commons-endring
(test: legg fixture under `src/portfolio_optimiser/data/`, aldri i `shared/`).
- **Verifisering:** `uv run pytest tests/test_portfolio_learning_loadbearing.py` → grønn; detach
(revert tråding) → rød; full suite + ruff + mypy grønne; `grep -n "cross-project learning" src/`
viser docstring som nå stemmer med wiring.
### S2.1 `[NY]` Outbox-persistens — output-laget materialiseres (P4)
- **Mål:** hver kjørings forslag/avvisning/provenance persisteres som deterministisk JSON i en
outbox-mappe (målbilde §3s output-lag), så utestående dommer kan spores (Fase 5) og
live-artefakter fanges (sammenligningsprotokoll §4.2).
- **Scope:** `run_project(outbox_dir=..., run_id=...)` (begge valgfrie; `run_id` er påkrevd når
outbox er satt — ingen wall-clock-default). Skriver `{run_id}-proposal.json` /
`{run_id}-outcome.json` (outcome-type, reason/percentiler, checker_verdict, provenance,
verdict-id). Systemet SKRIVER outbox (lovlig: rolledelingen §3 gjelder inbox/wiki, ikke
systemets eget output-lag — sitér method-spec §2 i docstring). IKKE bygges: ruting, varsling.
- **Avhengigheter:** ingen (parallell med S2.0).
- **Filer:** `run.py`, ny `outbox.py` (ren stdlib+pydantic — MAF-fri? Nei: importerer
validator-typer som er rene; hold `agent_framework`-fri og legg til i `_MAF_FREE_MODULES`),
`tests/test_outbox_loadbearing.py`.
- **TDD:** RØD: kjøring med outbox_dir → forventet filpar finnes, byte-deterministisk
(sort_keys, indent 2, LF); detach-punkt: fjern outbox-skrivingen → rød. Kontroll: uten
outbox_dir skrives ingenting.
- **Nøkkelantakelser:** `ValidatedProposal`/`Rejection` er JSON-serialiserbare via eksplisitt
mapping (test først med begge utfallstyper).
- **Verifisering:** ny testfil grønn; detach → rød; full gate grønn; to kjøringer med samme
input+run_id → byte-identiske filer (`diff`).
### S2.2 MCP-ingest-konnektor (gated: D-B) `[ENDRET fra «MCP i kjørestien»]`
- **Mål:** en fjerde kildefamilie `mcp` i ingest-manifestet materialiserer OKF-filer fra en
MCP-server — datainntak, aldri kjøresti.
- **Scope:** ingest-spec-amendment i commons FØRST (skjema for `{type:"mcp", id, server_ref,
tool}`; `server_ref` = navn på env-var med kommando, mirror `connection_ref`) → subtree pull →
impl i `ingest.py`. Transport: stdio-klient via `mcp`-pakken (allerede dep). Per-run-grant:
gjenbruk `allow_network`-mønsteret med nytt eksplisitt flagg (per D-B). Test: in-process
fixture-server (gjenbruk `datasource.build_mcp_server`-kjernen mot committed docs — det
AV-DEMOER extension-pointet). IKKE: fjern-servere, auth utover env-ref.
- **Avhengigheter:** D-B (+ commons-pull).
- **Filer:** `ingest.py`, `tests/test_ingest_mcp*.py`, golden-fixture `examples/ingest-golden-mcp/`
(i commons — flagg), `docs/extending.md`-avsnitt.
- **TDD:** RØD: manifest med `type: mcp` uten grant → `IngestError` (grant-detach → rød);
RØD: golden-materialisering mot fixture-server → byte-deterministisk bundle; RØD:
verdict-reservasjonen gjelder også mcp-familien.
- **Nøkkelantakelser:** `mcp`-klient kan drives i-prosess mot FastMCP uten socket (test som
første steg — hvis ikke, subprosess mot `python -m`-fixture med eksplisitt merking).
- **Verifisering:** `uv run pytest tests/test_ingest_mcp*.py` grønn; detach grant-sjekken → rød;
full gate grønn.
### S2.3 Dokument-konnektor (`doc`-familie; gated: D-B)
- **Mål:** PDF-rapport → OKF-konseptfil med provenance, deterministisk mot committed fixtures.
- **Scope:** ingest-spec-amendment (commons) for `{type:"doc"}`; `pypdf` som ny dep (D-B);
tekst-ekstraksjon per side, fenced-verbatim-rendering (gjenbruk `_render_fenced_block`-regler),
`max_rows`-cap, fail-fast på ikke-tekst-PDF. DOCX: IKKE (utsatt per D-B).
- **Avhengigheter:** D-B; uavhengig av S2.2.
- **Filer:** `ingest.py`, `pyproject.toml` (+pypdf), `tests/test_ingest_doc*.py`, golden-fixture
med liten committed PDF.
- **TDD:** RØD: golden byte-determinisme; RØD: kodefence-linje i ekstrakt → `IngestError`
(gjenbruk http-regelen); RØD: cap-overskridelse → feil, aldri trunkering.
- **Nøkkelantakelser:** `pypdf`-ekstraksjon er bit-stabil på tvers av versjoner for fixture-PDF-en
(test: pin versjon; golden fanger drift).
- **Verifisering:** nye tester grønne + detach → rød; full gate grønn.
### S2.4 Konnektor-herding mot mock (gated: D-B) `[ENDRET fra «live-kilde-herding»]`
- **Mål:** http-konnektoren tåler virkelighetens feilmoduser — bevist mot injisert transport,
aldri live.
- **Scope:** timeout-parameter på `_urllib_get`; feilkategorisering (transport/auth/decode) i
`IngestError`-melding; ingest-logg beriket (§8: kilde, tidspunkt, radantall — aldri innhold);
retry KUN hvis D-B sier ja (default nei — determinisme). Inkrementell re-ingest: IKKE (frossen
§8) med mindre D-B amender.
- **Avhengigheter:** D-B.
- **Filer:** `ingest.py`, `tests/test_ingest_http*.py`.
- **TDD:** RØD per feilmodus (canned transport som raiser URLError/timeout/feil-encoding) →
presis IngestError; detach timeout → rød.
- **Verifisering:** nye tester grønne; full gate grønn; `grep` bekrefter ingen nye socket-stier
utenfor `_urllib_get`.
### S2.5 `[NY]` Inbox- og robusthets-herding (F7, F11)
- **Mål:** verdict-inboxen slutter å være en uvalidert injeksjonsflate, og offline-bevisets
MAF-avhengighet får en versjonsvakt.
- **Scope:** (1) `load_verdicts_from_dir`: decision-vokabular-sjekk (§4.2 — ukjent decision →
SKIP, fortsatt tolerant, aldri raise), tak på rationale-lengde + antall filer per merge
(konfigurerbart, fail-fast ved overskridelse — aldri stille kutt); (2) konsolider de fire
`_inner_get_response`-duplikatene (conftest ×3 + simulation) til én delt scripted-klient
(plassering: `tests/`-hjelper + simulation beholder sin — eller flytt til `simulation.py` og
importér i conftest; velg det som ikke gir src→tests-import); (3) MAF-versjonsvakt: test som
feiler med handlingsrettet melding hvis `agent_framework.__version__` ikke matcher pinnet
versjon (tvinger bevisst re-verifisering av privat-API-premissene ved oppgradering);
(4) pin `agent-framework-core>=1.9.0,<2` i pyproject.
- **Avhengigheter:** ingen.
- **Filer:** `verdicts.py`, `conftest.py`, `simulation.py`, `pyproject.toml`, `tests/test_verdicts.py`,
ny `tests/test_maf_version_guard.py`.
- **TDD:** RØD: inbox-fil med `decision: "banana"` → hoppes over (detach vokabular-sjekken → den
entrer storen → rød); RØD: rationale > tak → skip + logg; versjonsvakt-testen rød mot en
fake-versjon.
- **Verifisering:** full gate grønn; `grep -c "_inner_get_response" tests/ src/` viser redusert
duplikat-antall.
### S2.7 `[NY]` Validator-stramming (F2; gated: D-A + commons-pull)
- **Mål:** den deterministiske validatoren håndhever sin egen stage-2-grense, og assumptions kan
ikke forfattes forbi den.
- **Scope:** (1) strukturell blokk `claimed > nominal_feasible` → Rejection (i tillegg til P90);
(2) IR-invariant: hvert assumption-band MÅ omslutte itemets `unit_cost` (`low ≤ unit_cost ≤ high`)
— Pydantic `model_validator`. Golden-kompatibilitet er verifisert i review (claim 30k ≤ 90k;
band [0.70,1.40] ∋ 1.0). IKKE: kostbaseline-avstemming (S4.0), metode-registry (S4.0).
- **Avhengigheter:** D-A pkt. 1 + commons-spec pullet.
- **Filer:** `validator.py`, `ir.py`, `tests/test_validator.py`, `tests/test_ir*.py` (eller
tilsvarende), `tests/test_bygg_energi_mikro.py` (uendret grønn — regresjonsbevis).
- **TDD:** RØD: reviewens moteksempel #1 (claim 100k, nominal 90k, band [0.70,1.40]) MÅ bli
Rejection; RØD: band [1.8,2.2] rundt unit_cost 1.0 → ValidationError ved konstruksjon;
detach-punkt: fjern nominal-blokken → moteksempel-testen rød.
- **Nøkkelantakelser:** ingen eksisterende test/fixture bruker band som ikke omslutter unit_cost
— **VERIFISERT 2026-07-09 av reviewer** (skann av alle JSON-fixtures + inline-band-grep: null
brudd; se review §6). Antakelsen er dermed et bekreftet premiss, ikke en risiko.
- **Verifisering:** `uv run pytest tests/test_validator.py tests/test_bygg_energi_mikro.py` grønn;
golden uendret; full gate grønn. **Flagg:** D7-søsken må speile (egen sesjon i søsken-repoet).
### S3.1 Semantisk henting bak seam (gated: D-C) `[roadmap C]`
- **Mål:** hentingen skalerer forbi feature-key-match uten å ofre determinisme/offline-testbarhet.
- **Scope (per D-C-anbefaling numpy):** `Retriever`-protokoll; `HybridRanker` = dagens
strukturelle score + cosine over lagrede vektorer (vekting konfig); vektor-lager = `.npy` +
id-jsonl ved siden av verdict-storen; `Embedder`-protokoll med deterministisk fake i tester
(hash-basert); ekte embeddings-klient (Azure/lokal) KUN som konfig-punkt, aldri kalt i tester.
IKKE: ANN, ekstern DB, endring i `shared/`.
- **Avhengigheter:** D-C; uavhengig av S2.x.
- **Filer:** ny `semretrieval.py` (MAF-fri, i `_MAF_FREE_MODULES`), `verdicts.py` (retrieve-seam),
`run.py` (opt-in), `pyproject.toml` (numpy), nye tester.
- **TDD:** RØD: syntetisk stor base (500+ verdicts) der KUN cosine-komponenten kan rangere riktig
dom øverst (strukturell score uavgjort ved konstruksjon) — detach cosine → rød; RØD:
determinisme (to kjøringer → identisk rangering); RØD: fake-embedder-bit-stabilitet.
- **Nøkkelantakelser:** numpy-dep er akseptabel i MAF-repoet (IKKE i `shared/`) — bekreftet av
D7-grensen (semantikk-laget er stack-lokalt).
- **Verifisering:** nye tester grønne + detach → rød; full gate grønn; `test_okf_is_maf_free`
utvidet til `semretrieval.py` grønn.
### S3.2 Multi-kandidat ExpeL-substrat (F5; gated: D-A pkt. 4 + commons-pull)
- **Mål:** dommer nøkles på SIN kandidat, ikke bundelens ene IR-projeksjon.
- **Scope:** seed leser features fra verdict-filas egen frontmatter (nye valgfrie felt:
`affected_codes`/`measure_type`/`claimed_saving_nok`; fallback = dagens kandidat-nøkling for
bakoverkompatibilitet); `promote_verdict` skriver disse feltene; multi-kandidat-bundle-fixture.
- **Avhengigheter:** D-A + commons (spec §3 Steg 1 seeding-regel + bundle-eksempel).
- **Filer:** `verdicts.py`, `tests/test_step8_promotion_loadbearing.py`, ny multi-kandidat-fixture
(repo-lokal under `data/`), nye tester.
- **TDD:** RØD: to kandidater i én bundle → dom om kandidat B når ALDRI kandidat As
hypotese-prompt (detach per-verdict-nøklingen → rød fordi B-dommen feil-nøkles til A).
- **Verifisering:** nye tester grønne; eksisterende step1/step7/step8-suite uendret grønn
(fallback-kompatibilitet); full gate grønn. **Flagg:** D7-speiling.
### S3.3 Concurrent fan-out (F6; gated: D-D) `[roadmap C]`
- **Mål:** N kjøringer parallelt med bevart determinisme: concurrent == sekvensiell.
- **Scope (per D-D bølge-modell):** `run_portfolio(concurrency=k)`; bølge = inntil k prosjekter
med samme store-SNAPSHOT; deterministisk merge-barriere (sortert på project_id) mellom bølger;
feilpolicy collect-and-continue (`RunResult`-slot med feil-felt, fullførte runs bevares);
én-skriver-regel for index/ledger håndheves (ingen promote/realize i kjørestien — allerede
sant; assert + dok).
- **Avhengigheter:** D-D, S2.0 (bundle-portefølje), helst S3.1 (men ikke hard).
- **Filer:** `run.py`, `tests/test_portfolio_concurrent_loadbearing.py`.
- **TDD:** RØD: `concurrency=3` gir BYTE-identisk aggregat + store-innhold som `concurrency=1`
på 3-prosjekt-fixturen (detach barrieren → udeterministisk → rød over repetisjoner — kjør
testen med fast rekkefølge-probe, ikke tidsavhengig); RØD: injisert feil i prosjekt 2 →
prosjekt 1+3s resultater bevart.
- **Nøkkelantakelser:** MAF-klienter er asyncio-samtidige i én loop (dokumentert modell er
asyncio; OS-tråd-garanti er IKKE dokumentert — hold alt i én loop).
- **Verifisering:** ny test grønn ×20 repetisjoner (`pytest --count` el. loop); full gate grønn.
### S3.4 Kostnadsstyring på tvers (F10) `[roadmap C]`
- **Mål:** et globalt token-tak over HELE porteføljepasset (og på tvers av pass), håndhevet
FØR kall — ikke bare per kjøring etterpå.
- **Scope:** `PortfolioBudget` (globalt tak + per-run-tak); pre-call-guard i `BudgetMiddleware`
(nekt kall når rest < 0 — i tillegg til dagens post-charge); spend-persistens (deterministisk
JSON, eksplisitt stamp-argument); oppstartsnekt når resttak < konfigurert minimum per kjøring.
- **Avhengigheter:** D-D (deler feilpolicy), S3.3 (bølge-integrasjon) — kan bygges før S3.3 mot
sekvensiell sti.
- **Filer:** `budget.py`, `run.py`, `tests/test_budget.py`, ny loadbearing-test.
- **TDD:** RØD: globalt tak krysses midt i passet → strukturert stopp med fullførte runs bevart
(detach global-sjekken → passet løper forbi taket → rød); RØD: pre-call-guard nekter kallet
(klient-mock som teller kall — kallet skal ALDRI skje).
- **Verifisering:** nye tester grønne + detach → rød; full gate grønn.
### S4.0 `[NY]` Validator-forankring mot kostbaseline (F3, F8; gated: D-A pkt. 2)
- **Mål:** `affected_items` avstemmes fail-closed mot prosjektets faktiske kostbaseline — den
deterministiske gaten kan ikke lenger mates med hallusinerte kostlinjer.
- **Scope:** baseline-projeksjon i bundle (`cost-baseline.json`: code→{quantity, unit_cost} —
commons-amendment) + fra `reference_domain.cost_items` på road-stien; ny avstemmings-stage i
`validate_proposal` (kode finnes ikke i baseline → Rejection; quantity/unit_cost utenfor
toleranse → Rejection; toleranse konfig); metode-registry: metode-caps keyes via
dimensjon/konfig, ikke strengen `energy_efficiency` (F8). Baseline-argument er VALGFRITT i
første omgang (None = dagens oppførsel) så eksisterende suite står — men run-stiene SETTER den.
- **Avhengigheter:** D-A + commons; S2.x-ingest kan generere baseline-projeksjonen (soft).
- **Filer:** `validator.py`, `run.py`, `okf.py` (baseline-loader, fail-fast), commons-fixture,
nye tester.
- **TDD:** RØD: reviewens F3-scenario (dikta kode `XX`, 10 MNOK) → Rejection «unknown cost code»
(detach avstemmingen → validated → rød); kontroll: ekte baseline-linje validerer som før.
- **Verifisering:** nye tester grønne + detach → rød; golden uendret; full gate grønn.
**Flagg:** D7-speiling. MÅ være grønn før M2.
### S4.1 Azure/Foundry-preflight (offline) `[roadmap D, offline-delen]`
- **Mål:** alt som KAN valideres uten et modellkall, valideres før operatøren betaler for ett.
- **Scope:** `python -m portfolio_optimiser.preflight --profile azure`: env-kontrakt
(`PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT` satt + URL-form; deployment-navn ≠ `REPLACE-WITH-*`;
modell-map-konsistens), credential-sjekk KUN som dokumentert manuelt steg (aldri auto-login).
Nekter live-profil med placeholders (fail-fast også i `resolve_model`-stien). Dokumentér
auth-oppskrift (Entra: `az login` / DefaultAzureCredential; FoundryChatClient tar
project_endpoint+model+credential — verifisert mot MS Learn, se review §6). Env-override for
model_map (`PORTFOLIO_MODEL_MAP` sti) så tenant-navn aldri committes.
- **Avhengigheter:** ingen (uavhengig av Fase 2/3).
- **Filer:** ny `preflight.py`, `backends.py`, `contracts.py` (placeholder-regel), tester.
- **TDD:** RØD: placeholder-deployment + azure-profil → strukturert nekt (detach
placeholder-sjekken → rød); RØD: manglende endpoint-env → aksjonérbar feilmelding.
- **Verifisering:** `uv run python -m portfolio_optimiser.preflight --profile azure` gir korrekt
nekt/OK uten nettverk (bevis: kjør med nettverk utilgjengelig antatt — ingen socket-kode i
modulen, grep-guard); full gate grønn.
### S4.2 Live-kjørings-drill + artefakt-fangst (offline) `[roadmap D, offline-delen]`
- **Mål:** den ENE fremtidige live-kjøringen (M2) er ferdig-rigget: alt bygges, artefakter fanges,
og drillen er øvd offline.
- **Scope:** `--live-dry-run`-modus i main(): last kontrakter, bygg klienter, valider budsjett,
STOPP før første modellkall (bevist med kall-tellende mock); kjøringskonfig-logg (modell-id,
parametre, tak — sammenligningsprotokoll §4.3); outbox-fangst (S2.1) av proposal/checker/
validator/tokens per §4.2.
- **Avhengigheter:** S2.1, S4.1.
- **Filer:** `run.py`, `preflight.py`/`main`, tester.
- **TDD:** RØD: dry-run mot mock-faktory → null chat-kall skjedde (detach stopp-punktet → kall
telles → rød); RØD: artefakt-settet komplett i outbox.
- **Verifisering:** nye tester grønne; full gate grønn.
### S5.1 Verdict-ruting + sporing `[roadmap E]`
- **Mål:** teamet ser hvilke forslag som venter på dom, og hvem som skal dømme.
- **Scope:** pending-registry = outbox-forslag uten matchende inbox-dom (id-join);
ruting-konfig `dimension → ekspert` (fail-fast skjema); CLI `python -m portfolio_optimiser.hitl
pending|route`; ren fil-basert (ingen server). IKKE: varsling (S5.2), web-UI.
- **Avhengigheter:** S2.1 (outbox); S2.0 (verdict_dir i portefølje).
- **Filer:** ny `hitl.py`, tester.
- **TDD:** RØD: forslag i outbox uten dom → listes utestående; dom lander i inbox → forsvinner
(detach id-joinen → rød).
- **Verifisering:** nye tester grønne; full gate grønn.
### S5.2 Varsling — B11 fra stub til ekte `[roadmap E]`
- **Mål:** `notify=` får leverbare implementasjoner uten å bryte ingen-stille-egress.
- **Scope:** `Notifier`-protokoll + `console`/`file`/`webhook`-impl; webhook krever eksplisitt
per-kjøring opt-in-flagg (mirror `allow_network`, ingest §8-mønsteret) + injiserbar transport
(canned i tester — null socket i suiten); konfig fail-fast.
- **Avhengigheter:** S5.1 (naturlig kobling), ellers uavhengig.
- **Filer:** ny `notify.py`, `run.py` (seam består), tester.
- **TDD:** RØD: webhook uten opt-in → nekt fail-fast (detach → rød); RØD: canned transport mottar
korrekt payload; RØD: ingen socket-sti utenfor den injiserbare seamen (grep-guard à la
`_urllib_get`).
- **Verifisering:** nye tester grønne; full gate grønn.
### S5.3 CLI-paritet + dokumentsync (P6, F12-rest)
- **Mål:** operatøren kan drive ALT bygget (dimensjon, mål, ledger, bundle-portefølje, inbox,
outbox) fra kommandolinja, og dokumentene påstår nøyaktig det koden gjør.
- **Scope:** `main()` (+ evt. `portfolio`-subkommando): `--dimension-config`, `--ledger`,
`--goals`, `--bundle-dir`, `--verdict-dir`, `--outbox-dir`; README/extending.md/CHANGELOG-sync;
ærlighets-grep (ingen «learning loop»-claims på stier uten fold).
- **Avhengigheter:** S2.0, S2.1 (og drar nytte av alt over).
- **Filer:** `run.py`, `README.md`, `docs/extending.md`, `CHANGELOG.md`, CLI-tester.
- **TDD:** RØD: CLI-kjøring med goals+ledger på fixtures → GoalReached-stopp observerbar i output.
- **Verifisering:** CLI-tester grønne; full gate grønn; manuell `uv run python -m
portfolio_optimiser.run …`-røyk på fixtures dokumentert i STATE.
---
## 4. Operatør-gatede milepæler (IKKE autonome Opus-sesjoner)
### M1 — Foundry-tenant-oppsett + smoke (etter S4.1)
- **Forutsetninger:** operatør oppretter i tenant: én Foundry-prosjekt-endpoint + ÉN
Global-Standard-deployment av billig modell (gpt-4o-mini-/gpt-5-mini-klassen — én deployment
dekker begge roller; research-verifisert). `az login`. Sett
`PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT` + `PORTFOLIO_FOUNDRY_DEPLOYMENT` + model_map-override.
- **Manuelle steg:** kjør preflight (S4.1) → grønn; kjør `uv run pytest tests/test_foundry_profile_live.py`.
- **Kost:** ett trivielt kall (øre-nivå; pris ikke verifisert — sjekk Azure-priser først).
### M2 — Første live-modell-kjøring, liten skala (etter S4.0 + S4.2 + M1)
- **Forutsetninger:** S4.0 grønn (validator forankret — F3 lukket FØR ekte modell), harde tak
satt lavt (f.eks. max_tokens 20k), outbox-fangst på.
- **Manuelle steg:** én `run_project` mot energi-bundelen, azure-profil; artefakter arkiveres;
utfall re-rulles IKKE (sammenligningsprotokoll §4.5).
- **Kost:** én kjøring innenfor taket; dokumenteres i rapport med konfig-logg.
### M3 — Fase 6-pilot (etter Fase 5 + M2 + D-B-amendment for live kilde)
- **Forutsetninger:** én ekte kilde (per §4.4-beslutningen) herdet og materialisert med ekte
provenance; én ekte fagekspert onboardet på inbox/outbox-kontrakten (samme mappe-grensesnitt
som personaen); ruting+varsling (S5.1/S5.2) i drift; ingest-målbilde-amendment på plass (P1).
- **Manuelle steg:** én dimensjon × ett prosjekt ende-til-ende; verifiser målbilde §7-sløyfa mot
virkeligheten; FØRST deretter skalering mot N×10.
---
## 5. Avhengighetsgraf + anbefalt sekvens
```mermaid
flowchart TB
subgraph DEC["Beslutnings-sesjoner (operatør)"]
DA["D-A spec-amendments"]; DB["D-B Fase2-scope/ingest-målbilde"]
DC["D-C vektor-store"]; DD["D-D concurrency"]; DE["D-E mål/paritet"]
end
S20["S2.0 portefølje-læring (F1)"]; S21["S2.1 outbox (P4)"]; S25["S2.5 inbox-herding (F7/F11)"]
S27["S2.7 validator-stramming (F2)"]; S22["S2.2 MCP-ingest"]; S23["S2.3 doc-konnektor"]; S24["S2.4 http-herding"]
S31["S3.1 semantisk henting"]; S32["S3.2 multi-kandidat (F5)"]; S33["S3.3 concurrent (F6)"]; S34["S3.4 kost på tvers (F10)"]
S40["S4.0 kostbaseline-forankring (F3)"]; S41["S4.1 preflight"]; S42["S4.2 live-drill"]
S51["S5.1 ruting/sporing"]; S52["S5.2 varsling"]; S53["S5.3 CLI-paritet"]
M1{{"M1 tenant+smoke"}}; M2{{"M2 første live-kjøring"}}; M3{{"M3 pilot"}}
DA --> S27; DA --> S32; DA --> S40
DB --> S22; DB --> S23; DB --> S24; DB -. amendment .-> M3
DC --> S31; DD --> S33; DD --> S34; DE --> M2
S20 --> S33; S20 --> S51; S21 --> S42; S21 --> S51
S41 --> M1; S40 --> M2; S42 --> M2; M1 --> M2
S51 --> S52; S20 --> S53; S21 --> S53
S33 --> M3; S34 --> M3; S52 --> M3; M2 --> M3; S31 --> M3; S32 --> M3
```
**Anbefalt sesjonssekvens** (parallellitet mulig der grafen tillater; én fase = Voyage-syklus):
1. **D-A, D-B** (kan tas i samme operatør-bolk; D-C/D-D/D-E kan vente)
2. **S2.0 → S2.1 → S2.5** (ingen beslutnings-avhengighet — kan starte FØR/parallelt med D-runden)
3. **S2.7** (etter D-A + commons-pull) · **S2.2 → S2.3 → S2.4** (etter D-B)
4. **D-C, D-D, D-E****S3.1 → S3.2 → S3.4 → S3.3**
5. **S4.1** (når som helst) → **M1** (operatør) · **S4.0 → S4.2****M2** (operatør)
6. **S5.1 → S5.2 → S5.3****M3** (operatør; krever også D-B-amendment + ekte ekspert)
**Offline-fullførings-grensen** går etter S5.3: alt over den linjen er gratis, autonomt og
load-bearing-testbart; M1M3 er de eneste stegene som koster penger, krever tenant eller krever
et menneske.