portfolio-optimiser/CLAUDE.md
Kjell Tore Guttormsen e2861cac0c feat(fase5): add the long/async verdict file inbox (Steg 7 resumable feedback)
The short loop captured the expert verdict inline into an in-memory store, so a
verdict arriving days/weeks later in a separate run could not influence any future
hypothesis (målbilde §5 row 7). Steg 7 adds the long timescale: run_project gains an
opt-in verdict_dir async inbox that load_verdicts_from_dir -> store.add MERGES into the
store BEFORE the Step-1 ExpeL fold, so a verdict dropped after an earlier run reaches a
separate, later run's hypothesis — fully resumable across runs separated in time.

- verdicts.py: verdict_to_dict / verdict_from_dict (id read verbatim, never re-minted),
  write_verdict (public authoring primitive, NOT wired into run_project — system reads
  the folder, expert/persona writes it, §3 role split), tolerant load_verdicts_from_dir
  (missing/foreign/half-written files skipped, not raised — RAW layer per §10 R2),
  VerdictStore.from_dir.
- run.py: verdict_dir kwarg; ingest-merge block after load_contracts (merge not replace
  keeps run_portfolio's cross-project threading; store.add idempotent on content-hash id;
  no change to the fold). CLI --bundle-dir/--verdict-dir thread the long loop to the
  console entry. No auto-persist of the run's own captured verdict (outbox/Steg 8).
- Load-bearing PAIR (test_step7_async_loop_loadbearing.py): a verdict dropped after run A
  must reach run B's prompt (run B uses a FRESH store -> the transfer is the file loop,
  not in-memory carryover); empty-inbox control proves causality. Marker = a realization
  value absent from the bundle (not the seed's 0.82). Proven RED on ingest detach.

Suite 138 -> 140 passed, 4 skipped; mypy + ruff check clean. Målbilde treated as frozen
(no §3/§5/§7 edit). Step 8 (gated wiki promotion) remains.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MHR8iKxJRxDiDfNw8HZmWE
2026-06-30 09:54:23 +02:00

78 lines
6.7 KiB
Markdown

# portfolio-optimiser
## Kontekst
Generisk, åpent Python-rammeverk på **Microsoft Agent Framework (MAF)** som finner kostnadsbesparelser INNI hvert prosjekt i en portefølje av uavhengige prosjekter. Multi-agent samarbeid genererer kandidat-tiltak; en obligatorisk deterministisk validator avgjør verdiene; fageksperter vurderer via HITL, og systemet lærer av dommene. Publiseres på Forgejo for alle som har MAF.
Bakgrunn og beslutninger: [research](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 via **MCP**-servere; data-source-konfig JSON-Schema-validert, fail-fast.
## Kommandoer
- Sync: `uv sync`
- 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).
- **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.
- **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).
- **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.
- **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.)