fix(1b): regelen gjelder HVER betalt arm - fan-out-testen var fortsatt ugatet [skip-docs]

Forrige commit skrev en CLAUDE.md-invariant om at en betalt test far sin egen
opt-in, men gatet bare den NYE testen. test_portfolio_live.py passerer ingen
client_factory og er derfor selv en betalt kjoring - den fyrte pa
to-variabel-paret fra et bart `uv run pytest`. Invarianten var altsa halvt
usann den dagen den ble skrevet, som er nyaktig Fase 3-klassen (en pastand
flaten gjor om seg selv uten dekning).

MALT etter fiksen, med begge Foundry-vars satt og UTEN PORTFOLIO_LIVE_FULL_RUN:
  test_foundry_profile_live   PASSED   (det billige trinnet, med vilje pa to vars)
  test_portfolio_live         SKIPPED  (var betalt og ugatet for dette)
  test_full_run_live          SKIPPED

Ovrige oppfolgingspunkter fra review:
- assert_full_run_contract far ekte type hints (repoets "type hints overalt";
  mypy src ser ikke tests/, sa den ville rotnet ubemerket).
- Maleprotokollen SS6 far oppsettet STATE peker pa - model-map-formen og de fire
  export-linjene. Pekeren lovet noe seksjonen ikke hadde.

869 passed / 5 skipped, uendret. ruff+format+mypy rene.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GF7va4cpRiuf79kTzAi3vW
This commit is contained in:
Kjell Tore Guttormsen 2026-08-14 20:12:47 +02:00
commit bb4807aff7
4 changed files with 56 additions and 9 deletions

View file

