feat(1b): skjemaet ER akseptert av det levende endepunktet - malt, ikke resonnert [skip-docs]
Fase 1b siste trinn: forste bundne levende kjoring over HELE run_project-stien mot Foundry (gpt-4-1-mini). Okt 37s uttalte aerlighets-grense er lukket ved maling: ingen -parse-failures.json i outboksen, altsa parset hvert eneste genererings-svar. assumptions-normaliseringen virket ende-til-ende. Utfall: rejected pa P90 (claimed 34500 > feasible 11488), checker approve. Kjoringen KONKLUDERTE - per pre-registreringen det bestatte utfallet. De to falsifisererne skilte lag for forste gang mot en levende modell. FUNN storre enn den gronne testen: modellen fant opp kostkoden EL-LIGHTING-OP-HR (null treff i kunnskapsbasen). Avvisningen var riktig men skjedde pa 30%-cap-en, ikke stage 0 - bundelen shipper ingen cost-baseline.json, sa S4.0-forankringen var inaktiv. Ko-fort, ikke rettet her. Gate-designet er ovis beslutning, tatt for koding: - TREDJE distinkt opt-in PORTFOLIO_LIVE_FULL_RUN (truthiness, 4b-invarianten). Begge eksisterende live-tester gater pa SAMME to Foundry-variabler, sa gjenbruk ville latt den billige proben fyre den dyre kjoringen - stigen i maleprotokollen ville kollapset til ett trinn. MALT: den dyre SKIPPET med begge Foundry-variablene satt. - Asserten i EN kopi (conftest.assert_full_run_contract, ko-(p)), smal med vilje: fravaer av parse-failures-artefaktet + at validatoren avgjorde. En rejected BESTAR - pastanden er schema-aksept, ikke modell-dommekraft. - Iron Law uten a betale to ganger: diskrimineringen bevist OFFLINE av test_live_full_run_contract.py. To mutasjoner, hver sin signatur: detach artefakt-sjekken (T1 rod ALENE) - raise ubetinget (T2 rod ALENE). STATE-premiss korrigert: "test_foundry_profile_live dekker KUN klient-nivaet" var upresist - test_portfolio_live.py dekket allerede fan-outen, men dens len(runs)==1 kan ikke skille validert fra avvist og bar derfor ikke pastanden. 869 passed / 5 skipped (fra 867/4), 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:
parent
986fc19350
commit
2d1264088e
5 changed files with 408 additions and 0 deletions
23
CLAUDE.md
23
CLAUDE.md
|
|
@ -429,6 +429,29 @@ Python ≥3.10. MAF (`agent-framework-core` 1.9.0). Pakkehåndtering: `uv`. To b
|
||||||
`run.py` · endre avrundingsmodus · la `realize` gå utenom `to_ore`. **Ærlighets-grense:**
|
`run.py` · endre avrundingsmodus · la `realize` gå utenom `to_ore`. **Ærlighets-grense:**
|
||||||
`sum_claimed_saving_nok` (`run.py:_aggregate`) er BEVISST urørt — et float-NOK-rapportfelt som
|
`sum_claimed_saving_nok` (`run.py:_aggregate`) er BEVISST urørt — et float-NOK-rapportfelt som
|
||||||
aldri kvantiseres og aldri sammenlignes mot ledgeren, altså utenfor ordens-defekten.
|
aldri kvantiseres og aldri sammenlignes mot ledgeren, altså utenfor ordens-defekten.
|
||||||
|
- **En BETALT test får sin EGEN opt-in, og instrumentet bevises GRATIS (Fase 1b, siste trinn):**
|
||||||
|
`tests/test_full_run_live.py` kjører hele `run_project`-stien mot et ekte Foundry-deployment, og
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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:
|
||||||
|
fraværet av `{run_id}-parse-failures.json` (økt 35-invarianten «filens tilstedeværelse er
|
||||||
|
signalet») + at `validator_decision` avgjorde. **En `rejected` BESTÅR** — påstanden som felles er
|
||||||
|
at det strukturerte skjemaet ER akseptert av det levende endepunktet, ikke at modellen resonnerer
|
||||||
|
godt; å kreve `validated` ville vært en modell-dømmekraft-påstand ingen enkelt kjøring kan bære.
|
||||||
|
**Iron Law uten å betale to ganger:** et betalt kall kan ikke kjøres rødt-så-grønt, så
|
||||||
|
diskrimineringen bevises OFFLINE av `tests/test_live_full_run_contract.py` — to armer over samme
|
||||||
|
helper (én parse-feil → kontrakten MÅ feile; alle parser → MÅ passere). Load-bearing MÅLT, to
|
||||||
|
mutasjoner med hver sin distinkte signatur: detach artefakt-sjekken (T1 rød ALENE — kontrakten
|
||||||
|
degraderer da til `test_portfolio_live.py`s `len(runs)==1`-klasse) · raise ubetinget (T2 rød
|
||||||
|
ALENE — den motsatte vakuiteten, en live-test som bare kan bli rød). **Det betalte kallet er
|
||||||
|
MÅLINGEN, aldri beviset på at måleinstrumentet virker.**
|
||||||
- **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.
|
- **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.
|
||||||
- **Offline simulering = primært metode-bevis (kostnadsdrevet, erstatter §11.8):** operatøren kjører
|
- **Offline simulering = primært metode-bevis (kostnadsdrevet, erstatter §11.8):** operatøren kjører
|
||||||
IKKE MAF mot ekte modell (verken Azure/Foundry eller Ollama — API for begge repoene er for kostbart
|
IKKE MAF mot ekte modell (verken Azure/Foundry eller Ollama — API for begge repoene er for kostbart
|
||||||
|
|
|
||||||
|
|
@ -108,3 +108,107 @@ serverfeil. Det er ikke rettet her; det er notert.
|
||||||
den hostede flaten.
|
den hostede flaten.
|
||||||
4. Først når 1 og 2 er på plass: en ny bundet kjøring, og en gatet test som dekker **hele
|
4. Først når 1 og 2 er på plass: en ny bundet kjøring, og en gatet test som dekker **hele
|
||||||
`run_project`-stien** (den eksisterende gatede testen dekker kun klient-nivået).
|
`run_project`-stien** (den eksisterende gatede testen dekker kun klient-nivået).
|
||||||
|
|
||||||
|
**Rettelse til punkt 4, målt 14.08 (økt 40):** parentesen er upresis. `tests/test_portfolio_live.py`
|
||||||
|
fantes allerede og dekker `run_portfolio`-utfoldingen — altså hele `run_project`-stien — med samme
|
||||||
|
env-gate. Men den asserterer `len(result.runs) == 1`, og fordi `runs` og `failures` *partisjonerer*,
|
||||||
|
kan den kun skille «kjøringen fullførte» fra «kjøringen raiste». Den kan **ikke** skille et validert
|
||||||
|
forslag fra et avvist, og heller ikke en kjøring der svarene parset fra en der de ikke gjorde det.
|
||||||
|
Den bærer derfor ikke påstanden 1b skal felle. Punkt 4 står, men grunnen er en annen enn skrevet.
|
||||||
|
|
||||||
|
## 5. Pre-registrerte utfall (skrevet FØR kjøringen)
|
||||||
|
|
||||||
|
Denne kjøringen har ett formål: å felle den ene gjenstående ærlighets-grensen fra økt 37 — **at det
|
||||||
|
emitterte `response_format`-skjemaet ER akseptert av det LEVENDE endepunktet er uverifisert**;
|
||||||
|
testene beviser konformitet med Azures *dokumenterte* subset, ikke aksept.
|
||||||
|
|
||||||
|
**Instrumentet** er `conftest.assert_full_run_contract`, og diskriminatoren er et artefakt repoet
|
||||||
|
allerede eier: `{run_id}-parse-failures.json` skrives hvis og bare hvis et svar ikke lot seg parse
|
||||||
|
(økt 35). Artefaktets **fravær** ved siden av et `RunResult` beviser at hvert genererings-svar kom
|
||||||
|
tilbake i den bestilte formen. Kontraktens evne til å skille er bevist **offline og gratis**
|
||||||
|
(`tests/test_live_full_run_contract.py`, to mutasjoner med hver sin signatur: detach artefakt-sjekken
|
||||||
|
→ T1 rød alene; raise ubetinget → T2 rød alene). Det betalte kallet er *målingen*, ikke beviset på at
|
||||||
|
måleinstrumentet virker.
|
||||||
|
|
||||||
|
**Hva hvert utfall betyr — avgjort på forhånd:**
|
||||||
|
|
||||||
|
| Utfall | Betydning |
|
||||||
|
|---|---|
|
||||||
|
| Ingen parse-failure-artefakt + validatoren avgjorde (`validated` **eller** `rejected`) | **Ærlighets-grensen er felt.** Skjemaet ble akseptert av det levende endepunktet. En P90-avvisning er et *bestått* utfall — kjøringen KONKLUDERTE. |
|
||||||
|
| Parse-failure-artefaktet finnes | Skjemaet ble **ikke** honorert. Et FUNN, ikke et bevis — og denne gangen finnes den råe teksten (økt 35), så neste steg kan begrunnes i stedet for gjettes. |
|
||||||
|
| `BudgetExceeded` | Fortsatt ubevist, men artefaktet forklarer hvorfor. Samme form som 1a-kjøringen. |
|
||||||
|
|
||||||
|
**Taket heves IKKE.** `max_rounds`/`max_tokens` står på defaultene den første levende kjøringen døde
|
||||||
|
på: fyrer ledgeren igjen, er dét informasjon, og å heve taket ville brukt mer penger på en sti som
|
||||||
|
kanskje fortsatt er brukket.
|
||||||
|
|
||||||
|
**Gate-variabelen er en TREDJE, distinkt opt-in** (`PORTFOLIO_LIVE_FULL_RUN`, lest på *truthiness*).
|
||||||
|
Begge de eksisterende live-testene gater på nøyaktig `PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT` +
|
||||||
|
`PORTFOLIO_FOUNDRY_DEPLOYMENT`; å gjenbruke det paret ville betydd at en operatør som eksporterer de
|
||||||
|
to for å kjøre den **billige** ett-ords-proben også fyrer den dyre fullkjøringen — altså at stigen i
|
||||||
|
§1 kollapser til ett trinn. `PORTFOLIO_MODEL_MAP` er med i skip-betingelsen av en annen grunn:
|
||||||
|
uten den feiler kjøringen av en *konfigurasjons*-årsak som ser ut som en modell-feil.
|
||||||
|
|
||||||
|
## 6. Den bundne fullkjøringen — målt 14.08 kl. 19:54
|
||||||
|
|
||||||
|
Stigen på nytt, samme disiplin som §1. Alt under er kjørt.
|
||||||
|
|
||||||
|
| Trinn | Kommando | Utfall |
|
||||||
|
|---|---|---|
|
||||||
|
| 1a | `az account get-access-token --resource https://ai.azure.com` (uten rør) | exit 0, `expiresOn 21:07:40` |
|
||||||
|
| 1b | `az ad signed-in-user show` (ekte Graph-kall, ikke lokal cache) | exit 0 — gmail-kontoen, ikke jobbkontoen |
|
||||||
|
| 2 | `preflight --profile azure` | `preflight OK (azure)` |
|
||||||
|
| 3 | `pytest tests/test_foundry_profile_live.py` | **1 passed, 5,48 s** |
|
||||||
|
| 3b | samme kall, med `test_full_run_live.py` samlet | **SKIPPED** — det tredje flagget holder stigen |
|
||||||
|
| 4 | `pytest tests/test_full_run_live.py` (`PORTFOLIO_LIVE_FULL_RUN=1`) | **1 passed, 22,87 s** |
|
||||||
|
|
||||||
|
Trinn 3b er verdt å uttale: den dyre testen hoppet over **selv med begge Foundry-variablene satt**.
|
||||||
|
Det er den empiriske bekreftelsen på at det tredje opt-in-flagget gjør jobben sitt design lover.
|
||||||
|
|
||||||
|
### Ærlighets-grensen ER felt
|
||||||
|
|
||||||
|
Outboksen inneholder `-proposal.json`, `-outcome.json`, `-runconfig.json` — og **ingen
|
||||||
|
`-parse-failures.json`**. Hvert eneste genererings-svar fra `gpt-4.1-mini` kom tilbake som et
|
||||||
|
parsebart objekt i den bestilte formen. Økt 37s uttalte grense — *«at det emitterte skjemaet ER
|
||||||
|
akseptert av det LEVENDE endepunktet er IKKE verifisert»* — er dermed **lukket ved måling**, ikke
|
||||||
|
ved resonnement. Kontrasten til den første levende kjøringen er hele funnet: der brant tolv runder
|
||||||
|
på formatfeil og etterlot null tegn; her feilet ingen.
|
||||||
|
|
||||||
|
`assumptions` kom tilbake som forventet (`{"EL-LIGHTING-OP-HR": [10.0, 13.0]}`), altså virker økt 37s
|
||||||
|
additive wire-form → IR-map-normalisering ende-til-ende mot et levende endepunkt. Det var
|
||||||
|
beslutningen som holdt den stokastiske falsifisereren fra å gå inert, og den er nå prøvd i felt.
|
||||||
|
|
||||||
|
### Utfallet: `rejected` — og det er et bestått utfall
|
||||||
|
|
||||||
|
```
|
||||||
|
outcome_type: rejected
|
||||||
|
reason: claimed saving 34500 exceeds P90 feasible 11488
|
||||||
|
checker_verdict: approve
|
||||||
|
validator_decision: rejected
|
||||||
|
token_usage: 15 306
|
||||||
|
```
|
||||||
|
|
||||||
|
Kjøringen KONKLUDERTE. Per §5s pre-registrering er dette det positive utfallet: validatoren ble nådd
|
||||||
|
med en parsebar kandidat og avgjorde. **Og de to falsifisererne skilte lag for første gang mot en
|
||||||
|
levende modell** — checkeren godkjente *resonnementet*, validatoren avviste *tallene*. Nøyaktig den
|
||||||
|
uavhengigheten `checker_verdict` holdes atskilt fra `provenance.validator_decision` for.
|
||||||
|
|
||||||
|
### FUNN som er viktigere enn den grønne testen: modellen fant opp en kostkode
|
||||||
|
|
||||||
|
Forslaget bar `code: "EL-LIGHTING-OP-HR"` (3 000 × 11,5). **Den koden finnes ikke noe sted i
|
||||||
|
kunnskapsbasen** (`grep` over hele bundelen: null treff). Kunnskapsbasen instruerer eksplisitt
|
||||||
|
mappingen `ENERGI-TOTAL-EL`, 300 000 kWh × 1,00 NOK — modellen konstruerte i stedet sin egen
|
||||||
|
kostlinje med en egen enhet.
|
||||||
|
|
||||||
|
Avvisningen var derfor **riktig, men skjedde på feil gate**: 30 %-cap-en fanget den på *magnitude*
|
||||||
|
(34 500 > P90 11 488), ikke stage 0 på *eksistens*. Grunnen er en kjent og uttalt egenskap, ikke en
|
||||||
|
defekt: S4.0-forankringen aktiveres på bundle-stien KUN når bundelen shipper `cost-baseline.json`,
|
||||||
|
og `bygg-energi-mikro` gjør ikke det (målt) — «en pre-amendment-bundle er legitimt uforankret».
|
||||||
|
|
||||||
|
Dette er akkurat den hallusinasjons-klassen S4.0 ble bygget for, observert i felt for første gang.
|
||||||
|
At den uforankrede gaten fanget den likevel er betryggende; at den fanget den på den dyre gaten
|
||||||
|
i stedet for den billige er en kø-post, ikke noe som endres her.
|
||||||
|
|
||||||
|
**Fortsatt ikke bevist:** at systemet produserer et *validert* forslag mot en levende modell. Denne
|
||||||
|
kjøringen avviste — korrekt, og med en begrunnelse som kan leses. Én kjøring er én kjøring, og
|
||||||
|
`gpt-4.1-mini`s egnethet for proposer-rollen er ikke avgjort av den.
|
||||||
|
|
|
||||||
|
|
@ -192,3 +192,52 @@ def docs_dir(tmp_path) -> str:
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
return str(d)
|
return str(d)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
|
# Fase 1b — the FULL-RUN contract, in ONE copy (kø-(p): a second copy of an assertion drifts).
|
||||||
|
# ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
#: What the paid run must fell, stated as an assertion rather than as prose. The open honesty limit
|
||||||
|
#: after økt 37 is narrow and specific: *"that the emitted structured schema IS accepted by the LIVE
|
||||||
|
#: endpoint is NOT verified — the tests prove conformance with the DOCUMENTED subset, not
|
||||||
|
#: acceptance."* So the contract asserts schema ACCEPTANCE, never model JUDGEMENT.
|
||||||
|
#:
|
||||||
|
#: The discriminator is an artefact this repo already owns: ``{run_id}-parse-failures.json`` is
|
||||||
|
#: written if and ONLY if some reply failed to parse (økt 35 invariant — "the file's presence is the
|
||||||
|
#: signal"). Its ABSENCE beside a RunResult therefore proves that every generation reply came back
|
||||||
|
#: in the requested shape, which is exactly what "the live endpoint honoured the schema" means.
|
||||||
|
#:
|
||||||
|
#: ``validator_decision`` is the second half: it mirrors the VALIDATOR alone (never the checker), so
|
||||||
|
#: reading it proves the deterministic gate actually ran on a parsed candidate. Both ``validated``
|
||||||
|
#: 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
|
||||||
|
#: which one paid run could not establish anyway.
|
||||||
|
def assert_full_run_contract(result, outbox_dir, run_id: str) -> None:
|
||||||
|
"""Assert the Fase 1b full-run contract on a completed ``run_project`` result.
|
||||||
|
|
||||||
|
Two things, and deliberately nothing else:
|
||||||
|
|
||||||
|
1. **No parse-failure artefact** — every reply parsed, i.e. the live endpoint accepted the
|
||||||
|
emitted ``response_format`` schema. This is the honesty limit being felled.
|
||||||
|
2. **The validator was reached and decided** — ``provenance.validator_decision`` is one of the
|
||||||
|
two decisions the deterministic gate emits.
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
artefact = Path(outbox_dir) / f"{run_id}-parse-failures.json"
|
||||||
|
if artefact.exists():
|
||||||
|
# Quote the evidence in the failure message: the whole point of økt 35 was that the operator
|
||||||
|
# should never again have to guess WHY a reply did not parse.
|
||||||
|
raise AssertionError(
|
||||||
|
f"the endpoint did NOT honour the structured schema — {artefact.name} exists.\n"
|
||||||
|
f"{artefact.read_text(encoding='utf-8')[:2000]}"
|
||||||
|
)
|
||||||
|
assert result.provenance.validator_decision in {"validated", "rejected"}, (
|
||||||
|
"the deterministic validator never decided — the run did not reach the gate with a "
|
||||||
|
f"parsed candidate (validator_decision={result.provenance.validator_decision!r})"
|
||||||
|
)
|
||||||
|
|
|
||||||
86
tests/test_full_run_live.py
Normal file
86
tests/test_full_run_live.py
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
"""Fase 1b, last step — GATED live run over the WHOLE ``run_project`` path (måleprotokoll §4.4).
|
||||||
|
|
||||||
|
NOT default CI, and NOT gated like its two siblings. This is the expensive arm: it drives the
|
||||||
|
complete vertical slice — bundle navigation, the maker/checker debate, generation under
|
||||||
|
``response_format``, and the deterministic validator — against a real Foundry deployment.
|
||||||
|
|
||||||
|
**Why a THIRD environment variable, and why it is load-bearing.** ``test_foundry_profile_live.py``
|
||||||
|
(client-level probe) and ``test_portfolio_live.py`` (``run_portfolio`` fan-out) both skip on exactly
|
||||||
|
``PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT`` + ``PORTFOLIO_FOUNDRY_DEPLOYMENT``. Reusing that pair here
|
||||||
|
would mean the moment an operator exports the two variables to run the CHEAP one-word probe, this
|
||||||
|
full run fires too — collapsing the måleprotokoll's whole point (§1: *"bevis så mye som mulig før
|
||||||
|
det dyre trinnet, så en feil er attribuerbar"*) into a single step, and spending money on a rung
|
||||||
|
whose predecessors have not been shown green. ``PORTFOLIO_LIVE_FULL_RUN`` is therefore a separate,
|
||||||
|
deliberate opt-in, read on **truthiness, not presence** (the Fase 4b invariant: an exported-but-empty
|
||||||
|
value is a shell accident, not a decision).
|
||||||
|
|
||||||
|
``PORTFOLIO_MODEL_MAP`` is part of the skip condition for a different reason — attribution. ``run.py``
|
||||||
|
stamps provenance with the deployment NAME before any client is built (målt 4e), so without the map
|
||||||
|
the run fails for a CONFIGURATION reason while looking exactly like a model failure. Skipping is
|
||||||
|
honest; failing there would misattribute.
|
||||||
|
|
||||||
|
**What this asserts is narrow on purpose** — see ``conftest.assert_full_run_contract``. The claim
|
||||||
|
being felled is *"the emitted structured schema is accepted by the live endpoint"* (økt 37's stated
|
||||||
|
honesty limit), NOT "the model proposes well". A validator REJECTION passes this test: the run
|
||||||
|
reached the deterministic gate with a parsed candidate, which is the whole question. The contract's
|
||||||
|
ability to discriminate is proven offline and for free by
|
||||||
|
``tests/test_live_full_run_contract.py`` — the paid call here is the measurement, not the proof that
|
||||||
|
the instrument works.
|
||||||
|
|
||||||
|
Outcomes are pre-registered in ``docs/2026-08-14-fase1b-forste-levende-kjoring.md`` §5, written
|
||||||
|
BEFORE the run, so the write-up cannot be negotiated after the fact.
|
||||||
|
|
||||||
|
The round/token caps are the SAME ones the first live run died on. They are deliberately not
|
||||||
|
raised: if the ledger fires again that is information, and raising it spends more on a path that may
|
||||||
|
still be broken.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from conftest import assert_full_run_contract
|
||||||
|
|
||||||
|
from portfolio_optimiser.run import RunResult, run_project
|
||||||
|
from portfolio_optimiser.verdicts import VerdictStore
|
||||||
|
|
||||||
|
BUNDLE_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro"
|
||||||
|
_PROJECT_ID = "BYGG-KONTOR-NORD"
|
||||||
|
|
||||||
|
_ENDPOINT = os.environ.get("PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT")
|
||||||
|
_DEPLOYMENT = os.environ.get("PORTFOLIO_FOUNDRY_DEPLOYMENT")
|
||||||
|
_MODEL_MAP = os.environ.get("PORTFOLIO_MODEL_MAP")
|
||||||
|
#: Truthiness, not presence (Fase 4b): ``PORTFOLIO_LIVE_FULL_RUN=`` must NOT arm a paid run.
|
||||||
|
_OPTED_IN = bool(os.environ.get("PORTFOLIO_LIVE_FULL_RUN"))
|
||||||
|
|
||||||
|
_SKIP = not (_ENDPOINT and _DEPLOYMENT and _MODEL_MAP and _OPTED_IN)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
_SKIP,
|
||||||
|
reason=(
|
||||||
|
"paid full run not armed (set PORTFOLIO_LIVE_FULL_RUN=1 alongside "
|
||||||
|
"PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT + PORTFOLIO_FOUNDRY_DEPLOYMENT + PORTFOLIO_MODEL_MAP)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def test_full_run_reaches_the_validator_on_a_live_model(tmp_path: Path) -> None:
|
||||||
|
"""The whole slice against a real deployment: every generation reply must come back in the
|
||||||
|
requested shape, and the deterministic validator must decide on it."""
|
||||||
|
outbox_dir = tmp_path / "outbox"
|
||||||
|
run_id = "live-full-001"
|
||||||
|
|
||||||
|
result = await run_project(
|
||||||
|
_PROJECT_ID,
|
||||||
|
"azure",
|
||||||
|
docs_dir=str(BUNDLE_DIR),
|
||||||
|
bundle_dir=str(BUNDLE_DIR),
|
||||||
|
verdict_input={"decision": "approved", "rationale": "expert reviewed (live 1b)"},
|
||||||
|
store=VerdictStore(verdicts=[]),
|
||||||
|
outbox_dir=str(outbox_dir),
|
||||||
|
run_id=run_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result, RunResult)
|
||||||
|
assert_full_run_contract(result, outbox_dir, run_id)
|
||||||
146
tests/test_live_full_run_contract.py
Normal file
146
tests/test_live_full_run_contract.py
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
"""The Fase 1b full-run contract must DISCRIMINATE — proven offline, for free (Fase 1b, last step).
|
||||||
|
|
||||||
|
``tests/test_full_run_live.py`` spends real money on a real endpoint, exactly once, and cannot be
|
||||||
|
run red-then-green: the paid call is the MEASUREMENT, not the proof that the measuring instrument
|
||||||
|
works. This file is that proof, and it costs nothing.
|
||||||
|
|
||||||
|
The repo's own rule — *"en test som ikke kan skille to implementasjoner beviser ingenting"* — has
|
||||||
|
been violated by this project's own measuring instruments three times (økt 27's slide sweep read
|
||||||
|
``section``'s 100vh and reported the same number for all eleven slides; økt 37's T3 asserted on a
|
||||||
|
schema the scripted client ignores). An assertion that can only ever pass is the same defect class,
|
||||||
|
and a LIVE assertion is the worst place to discover it, because a green result there is precisely
|
||||||
|
what the operator would act on.
|
||||||
|
|
||||||
|
So both arms drive the SAME ``assert_full_run_contract`` helper the live test uses (ONE copy, in
|
||||||
|
``conftest`` — a second copy would drift, kø-(p)), over the SAME bundle and the SAME project as the
|
||||||
|
live run, through the canonical ``ScriptedChatClient``'s ``reply_selector`` seam:
|
||||||
|
|
||||||
|
* T1 — a run in which ONE reply failed to parse must make the contract FAIL. This is the arm that
|
||||||
|
matters: it is the offline stand-in for "the live endpoint ignored ``response_format``", which is
|
||||||
|
the single outcome the paid run exists to rule out. Per the repo rule for negative asserts, it
|
||||||
|
first PROVES the event happened (the artefact exists) rather than inferring it from the failure.
|
||||||
|
* T2 — the CONTROL: a run in which every reply parsed must make the contract PASS. Without it, a
|
||||||
|
helper that raised unconditionally would satisfy T1 and the live test could then only ever be red,
|
||||||
|
which is the mirror-image vacuity.
|
||||||
|
|
||||||
|
Together they show the contract keys on the ACTUAL discriminator (the parse-failure artefact), not
|
||||||
|
on something both runs share.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from conftest import assert_full_run_contract
|
||||||
|
|
||||||
|
from portfolio_optimiser.budget import Budget, TokenMeter
|
||||||
|
from portfolio_optimiser.run import RunResult, run_project
|
||||||
|
from portfolio_optimiser.simulation import ScriptedChatClient
|
||||||
|
from portfolio_optimiser.verdicts import VerdictStore
|
||||||
|
|
||||||
|
BUNDLE_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro"
|
||||||
|
_PROJECT_ID = "BYGG-KONTOR-NORD"
|
||||||
|
_VERDICT_INPUT = {"decision": "approved", "rationale": "expert reviewed (contract probe)"}
|
||||||
|
|
||||||
|
#: The line ``generate._build_messages`` puts in EVERY generation prompt and nowhere else — the one
|
||||||
|
#: identifier separating a generation call from a debate turn (borrowed from
|
||||||
|
#: ``test_parse_failure_capture_loadbearing``, whose seam this file shares).
|
||||||
|
_GENERATION_MARK = "Respond with ONLY a JSON object"
|
||||||
|
|
||||||
|
#: Prose where an object was requested: what a model that ignored ``response_format`` returns.
|
||||||
|
_MALFORMED = (
|
||||||
|
"Sure! Here is what I found for this project.\n\n"
|
||||||
|
"The main opportunity looks like demand-controlled lighting, worth roughly 30 000 NOK.\n"
|
||||||
|
"Let me know if you want that as JSON."
|
||||||
|
)
|
||||||
|
|
||||||
|
#: BYGG-KONTOR-NORD: affected total 300000 x 1.0 -> degenerate Monte Carlo P90 = 90000, so a claim
|
||||||
|
#: of 30000 validates (same arithmetic as ``test_parse_failure_capture_loadbearing``).
|
||||||
|
_VALID_REPLY = (
|
||||||
|
'{"measure":"Behovsstyrt belysning i fellesarealer","affected_items":'
|
||||||
|
'[{"code":"ENERGI-TOTAL-EL","quantity":300000,"unit_cost":1.0}],'
|
||||||
|
'"claimed_saving_nok":30000}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _malformed_then_valid(failures: int) -> Callable[[str, str], str]:
|
||||||
|
"""Fail to parse ``failures`` times, then answer with a proposal that validates. Keyed on the
|
||||||
|
GENERATION prompt only, so debate turns — which are never parsed — do not consume the counter."""
|
||||||
|
seen = {"n": 0}
|
||||||
|
|
||||||
|
def _select(blob: str, _role: str) -> str:
|
||||||
|
if _GENERATION_MARK not in blob:
|
||||||
|
return "ok"
|
||||||
|
seen["n"] += 1
|
||||||
|
return _MALFORMED if seen["n"] <= failures else _VALID_REPLY
|
||||||
|
|
||||||
|
return _select
|
||||||
|
|
||||||
|
|
||||||
|
async def _run(select: Callable[[str, str], str], outbox_dir: Path, run_id: str) -> RunResult:
|
||||||
|
def factory(role: str) -> ScriptedChatClient:
|
||||||
|
return ScriptedChatClient(role=role, reply_selector=select, default_reply="ok")
|
||||||
|
|
||||||
|
result = await run_project(
|
||||||
|
_PROJECT_ID,
|
||||||
|
"local",
|
||||||
|
docs_dir=str(BUNDLE_DIR),
|
||||||
|
bundle_dir=str(BUNDLE_DIR),
|
||||||
|
verdict_input=_VERDICT_INPUT,
|
||||||
|
store=VerdictStore(verdicts=[]),
|
||||||
|
client_factory=factory,
|
||||||
|
outbox_dir=str(outbox_dir),
|
||||||
|
run_id=run_id,
|
||||||
|
meter=TokenMeter(Budget(max_tokens=10**9, max_rounds=8)),
|
||||||
|
)
|
||||||
|
assert isinstance(result, RunResult)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------------------------
|
||||||
|
# T1 — the arm that matters: an unparsed reply must FAIL the contract.
|
||||||
|
# --------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def test_contract_fails_when_a_reply_did_not_parse(tmp_path: Path) -> None:
|
||||||
|
"""The offline stand-in for "the live endpoint ignored the schema". The run itself SUCCEEDS —
|
||||||
|
the second reply validates — so the contract cannot be keying on the run's outcome; the only
|
||||||
|
thing separating this from T2 is the parse-failure artefact.
|
||||||
|
|
||||||
|
RED (i.e. this test fails) on a contract that ignores the artefact and merely checks that a
|
||||||
|
RunResult came back — which is what ``test_portfolio_live.py``'s ``len(runs) == 1`` does, and
|
||||||
|
the reason that existing gated test could not carry this claim."""
|
||||||
|
outbox_dir = tmp_path / "outbox"
|
||||||
|
run_id = "contract-dirty"
|
||||||
|
|
||||||
|
result = await _run(_malformed_then_valid(1), outbox_dir, run_id)
|
||||||
|
|
||||||
|
# Prove the event happened FIRST — a negative assert that merely observes a failure cannot tell
|
||||||
|
# "the contract caught the artefact" from "the contract failed for some unrelated reason".
|
||||||
|
artefact = outbox_dir / f"{run_id}-parse-failures.json"
|
||||||
|
assert artefact.exists(), "precondition: this arm must actually produce a parse failure"
|
||||||
|
assert result.provenance.validator_decision in {"validated", "rejected"}, (
|
||||||
|
"precondition: the run must otherwise CONCLUDE, so the artefact is the only difference"
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(AssertionError, match="did NOT honour the structured schema"):
|
||||||
|
assert_full_run_contract(result, outbox_dir, run_id)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------------------------
|
||||||
|
# T2 — the CONTROL: a clean run must PASS.
|
||||||
|
# --------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def test_contract_passes_when_every_reply_parsed(tmp_path: Path) -> None:
|
||||||
|
"""Without this control, a helper that raised unconditionally would satisfy T1, and the live
|
||||||
|
test could then only ever be red — the mirror image of a check that can only ever be green."""
|
||||||
|
outbox_dir = tmp_path / "outbox"
|
||||||
|
run_id = "contract-clean"
|
||||||
|
|
||||||
|
result = await _run(_malformed_then_valid(0), outbox_dir, run_id)
|
||||||
|
|
||||||
|
assert not (outbox_dir / f"{run_id}-parse-failures.json").exists()
|
||||||
|
assert_full_run_contract(result, outbox_dir, run_id)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue