fix(5): preflight kjenner samme endepunkt-variabler som kjørestien [skip-docs]

Målt fra den utpakkede overleveringspakka: med KUN plattformens injiserte
FOUNDRY_PROJECT_ENDPOINT — altså nøyaktig situasjonen i en hostet Foundry-container —
avslo preflight en konfigurasjon backends.py ville godtatt. Gaten og kjørestien kjente
ulike navn; det er repoets egen «checker og kjøresti validerer ulikt»-klasse, og for
mottakeren av pakka er det et falskt avslag på riktig oppsett.

_ENDPOINT_ENVS IMPORTERES nå fra backends i stedet for å gjentas, så de to kan ikke
drifte fra hverandre igjen. Presedens over VERDIER, ikke deklarasjoner: et eksportert-men-
tomt eget navn faller igjennom i stedet for å skygge et ekte injisert inn i en fail-fast.
Avslaget navngir BEGGE variablene.

Iron Law: 3 røde diskriminatorer + 1 grønn kontroll FØR fiksen. 854 passed / 4 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SeW1LhH5TtXxKZPe9JkqL1
This commit is contained in:
Kjell Tore Guttormsen 2026-08-14 10:48:45 +02:00
commit 88c223276c
3 changed files with 91 additions and 12 deletions

View file

@ -2,9 +2,11 @@
call before the operator pays for one.
Runs ``python -m portfolio_optimiser.preflight --profile azure``. It checks, purely offline
(config/string/env only NO client construction, NO network, NO auto-login): (1) the endpoint env
``PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT`` is set and shaped like a Foundry project endpoint
(``https://`` + host ``*.services.ai.azure.com``); (2) the effective model-map (honoring
(config/string/env only NO client construction, NO network, NO auto-login): (1) an endpoint is set
under EITHER name the run path accepts (``PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT`` first, then the
platform-injected ``FOUNDRY_PROJECT_ENDPOINT`` same tuple, imported from ``backends``) and is
shaped like a Foundry project endpoint (``https://`` + host ``*.services.ai.azure.com``); (2) the
effective model-map (honoring
``PORTFOLIO_MODEL_MAP``) is structurally valid (``ModelMapContract``); (3) no azure deployment is
still a ``REPLACE-WITH-*`` placeholder (via ``resolve_model`` the SAME seam the run path uses, so
preflight and run never validate different maps).
@ -26,10 +28,14 @@ from dataclasses import dataclass
from pydantic import ValidationError
from portfolio_optimiser.backends import Profile, _load_effective_map, resolve_model
from portfolio_optimiser.backends import _ENDPOINT_ENVS, Profile, _load_effective_map, resolve_model
from portfolio_optimiser.contracts import ModelMapContract
_ENDPOINT_ENV = "PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT"
# Fase 5 — the SAME tuple the run path resolves against, imported rather than restated. A second
# copy here is how the gate and the run path came to know different variable names in the first
# place: preflight refused a hosted container's platform-injected endpoint that backends.py would
# have accepted (measured from the extracted handover package, 14.08).
_ENDPOINT_ENV = _ENDPOINT_ENVS[0]
_FOUNDRY_HOST_SUFFIX = ".services.ai.azure.com"
_ROLES = ("default", "proposer", "checker")
# Exact operator-facing disclaimer marker (Norwegian, per docs-language convention). The docs note
@ -52,12 +58,28 @@ class PreflightRefusal:
reason: str
def _resolve_endpoint() -> str | None:
"""First NON-EMPTY of ``_ENDPOINT_ENVS`` — ours first, the platform-injected name as fallback.
Precedence over VALUES, not declarations: an exported-but-empty own name falls through instead
of shadowing a real injected one into a refusal (the 4b rule, same seam as ``backends.py``)."""
for name in _ENDPOINT_ENVS:
value = os.environ.get(name)
if value:
return value
return None
def _endpoint_error() -> str | None:
"""Return an actionable reason if the endpoint env is missing/misshapen, else ``None``. Pure
string work no ``urllib`` (both the NFR and the offline grep-guard forbid it)."""
endpoint = os.environ.get(_ENDPOINT_ENV)
endpoint = _resolve_endpoint()
if not endpoint:
return f"{_ENDPOINT_ENV} er ikke satt (påkrevd for azure-profilen)"
# Name BOTH: the operator on a laptop and the operator in a hosted container are looking
# for different variables (the fail-fast in ``backends.py`` says the same thing).
return (
f"{_ENDPOINT_ENVS[0]} (eller plattformens injiserte {_ENDPOINT_ENVS[1]}) "
"er ikke satt (påkrevd for azure-profilen)"
)
if not endpoint.startswith("https://"):
return f"{_ENDPOINT_ENV} må være en https://-URL, fikk: {endpoint!r}"
# Host = between the scheme and the first '/', minus any port; lowercased. Do NOT require the