ANTHROPIC_API_KEY is now the ONLY accepted credential. Through v0.1.0 the preflight cleared on CLAUDE_CODE_OAUTH_TOKEN, and run_s10 went further: an unset key printed "note: relying on the CLI's own credentials" and carried on. That note was not a warning, it was a decision - made silently, on the operator's behalf, about who pays. Both paths are gone; a run with no key refuses with exit 2 before anything is opened. Red first, both halves: _check_credentials refuses an OAuth-only env, and the run entrance is driven as a real subprocess with a deliberately missing bundle, so the credential refusal must win the race against the bundle error. Detach it and the process reaches navigate_bundle instead - a different exit code, no refusal line, the fallback back in the output. The positive control (key set) gets past the gate and fails on the bundle, so the gate is a gate and not a wall. 997 -> 1002, offline, no key in env. The SDK exception is now stated where a reader meets it, not implied: this framework runs on the Claude Agent SDK, which starts the Claude Code CLI it bundles as a subprocess. That is the SDK's intended use WITH an API key, and it is a deliberate, stated exception to the owner's rule that his own code never starts Claude Code. Rewriting to direct HTTP calls was weighed and declined - measuring what the Agent SDK offers is the point of D7. The repo is closed as a worked example. Two prose claims were corrected rather than left standing: run_s10.py is no longer byte-frozen (it carries exactly one change, and runs/s10/ is still the v0.1.0 run), and its two round() call sites moved 110->118, 130->138. The credential paragraph is prose under an existing heading, not a new section: test_readme_anchors_loadbearing.py pins 14 heading ids MEASURED on the published page and forbids re-deriving them. This order forbids push, so a new heading could not have been honestly re-measured. Version 0.1.1: pyproject.toml, uv.lock self-entry, CHANGELOG - 3 of 3. No version badge in README, no constant in src. v0.1.0 stands as released. Order 20260920T131502Z-7496226791-from-.claude. The older D7 mirroring order 20260913T053840Z-9473220509 is retired unexecuted: po closes at v1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
97 lines
6.5 KiB
Markdown
97 lines
6.5 KiB
Markdown
# portfolio-optimiser-claude
|
||
|
||
## Kontekst
|
||
Søsken-implementasjon (beslutning D7) av portfolio-optimiser-metoden på **Claude Agent SDK**:
|
||
generisk, åpent Python-rammeverk som finner kostnadsbesparelser INNI hvert prosjekt i en
|
||
portefølje. Bygges på **metode-spec-en** (`shared/method-spec.md`) som normativ kilde.
|
||
**Åpen konkurranse med MAF-søskenet** (`~/repos/portfolio-optimiser`, operatørbeslutning
|
||
2026-08-09): alle tilgjengelige midler er lov for å nå den beste løsningen, inkludert å lese
|
||
søskenets kode, og utvekslingen kan gå begge veier. **Grensen er hensikten, ikke kilden:**
|
||
kopiér kun der det faktisk gir mening for løsningen — **aldri som snarvei**. De reelle
|
||
begrensningene er hva MAF og Claude Agent SDK hver for seg tilbyr, og det er nettopp den
|
||
forskjellen sammenligningen skal måle. Fram til 2026-08-09 ble repoet bygget fra speccen
|
||
alene; det er datert i README og skal ikke påstås om arbeid etter den datoen. Program-planer
|
||
(sesjonsplan S5–S12, sammenligningsprotokoll, frossent målbilde) bor i hovedrepoets
|
||
`docs/plan/`. Løpende state: `STATE.md` (local-only).
|
||
|
||
## Stack
|
||
Python ≥3.10. **Claude Agent SDK** (`claude-agent-sdk` ≥0.2, kildeverifisert 0.2.139 per
|
||
2026-08-18 — pinnen `>=0.2.111,<0.3` er en TILLATELSE, `_VERIFIED_THROUGH` i
|
||
`tests/test_sdk_version_guard.py` er beviset og går rødt på en ulest nyere build. Per
|
||
2026-07-03: MIT-lisens, bundler Claude Code CLI — ingen separat installasjon; import og
|
||
konstruksjon fungerer uten API-nøkkel, nøkkel trengs kun ved faktisk `query()`).
|
||
Pakkehåndtering: `uv`. Pydantic for kontrakts-validering.
|
||
|
||
## Konvensjoner
|
||
- Type hints overalt; `mypy --strict`. `ruff` for lint+format. `pytest` for test.
|
||
- **Testsuiten kjører uten API-nøkkel og uten nettverk** (invariant fra S5; håndhevet ved at
|
||
all kontrakts-/kontekst-logikk er ren config/fil-lag).
|
||
- Modell-valg som konfig: `src/portfolio_optimiser_claude/data/model_map.json`
|
||
(rolle→Claude-modell-id per backend-profil, hver profil krever `default`; validert av
|
||
`contracts.py`). Modell-id-er verifiseres mot offisiell doc før endring.
|
||
- SDK-primitiver (verifisert 2026-07-03): `query()`/`ClaudeSDKClient`, `ClaudeAgentOptions`
|
||
(`max_turns`, `max_budget_usd` — førsteklasses USD-tak), `AgentDefinition` (subagenter;
|
||
sub-felt er camelCase), `@tool` + `create_sdk_mcp_server` (in-process MCP), hooks
|
||
(`PreToolUse` med `permissionDecision: "deny"` for deterministisk gating). MERK:
|
||
`SessionStart`/`SessionEnd` finnes IKKE som Python-callback-hooks.
|
||
- `shared/` er en **git subtree** av
|
||
[`portfolio-optimiser-commons`](https://git.fromaitochitta.com/open/portfolio-optimiser-commons)
|
||
(source of truth; remote `commons`). 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
|
||
i MAF-repoet 2026-07-03). Se `shared/README.md`.
|
||
|
||
## Kommandoer
|
||
- Sync: `uv sync`
|
||
- Test: `uv run pytest`
|
||
- Lint: `uv run ruff check .` + `uv run ruff format .`
|
||
- Type: `uv run mypy src scripts`
|
||
- Bevis en søm: `uv run python scripts/mutation_harness.py --help` (grønn-før/rød-etter)
|
||
|
||
## Arbeidsflyt (invarianter)
|
||
- **Rent teknisk rammeverk:** deployer eier DPIA/ROS/behandlingsformål — kun tekniske
|
||
forutsetninger (lokal-only, provenance, ingen stille egress) + disclaimer.
|
||
- **Metode-spec-en er normativ** (RFC 2119). Ærlighets-regelen (§1) er ufravikelig: ingen
|
||
artefakt påstår mer enn implementasjonen gjør; skriptede stand-ins merkes som det.
|
||
- **Deterministisk validator obligatorisk + blokkerende** (§3 steg 4); golden-suiten
|
||
(`shared/examples/bygg-energi-mikro/golden.json`) er ENESTE fasit (§7).
|
||
- **Mutasjonsharnesset er verktøyet, ikke en huskeregel** (beslutning 2026-08-25, etter ti
|
||
håndbygg): `scripts/mutation_harness.py` kjører grønn-før/rød-etter mot det EKTE treet og
|
||
håndhever de fire kontrollene som ellers svikter stille — unikt anker, node-id-en faktisk
|
||
kjørt (pytest rc=4 er IKKE rødt), restaurering sha256-verifisert fra disk, og rødhet på en
|
||
pinnet linje. Tre roller, og rekkefølgen følger av hva hver måler: `--red` grønn FØR
|
||
(måleapparatets positivkontroll) og rød ETTER (selve målingen, ikke besvarbar før
|
||
mutasjonen finnes), `--green` grønn i BEGGE (treffer den, landet mutasjonen for bredt).
|
||
Verktøyet ligger bevisst UTENFOR `tests/` — suiten kjører hver økt, og en fixture som
|
||
skriver til disk gjør enhver avbrutt kjøring til et mutert tre. Mutasjon av en KOPI i minnet
|
||
hører derimot hjemme i suiten og er der alt (`test_guard_red_when_*`).
|
||
- **Load-bearing tester** (§11): hver søm bevises med en test som blir RØD når sømmen
|
||
detaches — grønn-men-død er feilmoden regelen finnes for.
|
||
- **Stoppkriterier + budsjett-tak påkrevd ved oppstart** (§8; `contracts.py` fail-fast).
|
||
På kjørestien kommer USD-taket i tillegg via `ClaudeAgentOptions.max_budget_usd`.
|
||
- **Kostnadsdisiplin (D6):** alt utvikles offline/deterministisk; den ENE ekte API-kjøringen
|
||
i programmet er S10 (mikro-bundle, billigste egnede modell — per model_map Haiku 4.5 —
|
||
harde tak, kost loggføres).
|
||
- **Kun egen API-nøkkel (v0.1.1):** `ANTHROPIC_API_KEY` er ENESTE godtatte legitimasjon.
|
||
Et abonnements-token (`CLAUDE_CODE_OAUTH_TOKEN`) AVVISES av preflight og av kjøre-
|
||
inngangen, som ender i exit ≠ 0 — aldri et stille tilbakefall på CLI-ens innlogging.
|
||
- **Uttalt unntak (ikke skjult):** repoet er bygget på Claude Agent SDK, som selv starter
|
||
den medfølgende Claude Code-CLI-en som underprosess. Det er Anthropics tiltenkte bruk MED
|
||
API-nøkkel, og står som et UTTALT unntak fra eierens regel om at egen kode ikke starter
|
||
Claude Code. Omskriving til rene API-kall er vurdert og valgt bort — poenget med D7 er å
|
||
måle hva Agent SDK-en tilbyr. **Repoet er AVSLUTTET som eksempel:** ingen videre utvikling
|
||
er planlagt.
|
||
- **STATE.md er local-only** (gitignored — repoet skal publiseres; STATE når aldri et
|
||
offentlig speil).
|
||
|
||
## Communication patterns
|
||
|
||
### Linking to local files
|
||
|
||
When pointing to local files in responses, always use markdown link syntax with a descriptive name:
|
||
|
||
- Use `[Human-friendly name](file:///absolute/path)` — never bare `file:///...` URLs or autolinks `<file://...>`.
|
||
- Always use absolute paths. Never `~/` or relative paths.
|
||
- For multiple files, render as a bullet list of named markdown links.
|
||
|
||
Why: bare `file://` URLs only render the first as clickable across multiple lines. Named markdown links make each entry independently clickable and look cleaner.
|