feat(3): flaten sier sant om seg selv, og to gater måler at den fortsetter å gjøre det

Fase 3 (AAA+ på publisert flate). Tre av planens premisser falt på måling og er
rettet FØR handling, ikke etterpå:

* GOVERNANCE-raden hadde feil tiltak. Planen sa «skriv den»; org-ops D11 sier én
  kanonisk fil som hvert repo LENKER, og filen er nå publisert (målt: HTTP 200 på
  open/repo-standard). Å skrive vår egen ville gjort oss til kopi nr. 12 av en
  fil D11-bølgen holder på å rydde vekk. README lenker den, i samme form som
  repo-mailbox bruker, og bus-faktor 1 står uttalt i den kanoniske teksten.
* Release-objektet for v1.0.0 FINNES allerede på open/ (id 155, CHANGELOG-kropp,
  siden rendrer) — det som mangler er vedlegg, ikke objektet.
* WARN RELEASE-STALE fyrer ikke, og kan ikke: regelen sammenligner utgivelse mot
  tagg og er strukturelt blind for repo med null utgivelser (org-ops hovedbok
  #18). Gaten var OK/20 sjekker FØR arbeidet startet, så den kan ikke tjene som
  verifikasjon for denne fasen. Bevisene er Forgejo-APIet, filinnholdet og
  ren-klon-kjøringen.

A5-defekten rettet: env.template:21 sa at credential resolves via
DefaultAzureCredential. Den har aldri gjort det — backends.py:149 konstruerer
ManagedIdentityCredential eller AzureCliCredential, og Learns MAF-veiledning
navngir den spesifikke credentialen NETTOPP for å unngå probing. En operatør som
kopierte templaten ble fortalt at feil identitet ville bli brukt.

To load-bearing gater (Iron Law: begge røde før fiksen, 2 failed / 7 passed):

1. env.template navngir de credentials backends.py faktisk konstruerer, og ingen
   linje utgir DefaultAzureCredential for å være mekanismen. LINJEFORANKRET, ikke
   delstreng: backends.py NAVNGIR klassen fire ganger i kommentarene som
   begrunner hvorfor den ikke brukes, så en fil-bred substring-gate ville vært
   rød på nøyaktig den prosaen den beskytter (repoets 08-09-klasse, fjerde gang).
2. README-ens wheel-filnavn bærer versjonen bygget stempler på fila. Uten den
   ville en versjonsbump stille etterlatt en publisert install-kommando som peker
   på en fil som ikke finnes.

Hver positiv assert er paret med en KONTROLL på at det søkes etter noe som
finnes — en ekstraktor som stille finner null lager en gate som bare kan bli
grønn.

MUTASJONER MÅLT MOT HELE SUITEN, begge røde på riktig test og på INGEN annen:
gjeninnfør den usanne credential-påstanden (2 røde, 844 grønne) · la
wheel-filnavnet drifte til 1.0.0 (1 rød, 845 grønne). Restaurert fra scratchpad
+ shasum -c mellom hver. Bumpen selv var den andre mutasjonen: pyproject 1.0.0 →
1.1.0 gjorde README-gaten rød alene, før README ble rettet.

SECURITY.md: varslingsfrist (minst én minor-release og aldri under 30 dager
mellom kunngjøring og fjerning, med sikkerhetskritisk fjerning som uttalt
unntak). Støttetabellen er bevisst VERSJONSFRI — et release-nummer skrevet der
ville drevet ved neste tagg, altså samme defektklasse som gate 2 fanger.

CLAUDE.md beholdt på flaten med en engelsk innramming øverst (operatørvalg): den
sier hva fila er for en fremmed. Innholdet er repoets sterkeste bevis på at hver
beslutning er målt; å fjerne det ville fjernet bevis, ikke friksjon.

Versjon 1.1.0 — synket i pyproject, __init__, test_smoke og README-kommandoen.
1.0.0-treet kan ikke produsere en kjørbar wheel (force-include kom etter taggen,
målt: git show v1.0.0:pyproject.toml har den ikke), så en wheel hengt på den
utgivelsen ville vært nøyaktig den usanne påstanden denne fasen finnes for å
fjerne. Operatøren valgte bumpen framfor et vedlegg som ikke virker.

846 passed / 4 skipped (fra 837). ruff + format + mypy rene.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ckyg3Pc6k7FRuR6fDGQLJ
This commit is contained in:
Kjell Tore Guttormsen 2026-08-14 06:57:25 +02:00
commit 28a420ab97
10 changed files with 246 additions and 10 deletions

View file

@ -5,6 +5,56 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.0] - 2026-08-14
The release that makes the distribution stand on its own. `1.0.0` shipped a framework that could
only run from a checkout; this one runs from an installed wheel and inside a Foundry-hosted
container, and it is the first release whose artefacts are published alongside it.
### Added
- `shared/` now travels **as packaged data**. The wheel carries a byte-identical mirror of the tree
under `portfolio_optimiser/_shared/`, and `shared_root()` resolves at call time in a fixed order:
`PORTFOLIO_SHARED_ROOT`, then the working tree's `shared/` when one exists, then the packaged
copy. The working tree stays authoritative in a checkout — that is what keeps the pull-only
subtree contract and the byte-exact goldens untouched. Measured before and after: the `1.0.0`
wheel carried 58 files and none under `shared/`; this one carries 122, of which 64 are the mirror.
- A **hosted entry point**: `main.py` wraps `run_project` on a single asyncio loop and serves the
Foundry hosting contract (`GET /readiness`, `POST /invocations`, SIGTERM → exit 0), with a
`Dockerfile` and `azure.yaml` beside it. The invocation payload is whitelisted against
`run_project`'s own signature — an unknown field is refused by name with a 400 rather than
silently dropped — and a rejected proposal is a successful run (200), because the negative outcome
belongs to the payload and never to the transport.
- Wheel-install instructions in the README. A wheel is not installable on its own: two dependencies
are pinned to git tags, and `[tool.uv.sources]` does not travel with wheel metadata, so both must
be supplied as requirements alongside the wheel. The published command is the one that was
measured (65 packages, exit 0), not one composed afterwards.
- Two gates on claims the **published surface** makes about itself: that `env.template` names the
credentials `backends.py` actually constructs, and that the README's wheel-install command spells
the version the build stamps on the file. Both read raw text and are line-anchored, because prose
is the only place these claims live.
### Changed
- The AZURE profile now reads **its own environment** rather than the operator's laptop. The
endpoint resolves to the first non-empty of `PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT` and Foundry's
injected `FOUNDRY_PROJECT_ENDPOINT`; precedence applies to *values*, so an exported-but-empty name
falls through instead of masking a real one. The credential follows the same environment:
`AzureCliCredential` on a developer host, `ManagedIdentityCredential` when
`FOUNDRY_HOSTING_ENVIRONMENT` holds a non-empty value — never `DefaultAzureCredential`, whose
probing would walk a credential chain that cannot succeed in a container and turn a config error
into a slow one.
- `SECURITY.md` states a **deprecation notice period**: at least one minor release and no fewer than
30 days between announcement and removal, with security-critical removals named as the explicit
exception. The supported-versions table is deliberately version-free, since a release number
written there would drift at the next tag.
- The README links the organisation's single canonical `GOVERNANCE.md` instead of vendoring a copy,
and states the maintenance model (solo-maintained, no SLA, fork-and-own) on the first screen.
- `CLAUDE.md` opens with an English note explaining what the file is for a visitor: the working
agreement with the AI agent that builds this repository, doubling as its invariant ledger.
### Fixed
- `env.template` claimed the AZURE profile resolved its credential through `DefaultAzureCredential`.
It never has. An operator copying the template was told the wrong identity would be used.
## [1.0.0] - 2026-08-12
### Added