@ -434,11 +434,17 @@ Python ≥3.10. MAF (`agent-framework-core` 1.9.0). Pakkehåndtering: `uv`. To b
gates på **fire** ting — de to Foundry-variablene, `PORTFOLIO_MODEL_MAP`, og et TREDJE, distinkt gates på **fire** ting — de to Foundry-variablene, `PORTFOLIO_MODEL_MAP`, og et TREDJE, distinkt
`PORTFOLIO_LIVE_FULL_RUN` lest på **truthiness** (4b-invarianten). **Den tredje variabelen er `PORTFOLIO_LIVE_FULL_RUN` lest på **truthiness** (4b-invarianten). **Den tredje variabelen er
load-bearing, ikke pynt:** `test_foundry_profile_live.py` (klient-probe) og `test_portfolio_live.py` load-bearing, ikke pynt:** `test_foundry_profile_live.py` (klient-probe) og `test_portfolio_live.py`
(fan-out) gater på nøyaktig SAMME to variabler, så å gjenbruke det paret ville betydd at en (fan-out) gatet på nøyaktig SAMME to variabler, så å gjenbruke det paret ville betydd at en
operatør som eksporterer dem for den BILLIGE ett-ords-proben også fyrer den dyre fullkjøringen — operatør som eksporterer dem for den BILLIGE ett-ords-proben også fyrer den dyre fullkjøringen —
altså at måleprotokollens stige («bevis så mye som mulig før det dyre trinnet, så en feil er altså at måleprotokollens stige («bevis så mye som mulig før det dyre trinnet, så en feil er
attribuerbar») kollapser til ett trinn. **MÅLT:** med begge Foundry-variablene satt SKIPPET den attribuerbar») kollapser til ett trinn. **MÅLT:** med begge Foundry-variablene satt SKIPPET den
dyre, og den billige var grønn. `PORTFOLIO_MODEL_MAP` er med av en annen grunn — attribusjon: uten dyre, og den billige var grønn. **Regelen gjelder HVER betalt arm, ellers er den ingen regel:**
`test_portfolio_live.py` passerer ingen `client_factory` og er derfor selv en betalt kjøring — den
fyrte på to-variabel-paret fra et bart `uv run pytest`, og ble gatet på den TREDJE variabelen i
samme slengen. Å la den stå ville gjort denne raden halvt usann den dagen den ble skrevet; en
invariant som beskriver én av to armer er en påstand flaten gjør om seg selv uten dekning, som er
nøyaktig Fase 3-klassen. Den billige klient-proben beholder to-variabel-gaten med vilje — den ER
det billige trinnet. `PORTFOLIO_MODEL_MAP` er med av en annen grunn — attribusjon: uten
den feiler kjøringen av en KONFIGURASJONS-årsak som ser ut som en modell-feil. den feiler kjøringen av en KONFIGURASJONS-årsak som ser ut som en modell-feil.
**Asserten bor i ÉN kopi** (`conftest.assert_full_run_contract`, kø-(p)) og er smal med vilje: **Asserten bor i ÉN kopi** (`conftest.assert_full_run_contract`, kø-(p)) og er smal med vilje:
fraværet av `{run_id}-parse-failures.json` (økt 35-invarianten «filens tilstedeværelse er fraværet av `{run_id}-parse-failures.json` (økt 35-invarianten «filens tilstedeværelse er

View file

@ -151,6 +151,35 @@ uten den feiler kjøringen av en *konfigurasjons*-årsak som ser ut som en model
## 6. Den bundne fullkjøringen — målt 14.08 kl. 19:54 ## 6. Den bundne fullkjøringen — målt 14.08 kl. 19:54
### Oppsettet, reproduserbart
Model-mappet ligger **out-of-tree** med vilje: `data/model_map.json` i treet bærer
`REPLACE-WITH-FOUNDRY-DEPLOYMENT`-plassholdere, og tenant-spesifikke deployment-navn skal aldri
committes (B12). `PORTFOLIO_MODEL_MAP` peker på en fil operatøren eier:
```json
{
"local": {"default": "qwen3:4b", "proposer": "qwen3:4b", "checker": "qwen3:4b"},
"azure": {
"default": "gpt-4-1-mini",
"proposer": "gpt-4-1-mini",
"checker": "gpt-4-1-mini"
}
}
```
```bash
export PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT="https://po-foundry-ktg.services.ai.azure.com/api/projects/po-project"
export PORTFOLIO_FOUNDRY_DEPLOYMENT="gpt-4-1-mini"
export PORTFOLIO_MODEL_MAP="/absolutt/sti/til/model_map.live.json"
export PORTFOLIO_LIVE_FULL_RUN=1 # BETALER — utelat den for alt annet enn trinn 4
```
Merk at `PORTFOLIO_LIVE_FULL_RUN` bevisst settes SIST og alene for det dyre trinnet: uten den er
trinn 2 og 3 gratis-nok til å kjøres fritt, og det er hele stigens poeng.
### Stigen
Stigen på nytt, samme disiplin som §1. Alt under er kjørt. Stigen på nytt, samme disiplin som §1. Alt under er kjørt.
| Trinn | Kommando | Utfall | | Trinn | Kommando | Utfall |

View file

@ -10,11 +10,13 @@ from __future__ import annotations
import json import json
from collections.abc import Callable, Sequence from collections.abc import Callable, Sequence
from pathlib import Path
import pytest import pytest
from agent_framework import BaseChatClient from agent_framework import BaseChatClient
from portfolio_optimiser.reference_domain import load_reference_projects from portfolio_optimiser.reference_domain import load_reference_projects
from portfolio_optimiser.run import RunResult
from portfolio_optimiser.simulation import ScriptedChatClient from portfolio_optimiser.simulation import ScriptedChatClient
from portfolio_optimiser.verdicts import VerdictStore, seed_store from portfolio_optimiser.verdicts import VerdictStore, seed_store
@ -214,7 +216,7 @@ def docs_dir(tmp_path) -> str:
#: and ``rejected`` satisfy the contract — a P90 rejection is a run that CONCLUDED, and demanding #: and ``rejected`` satisfy the contract — a P90 rejection is a run that CONCLUDED, and demanding
#: ``validated`` would be asserting that the model reasons well, which no schema can promise and #: ``validated`` would be asserting that the model reasons well, which no schema can promise and
#: which one paid run could not establish anyway. #: which one paid run could not establish anyway.
def assert_full_run_contract(result, outbox_dir, run_id: str) -> None: def assert_full_run_contract(result: RunResult, outbox_dir: str | Path, run_id: str) -> None:
"""Assert the Fase 1b full-run contract on a completed ``run_project`` result. """Assert the Fase 1b full-run contract on a completed ``run_project`` result.
Two things, and deliberately nothing else: Two things, and deliberately nothing else:
@ -227,8 +229,6 @@ def assert_full_run_contract(result, outbox_dir, run_id: str) -> None:
Deliberately NOT asserted: ``checker_verdict``, token counts, ``validated_count``, or the Deliberately NOT asserted: ``checker_verdict``, token counts, ``validated_count``, or the
content of the proposal. Those are model-judgement claims, and one run cannot carry them. content of the proposal. Those are model-judgement claims, and one run cannot carry them.
""" """
from pathlib import Path
artefact = Path(outbox_dir) / f"{run_id}-parse-failures.json" artefact = Path(outbox_dir) / f"{run_id}-parse-failures.json"
if artefact.exists(): if artefact.exists():
# Quote the evidence in the failure message: the whole point of økt 35 was that the operator # Quote the evidence in the failure message: the whole point of økt 35 was that the operator

View file

@ -2,8 +2,16 @@
NOT default CI: skips cleanly without a configured Foundry endpoint + deployment. When set, NOT default CI: skips cleanly without a configured Foundry endpoint + deployment. When set,
``run_portfolio`` fans out over the reference portfolio on the REAL ``azure`` profile (one ``run_portfolio`` fans out over the reference portfolio on the REAL ``azure`` profile (one
project, hard token-capped per D6). Mirrors ``test_foundry_profile_live.py:14-24``. SC9 is project, hard token-capped per D6). SC9 is unchanged this arm stays skipped offline.
unchanged this arm stays skipped offline.
**``PORTFOLIO_LIVE_FULL_RUN`` added 14.08 (økt 40), and the reason is this file, not the new one.**
This test passes NO ``client_factory``, so it drives the real Azure backend: it is a PAID run, and
until now it fired on the same two variables as ``test_foundry_profile_live.py``'s one-word probe.
That is exactly the collapse the new full-run gate was designed to prevent an operator exporting
the pair to run the CHEAP rung would have paid for this fan-out too, from a bare ``uv run pytest``.
The rule the sibling file states must hold for EVERY paid arm, or it is not a rule; leaving this one
on the two-var pair would have made the CLAUDE.md invariant half-true the day it was written.
Truthiness, not presence (Fase 4b): an exported-but-empty value must not arm a paid run.
""" """
import os import os
@ -14,12 +22,16 @@ from portfolio_optimiser.run import PortfolioResult, run_portfolio
_ENDPOINT = os.environ.get("PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT") _ENDPOINT = os.environ.get("PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT")
_DEPLOYMENT = os.environ.get("PORTFOLIO_FOUNDRY_DEPLOYMENT") _DEPLOYMENT = os.environ.get("PORTFOLIO_FOUNDRY_DEPLOYMENT")
_NO_FOUNDRY = not (_ENDPOINT and _DEPLOYMENT) _OPTED_IN = bool(os.environ.get("PORTFOLIO_LIVE_FULL_RUN"))
_NO_FOUNDRY = not (_ENDPOINT and _DEPLOYMENT and _OPTED_IN)
@pytest.mark.skipif( @pytest.mark.skipif(
_NO_FOUNDRY, _NO_FOUNDRY,
reason="Foundry not configured (set PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT + PORTFOLIO_FOUNDRY_DEPLOYMENT)", reason=(
"paid fan-out not armed (set PORTFOLIO_LIVE_FULL_RUN=1 alongside "
"PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT + PORTFOLIO_FOUNDRY_DEPLOYMENT)"
),
) )
async def test_portfolio_live_azure_fanout() -> None: async def test_portfolio_live_azure_fanout() -> None:
# No client_factory -> the real AZURE backend is used; hard-capped per D6. # No client_factory -> the real AZURE backend is used; hard-capped per D6.