<!-- OpenSmartRoute: Changelog. https://opensmartroute.ai/docs/changelog -->
# Changelog

All notable changes are documented here. Format follows [Keep a Changelog](https://keepachangelog.com);
versions follow [SemVer](https://semver.org). Releases are cut by the `Prepare release` workflow
(`scripts/release.py`), which turns the Unreleased section into a dated section.

## [Unreleased]

### Added

The public site shows what routing saves, on real traffic
- `GET /api/v1/stats/public` gains `savings` - the deployment''s lifetime executed traffic priced two ways:
  routed as it actually ran vs sent entirely to the priciest model in the catalogue (baseline, routed /
  baseline / saved USD, saved share; the same arithmetic as every workspace''s own savings report) - and
  `top_targets`, the models, skills and agents that carried the most requests.
- The landing page uses them: the usage band adds "saved vs sending everything to <baseline>" next to the
  OpenRouter-style counters, and a new *Direct vs routed - the same traffic, two bills* section draws the two
  bills side by side with the live traffic mix per target kind, linking to /roi, /models and /rankings.

Marketplace snippets are real, runnable code
- `opensmartroute.stack.registry_resolver(base_url)` is public: the resolver behind `osr stack --registry`,
  usable directly with `load_stack("registry://slug@version", resolver=registry_resolver(url))`. The
  marketplace "Use it" Python snippets now import it (they previously called a function that did not exist)
  and the manifest snippet builds a working `Router` from the fetched capability.

Documentation site rebuilt for end users on Fumadocs
- The /docs shell is now [Fumadocs](https://fumadocs.dev): collapsible sidebar from the catalogue, clerk-style
  table of contents, full-text search over headings and paragraphs (/docs/search), copy buttons and anchor
  links on the Fumadocs components. The content pipeline is unchanged - plain repository Markdown, no MDX.
- Two task-shaped entry pages replace file-shaped navigation: docs/QUICKSTART_PLATFORM.md (workspace, key,
  first /api/v1/route call, OpenAI-compatible endpoint, feedback) and docs/QUICKSTART_SDK.md (install,
  targets, route, constraints, learning, CLI); the docs index and the Get started section lead with them and
  the user guide moved to the Python SDK section.

Light and dark themes for the website and the whole platform
- The site, documentation, dashboard, operator console and sign-in pages render in a light and a dark
  theme. The theme follows the system preference and can be pinned from the sun / moon switch in every
  header (`components/layout/theme-toggle.tsx`; next-themes sets the `dark` class on `<html>`, the choice
  is kept in `localStorage` as `osr.theme`). Every brand neutral and status colour in `globals.css` now
  has a value per theme (`bg-paper`, `text-ink`, `border-line` ... follow it without page edits), the
  pastel tone shades used for alerts, badges and status boxes invert to translucent tints in the dark
  theme, and panes that are dark in both themes (code, the SLM band) carry `.theme-dark`. Ink fills use
  `text-paper` so labels stay legible in both themes; `<meta name="theme-color">` and `color-scheme`
  ship both values. `docs/BRAND.md` lists the two palettes.

Marketplace content safety for imported listings
- `osr_platform.harvest` redacts credential-shaped strings (AWS / GitHub / OpenAI / Slack / Stripe / Google /
  platform keys, JWTs, `PRIVATE KEY` blocks) to `[REDACTED <kind>]` at parse time, when cached rows are
  replayed (`listing_for`) and server-side in the admin import (`clean_row`), so an author's leaked key is
  never republished. Repositories that publish attack corpora (poisoned / malicious / prompt-injection skill
  datasets) are skipped by every source, dropped from the cache replay and rejected by `validate_row`.

Email: Azure Communication Services, branded templates, welcome message, newsletter, scheduled reports
- `email_templates.py` composes every message the platform sends as a branded HTML card with a plain-text
  alternative (welcome, verification, reset, invitation, account deleted, password changed, operator test,
  usage reports, newsletter); the outbox stores both (`outbox.html`, `has_html` / `html` on the mail endpoints).
- `Mailer` delivers through **Azure Communication Services Email** when `OSR_PLATFORM_ACS_CONNECTION_STRING`
  is set (HMAC-SHA256 request signing, no SDK; wins over SMTP) or SMTP as before; `Mailer.transport` is `acs`,
  `smtp` or `outbox` and `GET /api/v1/admin/mail` reports it. A welcome message goes out after signup.
- Newsletter: `POST /api/v1/newsletter/subscribe` (always 202, confirmation mail) and
  `POST /api/v1/newsletter/unsubscribe` (one-click, per-send token) for the public; operators get
  `GET /api/v1/admin/newsletter`, `GET .../subscribers.csv`, `POST|DELETE .../subscribers[/{id}]` and campaigns
  (`POST /api/v1/admin/newsletter/campaigns`, `GET|PATCH|DELETE .../{id}`, `POST .../{id}/test`,
  `POST .../{id}/send` - sent once to every active subscriber). `OSR_PLATFORM_NEWSLETTER=false` hides the form.
- Scheduled usage reports (`reports.py`): weekly (Monday) and monthly (1st, UTC) digests to workspace owners and
  the deployment digest to `OSR_PLATFORM_REPORT_RECIPIENTS`; `OSR_PLATFORM_REPORTS_INTERVAL_S` (default hourly)
  drives the scheduler, `report_runs` makes a window idempotent across replicas. `GET /api/v1/admin/reports`,
  `POST /api/v1/admin/reports/send`, `POST /api/v1/admin/reports/tick`.
- Infra: `acsConnectionString` (secret) and `reportRecipients` through Bicep, azd, CI and compose.
- Console: `/platform/admin/newsletter` (subscribers with filter, add, remove, CSV; campaigns with plain-text
  editor, rendered preview, test send, send to everyone), `/platform/admin/reports` (scheduler state, sent digests,
  send a digest now, run the scheduler now); `/platform/admin/mail` shows the transport and renders the HTML of a
  message. Site: `/newsletter` sign-up page and footer form.

Workspace self-service: outcome reporting in the dashboard, key rotation, data export
- Outcomes without curl: `components/dashboard/outcome-form.tsx` - good / poor, a quality slider and "this
  target would have been better" (recorded as a pairwise preference) - on every expanded activity row and in
  the request trace; the onboarding step "report an outcome" now links to the activity page (or billing on
  plans without the feature).
- API keys: `PATCH /api/v1/keys/{id}` renames, `POST /api/v1/keys/{id}/rotate` issues a new secret for the
  same key in one transaction (shown once; the old secret dies immediately; id, name and creation date stay,
  so audit rows and dashboards remain attached). The keys page renames inline and rotates with a warning.
- Data portability: `GET /api/v1/me/export?days=` returns one JSON document with the person (profile,
  identities, sessions, memberships, history) and the workspace (plan, members, key prefixes, tenants,
  policy, SSO, usage, request log, reported outcomes); *Download my data* on the account page. Secrets and
  password hashes never appear; tests assert the export never contains the API key or password.

Machine-readable site: real llms.txt, Markdown pages, complete OpenAPI
- Every documentation page is served as Markdown at `/docs/<page>.md` (relative links resolved to the site or
  the repository, SKILL.md frontmatter stripped) and advertises it with a `text/markdown` alternate link;
  `/llms.txt` now links to those Markdown sources as llmstxt.org intends and `/llms-full.txt` is the whole
  documentation in one file. `/openapi.json` on the website serves the complete committed OpenAPI document
  (146 paths, `servers` set to the site) instead of proxying the gateway process, whose own document only
  lists the routes it serves itself once domains run as separate services (4 paths locally, 19 in
  production). Footer: `llms-full.txt` added, *Discussions* (not enabled on the repository, a 404) replaced
  by *Issues*; the support page sends questions to a labelled issue. Tests: `platform/web/tests/llms.test.ts`.

Operator console: analytics, health, request log, marketplace moderation, leads, plans, configuration, search
- `platform/api/osr_platform/admin_console.py`: `GET /api/v1/admin/analytics?days=` (deployment-wide daily
  requests / failures / cost / tokens / active workspaces / mean latency, per endpoint, per target, busiest
  workspaces, signups per day), `GET /api/v1/admin/activity` (every metered request across workspaces with
  cursor paging and endpoint / target / failed / workspace / request-id filters), `GET /api/v1/admin/lookup?q=`
  (users, workspaces, API keys by prefix, operators, tenants, request ids - each with its console page),
  `GET /api/v1/admin/settings` (effective configuration grouped with the `OSR_PLATFORM_*` variable of every
  value; secrets and nested `client_secret` / `api_key` values are masked), `GET /api/v1/admin/retention` and
  `POST /api/v1/admin/retention/purge` (run the sweep now), and `GET /api/v1/admin/health?window=` served by
  the routing process itself (readiness, HTTP series, last five minutes, circuit breakers, observability,
  deployment alerts, the alert evaluator, replica host).
- Console pages `/platform/admin/analytics`, `/health`, `/activity`, `/leads`, `/plans`, `/marketplace`
  (review queue and every status, approve / reject / unpublish with a note, featured and verified flags,
  imported catalogue per source, scheduled refresh with *run now*), `/settings` (configuration + retention)
  and `/search`; a search box in the console header opens the search page. Navigation regrouped into
  Deployment, Directory, Learning, Platform and Access.

Operators see both portals
- Superadmins open any workspace's dashboard from the console: `POST /api/v1/admin/accounts/{id}/session`
  mints a one-hour browser session on the workspace as its owner (provider `operator`, visible in the owner's
  session list, audited as `workspace.open`); *Open dashboard* on `/platform/admin/workspaces` and on each
  workspace page switches to the workspace portal, which shows an *Operator view* banner (who, which
  workspace, when the session ends) with *Back to console* and *End operator view* (revokes the session).
  The view sees every plan feature (statistics, audit, tenants ...) whatever the workspace's plan - quotas
  stay the workspace's - so no page is hidden behind an upgrade prompt for the operator.
  The console's account menu leads to the workspace dashboard and the dashboard's account menu back to the
  console while an operator session exists. `GET /api/v1/me` now includes `session` (null for API keys).

True streaming on the OpenAI-compatible endpoint
- `POST /v1/chat/completions` with `stream: true` now relays each provider token as it arrives instead of
  completing the request first and replaying the finished text as chunks (time to first token was the whole
  completion time - measured 2.55 s to the first of 39 chunks that then all landed within 130 ms). The final
  chunk still carries `usage` and the `opensmartroute` metadata, the call is metered and learned from exactly
  like the plain response, quota / routing / provider errors before the first token are ordinary HTTP errors,
  `X-OSR-Target` / `X-OSR-Confidence` name the routed target, and the automatic fallback to the next target
  applies only before the first token (`committed` in `_execute_with_fallback`).
- SDK: `OpenAICompatClient.chat(..., on_delta=)` / `stream_chat()` consume the upstream `stream: true`
  server-sent events (with `stream_options.include_usage`; token counts estimated when the provider sends
  none) and fall back to a single delta for providers that ignore `stream`; `chat_handler` forwards the
  callback, so `execute(decision, request, on_delta=...)` streams through the whole SDK pipeline.
  `platform/api/tests/test_streaming.py` drives a paced SSE upstream through the SDK client, the TestClient
  and a real uvicorn server (tokens spread over the provider's pacing, not one burst).

Operator console: directory, audit and mail administration
- `GET /api/v1/admin/users` filters by `disabled`, `verified` (email confirmed) and `method` (`password` /
  `sso`), sorts by `created_at`, `last_login_at`, `email` or `name` (`order=asc|desc`), returns the filtered
  `total` and downloads the matching rows with `format=csv`; `POST /api/v1/admin/users/bulk` applies
  `disable`, `enable`, `verify`, `revoke_sessions`, `reset_link` or `delete` to up to 500 users and reports each
  as done or failed. `GET /api/v1/admin/accounts` gains `plan`, `disabled`, `sort`, `order` and `format=csv`.
- `GET /api/v1/admin/audit-log` filters by `actor_type`, `actor_id`, `after` and free text `q` (actor, IP,
  details), returns the distinct `actions` with counts for the filter menu and exports with `format=csv`.
  Every operator action (`operator.*`, `user.*`, `account.*`, `mail.*`, `users.bulk`, `*.export`) is now
  recorded in `audit_log` with the operator as actor, not only mirrored on the event bus.
- Outbox: `GET /api/v1/admin/mail` filters by `kind` and `failed`, pages with `offset`, returns `total` and a
  `summary` (composed, unsent, failed, kinds); `POST /api/v1/admin/mail/{id}/resend` delivers a message again
  (409 without SMTP), `DELETE /api/v1/admin/mail/{id}` removes it, `POST /api/v1/admin/mail/test` sends a
  delivery check to an address. `Mailer.resend()` and `Mailer.test_message()`.
- `GET|DELETE /api/v1/admin/operators/{id}/sessions`: superadmins inspect any operator's console sessions and
  revoke one (`session_id`) or all of them; operators see their own.
- Console pages: `/platform/admin/users` (status / email / sign-in filters, sortable columns, last sign-in and
  verified columns, row selection with a bulk-action bar, *Export CSV*), `/platform/admin/workspaces` (kind,
  plan and status filters, sortable columns, *Export CSV*), `/platform/admin/audit` (action menu with counts,
  actor, time window, user / workspace and text filters, *Load older*, *Export CSV*, click an action badge to
  filter by it), `/platform/admin/mail` (counters, kind and delivery filters, paging, *Resend*, *Delete*,
  *Send a test message*), `/platform/admin/operators` (*Sessions* of another operator with per-session and
  sign-out-everywhere revocation).

SLO-aware routing (the platform plan's open "rolling p50/p90/p99 per endpoint" row)
- `realtime.LatencyWindow`: each target in `HealthRegistry` keeps its last 256 observed latencies with
  nearest-rank p50 / p90 / p99 (`health_snapshot()` gains `latency_p50_ms`, `latency_p90_ms`, `latency_p99_ms`;
  the platform's `/api/v1/stats` and the Health page show them next to the breaker state).
- `RequestConstraints.preferred_max_latency_ms`: a *soft* latency target. `HealthStrategy` now scores the
  observed p90 (once five calls are in the window; EWMA, then the declared latency before that) against the hard
  `max_latency_ms`, else this soft target, else `with_health(latency_slo_ms=...)`, so a target with a good mean
  and a bad tail is penalised without being excluded. Accepted by `osr route --constraint
  preferred_max_latency_ms=...`, the MCP `route` tool, `POST /api/v1/route` constraints and workspace / tenant
  policy (`preferred_max_latency_ms` in `POLICY_KEYS`; the stricter of request and policy wins).

Dashboard chart kit
- `components/charts`: the platform's own chart primitives on top of the time-series ones - `RankedBars`
  (horizontal ranking with values), `Donut` (share with a centre label), `Bars`, `ScatterPlot`, `Sparkline`,
  `Meter` (bounded value against a threshold), `ShareBar` (stacked composition, HTML only) and `Heatmap`
  (day x hour density, HTML only); `ChartTooltip`, `ChartLegend`, `ChartEmpty`, a `SERIES` palette with
  `seriesColor()` and the theme tokens `--chart-axis`, `--chart-grid`, `--chart-cursor`, `--chart-other`
  (light and dark). `components/dashboard/analytics.tsx` renders the API's breakdowns as charts instead of
  tables: `TargetBreakdown`, `EndpointShare`, `TrafficHeatmap`, `SavingsChart` (routed against the baseline
  per day), `SavingsByTarget`, `PlanMix`, `DecisionsBreakdown`, `LatencyHistogram`, `RouteTraffic`,
  `BreakerGrid`, `StrategyWeights`, `OutcomesByTarget`. `StatCard` takes a `trend` (delta pill with
  direction) and an `info` tooltip. Tests: `platform/web/tests/charts.test.tsx`.

Notifications and alert delivery
- Alerts are now delivered, not only shown. `platform/api/osr_platform/notifications.py`: the API
  re-evaluates the workspace and deployment alert rules every `OSR_PLATFORM_ALERTS_INTERVAL_S` (60 s; one
  replica through a Redis lease) and turns each condition into a notification episode (opened, escalated,
  resolved) kept in an inbox with unread counts, published to the `alerts` Kafka topic in cluster mode and
  delivered to channels: e-mail (platform mailer), `https` webhooks (JSON with `X-OSR-Event`,
  `X-OSR-Delivery` and an HMAC `X-OSR-Signature`), Slack and Teams incoming webhooks. Channels filter by
  minimum severity and rule names, keep last delivery / last error, have *Send test*; failed webhooks are
  retried on the next rounds (three attempts). Endpoints `GET /api/v1/notifications[/unread]`,
  `POST /api/v1/notifications/read`, `GET|POST /api/v1/notifications/channels`,
  `PATCH|DELETE /api/v1/notifications/channels/{id}`, `POST .../channels/{id}/test`,
  `GET /api/v1/notifications/deliveries`; operator equivalents under `/api/v1/admin/notifications` plus
  `POST /api/v1/admin/notifications/evaluate`. Dashboard page `/dashboard/notifications` with a bell and
  unread badge in the header; console page `/admin/notifications`.

The platform's own observability
- The platform observes itself; no external metrics, tracing or alerting system is involved (the OTLP bootstrap
  and the Prometheus / Grafana / Jaeger compose profile added earlier were removed). `osr_platform.telemetry`:
  a `DbEventSink` on the tracer persists every span and event to the platform database from a bounded queue
  and a daemon writer (never on the request path; `OSR_PLATFORM_TELEMETRY_STORE`, on by default;
  `OSR_PLATFORM_TELEMETRY_RETENTION_DAYS`, 14, purged by the retention sweep), so `GET /api/v1/events`
  (`since` / `until`, `source`, `persisted`, `retention_start`) and `GET /api/v1/trace/{id}` (`source`) survive
  restarts and `GET /api/v1/activity` marks persisted requests `traced`. `HttpMetrics.series` keeps per-minute
  HTTP counters for 24 hours. `GET /api/v1/telemetry/series?window=1h|6h|24h|7d|30d` buckets the workspace's
  metered traffic (requests, failures, p50 / p95, cost, tokens; per target and endpoint) and, with `stats`, the
  deployment's HTTP requests, 4xx / 5xx and p50 / p95 / p99. `osr_platform.alerts` + `GET /api/v1/alerts`: rules
  evaluated in-process - budgets at 80 % / exhausted (workspace and tenants), failing requests, and on `stats`
  plans readiness, 5xx rate, latency SLO, open breakers, drift alarms, autopilot errors, a stale SLM, tracing
  off - each naming the dashboard page that fixes it. Web: `AlertsPanel` and 24-hour traffic / latency series
  on the overview, deployment HTTP series with a window selector and the alerts on Health, a 1 h - 7 d window on
  Events served from the store, trace view labels the source. Tests: `platform/api/tests/test_telemetry.py`.

Platform services and model providers
- The platform API runs as one process or as several: `docs`, `rankings`, `marketplace`, `providers`,
  `onboarding`, `accounts` (sign-in, sessions, workspaces, keys, tenants, policy, governance, usage), `billing`,
  `admin`, `mcp` (the `/mcp` Model Context Protocol endpoint), `openai` (the `/v1` proxy; streamed completions
  are relayed through the gateway chunk by chunk) and `routing` (`/api/v1/route`, feedback, targets, stats,
  audit, the trace / event / learning reads of its decisions and the autopilot) each have their own entry point
  (`osr-platform-<name>`, `python -m osr_platform.services <name>`,
  ports 8091-8101) on the API image (`platform/api/osr_platform/services.py`). The API is the gateway: with
  `OSR_PLATFORM_<NAME>_URL` set it forwards that domain's paths (headers, body, `X-Forwarded-For`) and marks
  the answer with `X-OSR-Service`; unset, it serves the domain in-process. `/api/v1/info`, `/status`,
  `/estimate` and the health / metrics endpoints always stay with the API; every service also serves
  `/healthz`, `/readyz` and `/metrics`. Each process owns its background
  work (`PlatformContext`: the API bootstraps operators and evaluates alerts, the owner of `routing` runs the
  autopilot and the telemetry store, `rankings` refreshes the
  reference catalogue, `marketplace` seeds the registry). `GET /api/v1/info` -> `services`,
  `GET /api/v1/admin/services` (topology + live health) and the `/admin/services` page; the dev
  `compose.yaml` and `platform/docker-compose.yml` start every service next to the gateway.
- Model providers managed at run time: `ProviderStore` (tables `providers`, `provider_models`) with
  `GET|POST /api/v1/admin/providers`, `GET|PATCH|DELETE /api/v1/admin/providers/{id}`,
  `POST /api/v1/admin/providers/{id}/check` (probes `/models`, records latency and the upstream model ids)
  and `PUT|DELETE /api/v1/admin/providers/models/{target_id}`; presets for OpenAI, Azure, OpenRouter,
  Anthropic, Mistral, Groq, Together, Fireworks, DeepSeek, Ollama, vLLM, LiteLLM. The mounted
  `OSR_PLATFORM_PROVIDERS` file is imported once and stays merged underneath; every write re-binds the
  handlers at once and other API replicas re-read the store every `OSR_PLATFORM_PROVIDERS_RELOAD_S` (30 s).
  Public `GET /api/v1/providers`: the OpenRouter-style catalogue (provider, upstream model, reference prices,
  context, health, 7-day traffic per executable target; never credentials) and its page on the site, `/providers`.
  Console page `/admin/providers`.
- `GET /api/v1/onboarding`: the getting-started checklist of a workspace (e-mail confirmed, key, first route,
  first outcome, provider connected, teammate invited) with links and the completing API call; the dashboard
  overview shows it until every step is done. Signup,
  verification, password reset and invitation links moved into the `onboarding` router (same paths).

### Changed

Source and pipelines moved from GitHub to Azure DevOps
- The repository is `the project repository issues are work items
  in the same project. Package metadata, the changelog compare links, `branding.REPOSITORY`, the Helm chart, the
  image labels and every repository link on the website point there (`site.repo`, `repoPath()` in
  `platform/web/src/lib/config/site.ts`; the docs renderer resolves unpublished paths to Azure Repos and
  brand images to `/brand/`). `scripts/release.py` writes Azure Repos `branchCompare` links (GitHub URLs still work).
- GitHub Actions are replaced by Azure Pipelines under `azure-pipelines/`, one pipeline per deployable so a
  change only builds and ships the service it touches: `ci.yml` (the SDK - gates on Python 3.10-3.13 and
  Windows, distribution, `osr serve` image), `platform-api.yml` (platform/api and the SDK it embeds: API tests
  on SQLite and on PostgreSQL + Redis + Kafka service containers, the cluster compose smoke, then `azd deploy
  api` and a rolling image update of every domain-service container app - docs, rankings, marketplace,
  providers, onboarding, accounts, billing, admin, mcp, openai, routing, slm - which all run the API image),
  `platform-web.yml` (platform/web plus the docs content it renders: check, build, Playwright journey,
  two-container smoke, then `azd deploy web` and `azd deploy platform`), `infra.yml` (Bicep validation on every
  change; the only pipeline that runs `azd provision`, with the full deployment-input set so custom-domain
  bindings are never dropped), `release-prepare.yml` (release pull request), `release.yml` (tag, `dist` artifact
  with `SHA256SUMS` and the installers, PyPI upload with `PYPI_API_TOKEN`, `az acr build`) and `security.yml`
  (weekly bandit, pip-audit, npm audit, safety suite - replaces CodeQL and Dependabot). Deployment signs in
  through the `osr-platform-prod` service connection (workload identity federation) and reads its inputs from
  the variable group of the same name. The released container image is
  `crosrua2c6onyidguw.azurecr.io/opensmartroute:<version>` instead
  of `ghcr.io/isathish/opensmartroute`; `scripts/domains.ps1` and `scripts/sso.ps1` write the variable group
  instead of GitHub repository variables and secrets.

Website and platform as two services, one hostname
- The signed-in product now lives under `/platform`: `/platform/login`, `/platform/signup`, password recovery,
  invitations and `/platform/cli/authorize`; the dashboard at `/platform/dashboard/...`; the operator console at
  `/platform/admin/...`; the playground at `/platform/playground` and the publish flow at
  `/platform/marketplace/publish`. The website (landing, `/docs`, `/models`, `/vendors`, `/rankings`,
  `/pricing`, `/marketplace`, `/compare`, `/roi`, `/estimate`) keeps the root. The old paths redirect (308,
  query string preserved), the OAuth redirect URI registered at providers stays `<web>/auth/callback`, and
  links the API generates (emails, invitations, device sign-in, Stripe return, alert `href`s) use the new paths.
- Deployment: a third container app, `osr-platform` (azd service `platform`, the web image, internal ingress),
  serves `/platform`; `osr-web` forwards those requests to it (`OSR_WEB_PLATFORM_URL`, `platform/web/src/proxy.ts`).
  A single web process without that variable serves both halves (`next dev`, Compose, the CI smoke container).

### Fixed

- Every signed-in page (`/platform/dashboard/...`, playground, publish) was blank on the hosted platform: the
  HTML came from the internal `osr-platform` app while the browser fetched `/_next/static` chunks from
  `osr-web`, and the two container apps ran different builds (a provision without `SERVICE_PLATFORM_IMAGE_NAME`
  had even reset `osr-platform` to the placeholder image). `osr-platform` now always takes the web image in
  `infra/`, the separate `platform` azd service is gone, and `azd deploy web` rolls `osr-platform` to the image
  it just deployed from a postdeploy hook.
- `osr mcp --remote --list-tools` (or `--url ... --list-tools`) ignored the flag and blocked on stdin as a
  bridge; it now asks the remote platform for `tools/list` and prints the tools.
- PostgreSQL connections that sat idle (low-traffic services such as `mcp` or `billing`) were dropped by the
  network and the next request failed with `server closed the connection unexpectedly`. Connections now use
  TCP keepalives and a statement issued outside a transaction is replayed once on a fresh connection.
- Azure cluster mode connected to PostgreSQL, Redis and Kafka through `<app>.internal.<domain>` names, which
  resolve to the HTTP ingress and never answer on a TCP port; the API hung inside `psycopg.connect` until the
  probes killed it and the platform silently kept serving from the previous SQLite revision. `infra/` now uses
  the bare container-app names (`osr-postgres:5432`, `osr-redis:6379`, `osr-kafka:9092`), the database URL
  carries `connect_timeout=10` (also the default in `Database` for PostgreSQL URLs without one) and the API and
  domain services have a Startup probe so a slow first boot is not restarted mid-migration.

## [1.0.0] - 2026-09-07

1.0 freezes the public API. Every `opensmartroute.*` name in `tests/public_api.json` is stable from here;
removals go through `errors.deprecated()` for at least one minor first. The release rows of the v1.0
readiness table are met from the repository (`python scripts/release.py readiness`); the adoption rows -
independent security report, accepted leaderboard listings, two production users - stay open and are
tracked in `docs/ROADMAP.md`, `docs/SECURITY_REVIEW.md`, `examples/leaderboard/results/README.md` and
`ADOPTERS.md`.

### Added

Routing SLM and datasets
- `RouterSLM.info()`: metadata, shape (encoder, dims, embedder), calibration, size and the fit history of a
  model without its weights; `osr slm info` prints it and the platform's `GET /api/v1/learning` uses it.
- `osr collect --file NAME=PATH`: load a benchmark you downloaded by hand (`.jsonl` / `.json` / `.csv` in the
  `--preset` shape; `--tier` folds model columns onto your targets) into the dataset cache - the path for
  gated RouterBench / RouterEval copies.

Release readiness
- `scripts/release.py readiness` distinguishes **release** rows (repository evidence; block a `1.x`
  version in `check`) from **adoption** rows (third-party evidence; tracked and printed, never blocking),
  and counts the freeze step `0.Y.0 -> 1.0.0` as a non-breaking release step when the changelog has no
  removals and the tagged `tests/public_api.json` only grew.

Platform administration and the operator console
- Operators sign in to `/admin` with a **username and password** (`POST /api/v1/admin/auth/login` mints an
  `osr_op_` session valid for twelve hours; scrypt password hashes; sign-in attempts rate limited per address).
  `OSR_PLATFORM_ADMIN_USERNAME` / `OSR_PLATFORM_ADMIN_PASSWORD` create - or reset - the first `superadmin` at
  API start-up; the static `OSR_PLATFORM_ADMIN_TOKEN` keeps working and counts as a superadmin. Superadmins
  manage operators (`/api/v1/admin/operators`); every operator changes their own password and sessions.
- Full administration API and web console: deployment overview (`/api/v1/admin/overview`), users (search,
  create with personal workspace / plan / password / first key, detail with workspaces, identities and sessions,
  rename, disable, reset password, sign out everywhere, delete), workspaces (search, create organizations for an
  owner email, plan / name / slug / disable / delete, members with roles, API keys, tenants with validated
  configuration, policy, SSO and usage), every tenant across workspaces, plans. Pages `/admin`,
  `/admin/users[/{id}]`, `/admin/workspaces[/{id}]`, `/admin/tenants`, `/admin/operators`, `/admin/login`.
  Every mutation is written to the access log as `operator:<username>` and published as an `admin` event.
- Email + password for workspace users: `POST /api/v1/signup` accepts a `password` (and then also returns a
  browser session), `POST /api/v1/auth/password/login` signs in by email, `POST|DELETE /api/v1/auth/password`
  set, change or remove it (`has_password` on the user); `/login`, `/signup` and `/dashboard/account` have the
  forms. `OSR_PLATFORM_PASSWORD_LOGIN=false` turns it off.

Self-hosted cluster: PostgreSQL, Redis, Kafka as containers
- `OSR_PLATFORM_DATABASE_URL=postgresql://...` runs the platform store on PostgreSQL through `psycopg` (the same
  SQL is written once; the backend rewrites placeholders, `INSERT OR IGNORE`, `REAL`/identity columns); the
  whole test suite passes against PostgreSQL (`OSR_TEST_DATABASE_URL`). SQLite stays the single-replica default.
- `OSR_PLATFORM_REDIS_URL` shares rate-limit windows between replicas (atomic sliding window in Redis, falls
  back to per-replica limiting when Redis is unreachable) and persists the enterprise learners' state in Redis
  instead of the state directory.
- `OSR_PLATFORM_KAFKA_BOOTSTRAP` publishes `usage`, `feedback` and `admin` events as JSON to Kafka topics
  `osr.<name>` (any Kafka-protocol broker; never prompt text; broker outages drop events instead of failing
  requests). `GET /readyz` reports `database_dialect`, `redis` and `events`; `GET /api/v1/info` and `/admin`
  report `storage` (`{database, clustered, redis, events}`).
- `platform/docker-compose.yml`: the complete platform on any Docker host with PostgreSQL, Redis, a single-node
  KRaft Kafka broker, the API and the web app - no cloud service involved. `infra/`: `OSR_PLATFORM_CLUSTER_MODE`
  (default true) adds `osr-postgres`, `osr-redis` and `osr-kafka` container apps with internal TCP ingress and
  their own Azure Files shares, and scales `osr-api` to `OSR_PLATFORM_API_MAX_REPLICAS` replicas; the API image
  ships the `psycopg`, `redis` and `kafka-python` clients (`osr-platform-api[cluster]`).
- The routing SLM as its own service: `osr-platform-slm` (`osr_platform.slm_service`, the API image with another
  entry point; `slm` in the Compose stack, `osr-slm` on Container Apps) consumes the `training` topic (request id +
  prompt, published by the API when `OSR_PLATFORM_TRAINING_EVENTS=true`) and `feedback`, runs the `SelfImprover`
  champion/challenger cycle on a schedule or on demand and writes promotions to `/data/autopilot/slm.json`; API
  replicas reload that file every `OSR_PLATFORM_SLM_RELOAD_S` seconds (and serve it even without a mounted bundle).
  Operator endpoints `GET /api/v1/admin/slm`, `/slm/reports`, `POST /slm/cycle`, `/slm/predict` proxy to the
  service (`OSR_PLATFORM_SLM_URL`); the console page `/admin/slm` shows model, evidence, cycles and a prompt probe.
- Microservice deployment of the platform: `platform/docker-compose.yml` runs `docs`, `rankings`, `marketplace`,
  `providers`, `onboarding` and `slm` as their own containers (`python -m osr_platform.services <name>`) behind the
  API gateway (`OSR_PLATFORM_<NAME>_URL`, answers carry `X-OSR-Service`); Container Apps cluster mode adds the
  internal apps `osr-docs`, `osr-rankings`, `osr-marketplace`, `osr-providers`, `osr-onboarding` (HTTP ingress, 1-3
  replicas, shared configuration and `/data` share) and points `osr-api` at them. The operator console page
  `/admin/services` shows the topology with a live health probe per remote service; CI's cluster smoke asserts each
  domain is served by its own container.

- `compose.yaml`: the whole hosted platform on a workstation with `docker compose up --watch` - development
  images `platform/api/Dockerfile.dev` (editable installs, uvicorn `--reload`) and `platform/web/Dockerfile.dev`
  (`next dev`), Compose Watch syncing `src/`, `platform/`, `docs/`, `examples/` and the skills into the running
  containers so edits show immediately, a persistent data volume, and an optional `llm` profile that starts
  Ollama with `deploy/compose/providers.ollama.yaml` so chat completions execute end to end without cloud keys.
- `python scripts/release.py readiness`: the v1.0 readiness table of `docs/ROADMAP.md` computed from the
  repository - frozen API snapshot and test, performance envelope, reference deployments, review pack and CI
  safety suite, published leaderboard run, two consecutive non-breaking minor steps (changelog sections
  without removals, verified against the tagged `tests/public_api.json` when both tags exist), independent
  report rows in `docs/SECURITY_REVIEW.md`, `accepted` rows under *Listings* in
  `examples/leaderboard/results/README.md` and *Production users* rows in the new `ADOPTERS.md`.
  `--require` exits 1 while a row is open and `release.py check` applies the same rule to any `1.x`
  version, so the *Release* workflow cannot tag 1.0.0 before the evidence exists; the workflow prints the
  table in its job summary.
- Tests for the OpenTelemetry bridge (`OpenTelemetrySink` driven through a fake `opentelemetry` API:
  spans, nesting through the context, `traceparent` parent, events, counters, duration histogram, error
  status, missing-extra error).
- Documentation trust gates: `tests/test_docs_claims.py` fails when any Markdown source names an `osr`
  command or flag the CLI parser does not have, or an `OSR_*` variable nothing in the code or deployment
  configuration reads; `platform/api/tests/test_platform_docs_claims.py` fails when a documented HTTP endpoint is not
  a route of the platform app (method included), a documented `/dashboard/<page>` has no page, a dashboard
  page or nav entry is undocumented, a user-facing `docs/*.md` is missing from the site catalogue, or a
  CHANGELOG section lacks a date, body or compare link. Historical tags `v0.2.0`, `v0.3.0`, `v0.4.0` mark
  the commits that carried those versions so the compare links and the readiness snapshot check resolve.

### Changed

- Marketing and docs honesty: the editions matrix credits the community edition with the guard, PII
  redaction, metrics and tracing it actually runs; the SLM showcase caveats the 0.94 figure as a
  single-source number; the pricing page labels the `sso` feature; the hero shows the version only when the
  API reports it; the Learning page points operators at `POST /api/v1/admin/autopilot/cycle` instead of a
  button that could only fail; `platform/README.md` documents `MODELS_REFRESH`, `REGISTRY_AUTO_PUBLISH` and
  `REGISTRY_SEED`; the Helm chart and deploy guide state that autopilot promotion is per pod; RESEARCH.md
  reflects the gated RouterBench / RouterEval sources.

## [0.5.0] - 2026-09-06

### Added

Roadmap: everything 1.0 needs from the repository
- `examples/leaderboard/results/`: the published 0.5.0 benchmark run made with the leaderboard recipe (SLM trained on
  Arena-55k + RouteLLM battles, seed 0; MT-Bench human, PPE human and WebDev Arena held out at the suite level,
  `--limit 1000`): one JSON per suite for the learned router (`--baselines --calibration --robustness`) and for the
  declarative router, plus a README with the data table, the SLM SHA-256 and the result table (learned 0.52-0.57 vs
  declarative 0.19-0.42 and static task table 0.54-0.67; paraphrase 0.88-1.00; held-out isotonic ECE 0.03-0.05).
  Published on the documentation site as `/docs/leaderboard-results`.
- `docs/SECURITY_REVIEW.md`: the external security review pack - engagement terms, system and trust boundaries,
  the threat-model reference, an evidence table with a reproduction command per row, nine ordered reviewer
  questions, known gaps that need not be re-reported and the review log where a third-party report is linked.
  `SECURITY.md` supported versions updated to 0.5.x / 0.4.x.
- `docs/ROADMAP.md`: v0.9 is Complete (its exit criterion now names the published review pack; the independent
  report moved to the v1.0 readiness table as the adoption item it is) and v1.0 has an explicit readiness table:
  five repository items met, one of two stable minors done, three adoption rows open (independent report, accepted
  leaderboard listings, two production users).

### Added

Hosted platform: billing and SSO as deployment inputs
- `infra/main.bicep` accepts `stripeSecret`, `stripeWebhookSecret`, `stripePrices`, `ssoProviders` and
  `metricsPublic` (azd `OSR_PLATFORM_STRIPE_SECRET`, `_STRIPE_WEBHOOK_SECRET`, `_STRIPE_PRICES`,
  `_SSO_PROVIDERS`, `_METRICS_PUBLIC`; GitHub secrets / variables of the same names in the deploy job). Container
  secrets are added only when set, so a provision without them changes nothing; with the Stripe values the hosted
  pricing page goes to Checkout instead of the operator instructions. `GET /metrics` on the Azure deployment now
  requires `X-Admin-Token` by default.
- `platform/api/tests/test_docs_links.py` checks that every `/docs/<slug>#anchor` link in the web app and every
  `file.md#anchor` link between the Markdown sources points at an existing heading (the site's `rehype-slug` ids).
  The container smoke in `.github/workflows/platform.yml` now also requests `/models`, `/rankings`, `/marketplace`,
  `/playground`, `/estimate`, `/roi`, `/mcp`, `/login`, `/signup`, `/cli/authorize`, `/docs/REFERENCE`,
  `/sitemap.xml`, `/openapi.json` and `/api/v1/status`.
- The web app has a unit test suite (`platform/web/tests/*.test.ts`, vitest, `npm test`; part of `npm run check`
  and therefore of the CI web job): the documentation catalogue against the content snapshot, the Markdown
  renderer on the real documents (heading ids, link rewriting, Shiki, skill frontmatter), changelog version
  parsing, the `/api` / `/v1` proxy (forwarded headers, 304/204, 502), `pageMetadata` and JSON-LD builders and
  the formatting helpers.
- Browser end-to-end journey for the hosted web app (`platform/web/e2e/customer-journey.spec.ts`, Playwright,
  `npm run e2e`; the CI web job runs it against the standalone build and a throw-away platform API): sign up in
  the UI and read the one-time key, dashboard overview, every sidebar page, a playground decision and the free-plan
  upgrade hint, key create and revoke, the activity row, sign-out and the login redirect, a rejected key, and the
  public pages with live data. The dashboard sidebar is a `navigation` landmark ("Dashboard").

### Fixed

- `osr login --url <server> --token osr_local_...` printed "Signed in to URL as URL": a self-hosted `osr serve`
  token has no account behind it, so the message now names the URL once ("Signed in to URL (self-hosted edition)").
- Web: the dashboard sidebar never listed Governance, Events, Learning and Health (the pages existed and were in
  `dashboardNav`, but not in the sidebar's section map); they are now an "Operate" section, and any future nav
  entry without a section renders under "More" instead of disappearing. The section label reads "Organization"
  like the rest of the site.
- Web: on 1024-1279 px viewports the header's "Compare" link rendered over the search box; the wide search box now
  appears only where it fits (md-lg without the desktop nav, xl and up with it) and the icon button elsewhere.
- Web: documentation sidebar entries were hard-clipped mid-word ("Cost estimates and the MCP se") because Radix
  ScrollArea lays its content out as a table; the viewport content is now block-level and long titles wrap.
- Web: the support and privacy pages linked `/docs/PLATFORM#13-errors` and `#12-privacy-and-data-handling`, two
  sections behind the current numbering (`#15-errors`, `#14-privacy-and-data-handling`). `/dashboard/billing`
  points at support when self-serve billing is disabled instead of only showing the operator command.
- `platform/README.md`: `OSR_PLATFORM_SSO_PROVIDERS` is a JSON map keyed by provider id, not a list.

### Added

Marketplace: the public skills ecosystem
- `osr_platform.harvest` imports the Agent Skills ecosystem and the official MCP registry into the marketplace:
  every `SKILL.md` in each repository on the skills.sh leaderboard, GitHub code search for the long tail (token
  required), and the latest active version of every MCP server as a `tool` listing with its remote endpoints.
  Rows become OCM manifests with `metadata.source` (provider, repository, path, ref, url), the upstream owner as
  publisher, the author's licence and an install command in the readme; the listing page shows a *Source* card
  that links back. `platform/api/scripts/harvest_skills.py collect|stats|publish` runs it (resumable JSONL cache,
  batches through the new admin endpoints `GET|POST /api/v1/admin/registry/import`, or straight into SQLite).
  Marketplace facets are cached for 60 s and the index paginates with a window (the hosted catalogue now has
  140 000+ listings). Every listing payload carries a compact `source` (provider, url, repository) and cards
  show a provenance badge. The catalogue keeps itself current from inside the platform process:
  `OSR_PLATFORM_MARKETPLACE_REFRESH_S` (weekly on Azure) harvests what is new upstream and publishes the
  slugs the store lacks in small batches (`harvest.MarketplaceRefresher`; status under
  `GET /api/v1/admin/registry/import`, `POST /api/v1/admin/registry/import/refresh` runs it now). From a
  machine, `publish --only-new` asks `POST /api/v1/admin/registry/import/check` which slugs exist and sends
  only the rest. Browsing 100k+ listings stays fast: covering indexes for facets, search counts and the
  popular sort, cached stats and a start-up prewarm. `docs/MARKETPLACE.md` "Imported catalogue".

Roadmap exit criteria: v0.5 and v0.6 closed
- `eval.criteria.effort_token_savings()` and `synthetic_effort_rows()` measure the effort-routing half of the
  v0.5 exit criterion: a reasoning model exposed through `expand_elastic()` as `effort="low"` / `effort="high"`
  siblings, routed by the real `Router` with `EffortStrategy` + `TokenBudgetStrategy`, spends 61.6 % of the
  always-think tokens at equal quality (2 000 rows). `EvalRow.tokens` (per-target tokens spent) is the new
  dataset field and `osr eval DATASET --effort` runs the same comparison on rows collected from live
  thinking / non-thinking pairs.
- `eval.agentic` (`AgentTask`, `load_agentic_tasks()`, `synthetic_agentic_tasks()`, `task_routing_frontier()`)
  measures the tau-bench half of the v0.6 exit criterion: multi-step tasks with a per-agent per-step success /
  latency table are replayed through `ProgressRouter` + `TaskCredit` with `CallableHarness` agents and compared
  with every always-use-agent-X policy on common random numbers; routed accuracy 0.987 vs 0.977 for the best
  single agent at 45.4 % of its latency (1 000 tasks). `osr eval TASKS.jsonl --agentic [--retries N]` runs it on
  a real table.
- `scripts/exit_criteria.py` and `criteria.run_all()` include both; 8/8 criteria met. `docs/ROADMAP.md` marks
  v0.5-v0.8 **Complete**; v0.9 closes with the review pack and v1.0 gets its readiness table (see above).
- v0.4 public-suite measurement published in `docs/ROADMAP.md`: on the held-out MT-Bench / PPE / WebDev Arena
  human-preference suites relabelled to three tiers, the learned router beats the declarative router by 10-14 pp
  and every naive baseline, but not the always-best-single-tier task table (weak pairwise labels, noise floor not
  measurable on single-sample rows); ECE <= 0.10 holds only after fitting a calibrator.
- `calibration_report()` returns `held_out`: a `TemperatureScaler` and an `IsotonicCalibrator` fitted on the
  even rows and scored on the odd rows (raw vs temperature vs isotonic ECE / Brier), so `osr eval --calibration`
  shows what a fitted calibrator would achieve out of sample instead of only the raw ECE.
- `examples/leaderboard/`: the three-tier catalogue (`targets.yaml`) and the collect / train / evaluate recipe
  behind the v0.4 table, i.e. the reproducible config the v1.0 leaderboard submissions link to; published on the
  documentation site as "Benchmarks and leaderboard recipe" (`/docs/leaderboard`, Python SDK section).

### Changed

Web site redesign
- The hosted web app has a light, editorial design: Inter for text, Instrument Serif for display titles (marketing,
  docs and auth pages), Geist Mono for code and numerals, Quicksand kept for the wordmark; a warm neutral palette
  (white pages, hairline rules, ink buttons and links) with the brand hues reserved for the mark, charts and target
  kinds. The landing page has a new section structure and copy (hero with the live ledger of most-routed targets,
  vendor strip, six routing principles, how it works, featured models, model landscape, code, live catalogue,
  editions, research, get started) while keeping every live data feed. Marketing pages, the light footer, the auth
  pages, the dashboard shell and the documentation shell (sidebar, topbar, titles) follow the same system; the
  documentation prose itself is unchanged. `docs/BRAND.md` records the website palette and typography.
- The site header dropdown menus render in the shared centered viewport (they previously overflowed the right
  viewport edge and flickered on hover). The landing page gains a live usage band (tokens routed all time and in
  the last 24 hours, requests, success rate, models, accounts), a value section with concrete savings / audit /
  cost numbers, per-industry routing examples and a real healthcare `rules.yaml`, and restrained motion (scroll
  reveals, animated counters, a vendor-logo marquee, all disabled under `prefers-reduced-motion`). A later
  typography pass removed the italic mid-headline pivot from every display heading, the decorative hero glow
  and the card tilt.
- Vendor icon coverage matches the live catalogue (Kwaipilot, Meituan LongCat, IBM Granite added; the Moonshot AI
  mark switched to the visible monochrome variant) and curated vendor display names win over harvested ones.
  The hero prompt examples are chip pills, a floating back-to-top button appears on every page, the rankings
  page shows the daily per-target traffic and vendor/domain share charts, and alert notices flow inline text
  correctly.
- A dark landing-page band presents the routing SLM: the collect / train / eval / serve pipeline, the measured
  holdout numbers, and the autopilot, champion-challenger and distillation loops.

### Added

Account lifecycle end to end
- Anyone can sign up with any email address and a password, and recover the account: `POST /api/v1/auth/password/forgot`
  emails a single-use, one-hour reset link (always `202`, never reveals whether an address exists) and
  `POST /api/v1/auth/password/reset` sets the new password, revokes every session, confirms the address and signs the
  person in. Web pages `/forgot-password`, `/reset-password` and a *forgot password?* link on `/login`.
- Email confirmation: signup sends a link to `/verify-email`; SSO profiles with a verified address, accepted invitations
  and password resets confirm it too. `email_verified` and `last_login_at` on the user object; the account page shows
  a banner with *Send again* (`POST /api/v1/auth/verify`, `POST /api/v1/auth/verify/send`).
- Self-service deletion (`POST /api/v1/me/delete` with the email typed and the password): sessions, identities and
  memberships go, solo workspaces are disabled with their keys, an organization with other members needs another
  owner first; a notice is emailed. *Delete my account* on `/dashboard/account`.
- Invitations are emailed to the invitee (the link is still returned once to the inviter); a removed member's
  sessions on that workspace are revoked.
- Account audit log (`audit_log` table): signups, sign-ins and failed attempts, password and email changes,
  invitations, joins, role changes, removals, deletions and operator actions, mirrored on the `admin` event topic.
  `GET /api/v1/me/audit` (own history, shown on the account page), `GET /api/v1/workspace/audit` (admins),
  `GET /api/v1/admin/audit-log` and the `/admin/audit` console page.
- Outbound email (`osr_platform/mail.py`): standard-library SMTP from `OSR_PLATFORM_SMTP_URL` (`smtp://` STARTTLS or
  `smtps://`) with `OSR_PLATFORM_MAIL_FROM`; every message is also kept in an outbox, so without a mail server operators
  hand the links over from `/admin/mail` (`GET /api/v1/admin/mail[/{id}]`). `mail` in `GET /api/v1/info`; Bicep/azd
  parameters and the deploy workflow carry the two variables.
- Operator console per user: *Send reset link* (`POST /api/v1/admin/users/{id}/reset-link`, link returned for
  support), *Resend confirmation* / *Mark verified* (`POST .../verify-link`, `PATCH` with `email_verified`), lifecycle
  history (`GET .../audit`), last sign-in; users created without a password get a reset link to choose one.

Single sign-on configuration from the CLI
- `scripts/sso.ps1` configures the hosted platform's sign-in providers end to end: `microsoft` creates the Microsoft
  Entra app registration with `az` (work/school and personal accounts, `<WEB_URL>/auth/callback` redirect, Graph
  `openid email profile User.Read`, service principal, two-year client secret), `google` / `github` / `gitlab` walk
  through the provider consoles and prompt for the client pair (no API exists for those OAuth clients), `show`,
  `remove`, `push` (GitHub Actions secret) and `apply` (`azd provision` keeping the running images, then verifies
  `GET /api/v1/auth/providers`). Secrets are never printed.
- JSON-valued deployment inputs are now base64-encoded parameters (`OSR_PLATFORM_SSO_PROVIDERS_B64`,
  `OSR_PLATFORM_STRIPE_PRICES_B64`, decoded in `infra/main.bicep`): `azd` substitutes them textually into
  `main.parameters.json`, so a raw JSON value made every provision fail.

Web site analytics and search optimisation
- Google Analytics 4 on the public pages of the hosted web app with Consent Mode v2: a consent banner (Accept /
  Decline, stored in `localStorage` as `osr-consent`, changeable on the privacy page), denied defaults until the
  visitor accepts, truncated IPs, one `page_view` per client-side navigation and `sign_up` / `login` events as the
  conversions for Google Ads. The tag never loads under `/dashboard` and is absent when the build has no
  `NEXT_PUBLIC_GA_MEASUREMENT_ID` (azd: `OSR_GA_MEASUREMENT_ID`; optional `OSR_GOOGLE_ADS_ID`,
  `OSR_GOOGLE_SITE_VERIFICATION`, `OSR_BING_SITE_VERIFICATION`).
- Every public page now ships a canonical URL, keywords, Open Graph and Twitter card (`pageMetadata` in
  `platform/web/src/lib/seo.ts`); the landing page has a descriptive title, and the model, marketplace, comparison
  and documentation pages have per-entity titles and descriptions. schema.org JSON-LD: `Organization` and `WebSite`
  site-wide, `SoftwareApplication` for the product and for each marketplace listing (with ratings), `Product` offers
  and `FAQPage` on pricing, `BreadcrumbList` and `TechArticle` on documentation, model and comparison pages.
  `robots.txt` blocks the dashboard, proxies, one-time links and query-string variants; the sitemap lists canonical
  paths only. The privacy page and the platform guide describe the analytics tag.
- Search and social distribution end to end: generated Open Graph cards for every page (`/og?title=...`, brand
  typeface, used automatically by `pageMetadata`), programmatic vendor hubs (`/vendors` and `/vendors/<vendor>`
  with per-vendor prices, context windows, benchmarks and FAQ), a `/compare` index for the alternatives pages,
  `/llms.txt` for AI assistants, an Atom release feed at `/feed.xml` (advertised on every page), a `SearchAction`
  on the `WebSite` schema and `ItemList` schema on hub pages. IndexNow: the web app serves `INDEXNOW_KEY` at
  `/indexnow.txt` (azd: `OSR_INDEXNOW_KEY`) and the platform workflow submits the sitemap to Bing/Yandex after
  each deploy (`platform/web/scripts/indexnow.mjs`). Analytics: Core Web Vitals reported to GA4, outbound-link
  and `data-track` call-to-action events, `copy_code` on install snippets and Google Ads conversion labels per
  event (`OSR_GOOGLE_ADS_CONVERSIONS`, e.g. `sign_up=AW-123/label`). Vendors and Compare join the header,
  footer and command palette.

### Added

CLI installers and sign-in
- Native installers for the `osr` command: `curl -LsSf https://opensmartroute.ai/install.sh | sh` (Linux, macOS)
  and `irm https://opensmartroute.ai/install.ps1 | iex` (Windows). Both pick `uv tool install`, `pipx` or a private
  venv (never the system Python), bootstrap uv when nothing else is available, honour `OSR_VERSION`, `OSR_EXTRAS`,
  `OSR_INSTALLER` and `OSR_NO_MODIFY_PATH`, put `osr` on PATH and verify with `osr --version`. The scripts live at
  the repository root, ship in the sdist and with every GitHub release, and the web app serves them at
  `/install.sh` and `/install.ps1`; `osr --version` prints the SDK version.
- `osr login` signs in to the hosted platform (community or enterprise edition) with the OAuth 2.0 device
  authorization grant (RFC 8628): the CLI prints a short code, opens `/cli/authorize`, the user approves in the
  browser and the platform mints a workspace API key for that machine. `osr login --token` / `--with-token` store a
  pasted key or a self-hosted server token instead; `--url` and `--profile` keep several deployments side by side.
  `osr whoami`, `osr logout [--all]`, `osr token generate|create|list|revoke` and `osr mcp --remote` (bridge to the
  signed-in workspace without copying the key) complete the set. Credentials are stored per profile in
  `~/.config/opensmartroute/credentials.json` (`%APPDATA%\opensmartroute` on Windows, `OSR_CONFIG_DIR`); flags,
  then `OSR_API_URL` / `OSR_API_KEY`, then the saved profile decide which one applies.
- `opensmartroute.credentials`: `config_dir`, `CredentialStore`, `Credential`, `PlatformClient`, `device_login`,
  `whoami`, `generate_token`, `token_kind`, `redact` - stdlib only, injectable transport. `branding` gains
  `WEBSITE`, `REPOSITORY`, `INSTALL_SCRIPT_SH|PS1`, `API_URL_ENV`, `API_KEY_ENV`, `CONFIG_DIR_ENV`, `TOKEN_PREFIX`,
  `LOCAL_TOKEN_PREFIX` and `platform_url()`; `errors.AuthenticationError` (`OSR_AUTH`).
- Self-hosted `osr serve` can now require access tokens: `serve --token T` (repeatable), `--generate-token`
  (prints an `osr_local_...` token once with the matching `osr login` command), `--require-auth`, or
  `ServerSettings` (`OSR_SERVER_AUTH_TOKENS`, `OSR_SERVER_AUTH_TOKENS_FILE`, `OSR_SERVER_REQUIRE_AUTH`);
  `create_app(router, auth_tokens=[...])`. Every path except `/healthz`, `/readyz`, `/metrics`, `/whoami` and the
  OpenAPI documents then needs `Authorization: Bearer` or `X-API-Key` (constant-time compare, 401 with
  `WWW-Authenticate`). New `GET /whoami` describes the server and whether the caller is authenticated. Helm chart:
  `auth.tokens`, `auth.existingSecret`, `auth.required`.
- Platform API: `POST /api/v1/auth/device/code` (anonymous, rate limited), `GET /api/v1/auth/device/{user_code}`,
  `POST /api/v1/auth/device/approve|deny` (admin role, plan key quota) and `POST /api/v1/auth/device/token`
  (`authorization_pending`, `slow_down`, `access_denied`, `expired_token`); device codes are stored hashed and
  expire after 15 minutes. Web: `/cli/authorize` approval page (sign-in redirect, code entry, key name, approve/deny),
  install one-liners on the docs landing page, footer and integrations page.

Tracing and observability
- `opensmartroute.observability`: a `Tracer` that every stage of a request reports to - spans `request`,
  `http.request`, `route`, `plan`, `execute`, `autopilot.cycle` and events for signals, policy, pinning,
  narrowing, shortlist, ranking (per-strategy timings), escalation, exploration, abstention, fallback, plan
  slots, execution steps, outcomes, task credit, calibration, breaker transitions, cache hits and misses,
  tenant rejections, slow decisions, guard verdicts, shadow comparisons and A/B verdicts, fair-share
  throttling and autopilot drift (`SPAN_NAMES`, `EVENT_NAMES`). Sinks: `MemorySink` (ring buffer with
  `events()` / `trace(request_id)`), `MetricsSink` (counters, p50/p95/p99, `prometheus()`), `LoggingSink`,
  `FileSink` (JSONL) and `adapters.optional.OpenTelemetrySink` (`otel` extra: OTel spans nested through
  the OTel context and joined to an inbound `traceparent`, span events, counters and a duration histogram).
  `configure_tracing(*sinks)` / `get_tracer()` for the process-wide tracer, `Router(tracer=)`,
  `RouterBuilder.with_tracing(...)`, `current_tracer()` / `use_tracer()` for components; trace ids come
  from `traceparent`, a UUID request id or a hash of the request id. Request text never enters a span
  (`text_digest()`); attributes are bounded and JSON-clean; a failing sink is counted, never raised.
  `ObservabilitySettings` (`OSR_OBSERVABILITY_METRICS|MEMORY_EVENTS|LOG_EVENTS|EVENTS_FILE|OTEL|SAMPLE_RATE|ATTRIBUTE_MAX_LEN`).
- `osr serve`: `http.request` span per request (probes and observability endpoints excluded),
  `X-OSR-Trace-Id` response header, `GET /events` (filter by request id, trace id, name glob, kind, level),
  `GET /trace/{request_id}`, tracer counters appended to `GET /metrics`, `observability` block in `/stats`.
  `osr route --events` prints the trace of the decision; every CLI router traces into the sinks named by
  `OSR_OBSERVABILITY_*`. The hosted platform wires the same tracer into both editions (`/api/v1/stats`).
  New `docs/OBSERVABILITY.md`.

Metrics, caching and AI governance end to end
- `enterprise.MetricsTelemetry` is a real metrics store: `snapshot()` returns decision counts per target,
  policy rejections, errors by type, a routing-latency histogram with p50/p95/p99, mean confidence,
  outcomes per target (success rate, cost, quality, latency) and uptime; `prometheus(namespace=, extra=)`
  renders the Prometheus exposition format (`osr_route_decisions_total`, `osr_route_latency_ms_bucket`,
  `osr_outcome_cost_usd_total`, `osr_decision_cache_hits_total`, ...). `CacheMiddleware.stats()`,
  `invalidate()` and `len()`; `RouterBuilder.with_cache(ttl_s, max_size)` and `with_audit(sink,
  outcomes=True)` so the hash chain also records how each decision turned out.
- `osr serve`: `/healthz` and `/readyz` probes, `GET /metrics` in Prometheus format (router metrics plus
  tracer counters and a `targets` gauge), request-latency and error observation on every routed call. The
  Helm chart and the Azure Container Apps template use `/readyz` for readiness; the chart's default
  `podAnnotations` carry `prometheus.io/scrape|path|port`.
- Platform API observability (`osr_platform.observability`): every response carries `X-Request-Id`
  (echoed when the client sends one) and `Server-Timing`; JSON access log on `osr.platform.access` with
  route template, status, latency and account; per-route HTTP counters, latency histogram and in-flight
  gauge. `GET /healthz` (targets, uptime), `GET /readyz` (503 with the failing check: database,
  catalogue, router, audit file) and `GET /metrics` (`osr_platform_http_*` plus the router's metrics;
  public by default, `OSR_PLATFORM_METRICS_PUBLIC=false` restricts it to `X-Admin-Token`).
  `GET /api/v1/stats` adds `http`, `controls`, `cache` and `observability` blocks; breaker snapshots
  serialise cleanly (`budget_remaining: null` instead of infinity).
- Platform caching: public catalogue reads (`/api/v1/info`, `/models*`, `/rankings`, `/llms*`,
  `/catalogue`, `/stats/public`) return `ETag` and `Cache-Control: public, max-age, stale-while-revalidate`
  and answer `304` to `If-None-Match` (`OSR_PLATFORM_HTTP_CACHE_MAX_AGE_S`, `0` disables). An optional
  decision cache in front of either edition (`OSR_PLATFORM_ROUTE_CACHE_TTL_S`) reuses recent decisions for
  identical requests; hit rates appear in `/api/v1/stats` and `/metrics`.
- Platform governance: a **workspace policy** (`GET|PUT|DELETE /api/v1/policy`, admin role) applies the
  tenant constraint keys plus `daily_budget_usd` / `monthly_budget_usd` to every request of the
  workspace; tenants accept the same budget keys. Policy and tenant constraints merge deterministically
  (boundaries fill in, caps take the stricter value, deny lists union, allow lists intersect); unknown
  keys, unknown targets and allow/deny overlaps are rejected with `400`. Executed spend is attributed per
  tenant (`X-OSR-Tenant`); once a budget is exhausted routed calls return `429` with `Retry-After` and
  `X-Budget-Limit|Used|Period`. `GET /api/v1/governance` returns the whole posture in one payload:
  controls in force (input guard, PII redaction, steering strip, metrics, tracing, audit chain, circuit
  breakers, decision cache, learning persistence), audit-chain verification, retention, policy, budgets
  and spend, quota, tenants with monthly spend, and the catalogue's data boundaries and PII-capable
  targets. Usage rows older than `OSR_PLATFORM_RETENTION_DAYS` (default 365, `0` keeps everything) are
  purged by a background sweep.
- Web: `/dashboard/governance` (policy editor with budgets, controls, budget bars, audit and retention,
  catalogue boundaries, tenant spend) and `/dashboard/health` (enterprise: HTTP traffic, error rate,
  routing decisions and latency histogram, circuit breakers, decision cache hit rate, per-route table,
  auto-refresh). The tenant editor gains daily and monthly budgets. The proxy forwards `If-None-Match`,
  `ETag`, `Server-Timing` and the `X-Budget-*` headers; `ApiError` exposes `requestId` and `budget`.

Observability portal: traces, events and readiness in the dashboard
- Platform API: `GET /api/v1/trace/{request_id}` returns the activity row of one of the workspace's
  requests plus every span and event the tracer buffered for it (`trace_id`, `spans`, root `duration_ms`;
  `404` for another workspace's id, `events: []` once evicted). `GET /api/v1/events` lists recent tracer
  events scoped to the workspace through its request ids (`name` with `*` wildcard, `kind`, `level`,
  `request_id`, `limit` up to 2000; deployment-wide events without a request on plans with `stats`;
  `404` when `OSR_OBSERVABILITY_MEMORY_EVENTS=0`). `GET /api/v1/status` is a public readiness endpoint
  for status pages (checks, edition, versions, uptime, controls, tracing buffer; `503` while a check
  fails, `no-store`). `POST /api/v1/route` responses carry `trace_id` and `X-OSR-Trace-Id`;
  `GET /api/v1/activity` marks rows still in the buffer with `traced` and returns the buffer state.
  `usage(request_id)` is indexed.
- Closed loop: `POST /api/v1/feedback` is now stored per request (`feedback` table, purged with usage
  retention); `GET /api/v1/trace/{request_id}` returns the request's `outcomes` and its hash-chained
  `audit` records (decision and outcome, plans with `audit`), and activity rows carry an `outcome`
  summary (`reports`, `success`, mean `quality`).
- Web: `/dashboard/events` (live event stream with auto-refresh, stage/kind/level filters, most-frequent
  names, click-through to the trace); a **trace drawer** on every traced activity row and a *Trace* tab
  in the playground rendering a span waterfall (offsets and durations relative to the root span, nested
  events, level badges, expandable attributes, problems filter) with a *How it was routed* story
  (signals, policy rejections and guard verdicts, ranking bars, decision with fallbacks and execution
  steps), the reported outcome (or the feedback call to make) and the audit records; an *Outcome* column
  in the activity log; a *Systems* card on `/dashboard/health`
  (also shown in the community edition) and a status pill on the overview polling `/api/v1/status`.
  New `components/observability/trace-view.tsx` and `system-status.tsx`; the proxy forwards
  `X-OSR-Trace-Id`.

Routing SLM and autopilot on the hosted platform
- SDK: `EnterpriseRouter` now runs `Router.observers` on the auto-learning path too, so an autopilot's
  drift hook sees every outcome in the enterprise edition (it was silently dead there). `SelfImprover`
  reports each cycle as a `learn.improve` event (rows, champion vs challenger accuracy, verdict, reason)
  and each promotion as `learn.promote`; `Autopilot(tracer=)` names the tracer cycles report to when the
  host's tracer is not the process-wide one.
- Platform: `OSR_PLATFORM_SLM` serves a routing SLM as the `slm` strategy; `OSR_PLATFORM_AUTOPILOT`
  (with `_OFFLINE`, `_SOURCES`, `_INTERVAL_S`, `_MIN_ROWS`) runs the self-improvement loop in-process on
  the platform's own feedback, promoting a challenger only when it beats the champion and saving it to
  `DATA_DIR/autopilot/slm.json` (preferred over the mounted file after a restart). `GET /api/v1/learning`
  (any plan) returns the strategy ensemble and weights, learner state (persistence, drift resets,
  quarantine), per-target outcome statistics (JSON-safe), the SLM in service (rows, sources, encoder,
  calibration, fit history) and the autopilot status with recent improvement reports;
  `POST /api/v1/admin/autopilot/cycle` schedules a cycle; `controls` / `info` gain `slm` and `autopilot`.
- Web: `/dashboard/learning` (strategy weights, outcomes per target, SLM card with accuracy sparkline and
  fit history, autopilot card with drift, cycles, promotions, champion-vs-challenger and a *Run cycle*
  action, improvement-cycle table); Governance lists the SLM and autopilot controls.

Azure DNS zone and custom domain
- `infra/dns.bicep` (parameter `dnsZoneName`, azd `OSR_DNS_ZONE`): public Azure DNS zone for the platform with
  apex A to the Container Apps environment static IP, `www`/`api` CNAMEs to the app FQDNs, `asuid.*` TXT records
  for hostname validation and SPF/DMARC reject records; outputs `OSR_DNS_NAME_SERVERS`. `scripts/domains.ps1` is
  now Azure-only (phases `dns`, `bind`, `verify`, `clean`; no Cloudflare token): it creates the zone, waits for the
  registrar delegation, binds the hostnames with managed certificates, rebuilds web with the domain baked in and
  checks site, API, MCP and redirects end to end; `clean` retires the superseded `osr-platform` container app.
- Web: `platform/web/src/proxy.ts` 301s alias hostnames listed in `OSR_WEB_REDIRECT_HOSTS` (set by Bicep to the
  web alias domains, e.g. `www.`) to the canonical `NEXT_PUBLIC_SITE_URL`.
- Apex managed certificates validate over HTTP (`TXT` validation never completed for the apex); subdomains keep
  CNAME validation.
- Continuous deployment: the `azd up` job in `.github/workflows/platform.yml` signs in with an OIDC federated
  credential bound to the `production` GitHub environment, reads the custom-domain parameters from repository
  variables and seeds `SERVICE_*_IMAGE_NAME` from the running apps so a provision never swaps them to the placeholder
  image. Setup commands in `platform/README.md` ("Continuous deployment").

Quotes, recommended models and the MCP server
- `opensmartroute.estimate`: quote a request before sending it. `estimate(router, request,
  output_tokens=, kinds=, prices=, quality_tolerance=, decision=)` routes as usual and prices every
  ranked candidate (`TargetEstimate`: input / output / per-call cost, latency, quality estimate,
  context fit, energy and CO2, rationale) into a `RequestEstimate` with the `recommended`,
  `cheapest` (within a quality tolerance of the best), `best_quality` and `fastest` picks and
  `savings_usd`; `estimate_tokens` / `estimate_messages_tokens` heuristics, `target_prices` (split
  `usd_per_1k_input` / `usd_per_1k_output` when declared), `PriceHook` for live price feeds. CLI
  `osr estimate TEXT [--output-tokens N] [--cost-weight W] [--monthly R] [--json]`.
- `opensmartroute.mcp_server`: a standard-library Model Context Protocol server (`2025-06-18`, JSON-RPC
  2.0) over any router. Tools `route`, `estimate`, `recommend` (priority balanced / cost / quality /
  speed), `explain`, `list_targets`, `feedback`, plus `ask` (route and execute) and
  `marketplace_search` / `marketplace_get` when the `execute` / `marketplace` hooks are given;
  resources `osr://targets` and `osr://stats`; `serve_stdio` transport, `RemoteMCP` / `bridge_stdio`
  HTTP forwarder. CLI `osr mcp [--list-tools]` serves a catalogue over stdio, `osr mcp --url <platform>/mcp
  --api-key` bridges to a hosted platform. `osr serve` mounts `POST /mcp` and `GET /mcp`.
- Platform: `POST /api/v1/estimate` (anonymous, rate limited per client, or keyed and metered as
  `estimate`; vendor / model names, live catalogue prices, optional `monthly_requests` projection),
  `GET /api/v1/models/recommended` (recommended, cheapest, best-quality and fastest pick per use case -
  chat, code, reasoning, summarise, extraction, pii - from live quotes, plus a leaderboard, cached one
  minute) and the MCP endpoint `POST /mcp` / `GET /mcp` (tag `mcp`; scoped to the workspace, metered
  as `mcp`, `ask` on the plan tier, marketplace tools backed by the registry). Web: `/estimate` page
  (quote form, four picks, signals, candidate table, monthly projection), "Recommended per use case"
  on `/models`, *Dashboard -> Integrations* with copy-paste MCP configuration for VS Code, Cursor,
  Windsurf, Claude Code and Claude Desktop, and `/mcp` proxied on the web origin. `docs/MCP.md`.

Web app chrome and platform pages
- Header: the main navigation collapses into the menu below the `lg` breakpoint (it overflowed on
  tablets), the menu closes on navigation and lists the signed-in shortcuts, active items carry
  `aria-current`, and a "Skip to content" link targets `<main id="main">`.
- Footer: plain headings, four groups (Product, Developers, Resources, Company) with the REST and
  Python references labelled apart, licence / SDK version / edition as a definition list, and a legal
  bar (Terms, Privacy, Support, Security).
- New pages `/support` (documentation entry points, issues and discussions, account and billing,
  security reports, enterprise, service status), `/privacy` (what the platform stores per table, request
  content handling, browser storage, processors, retention, access and deletion) and `/terms` (accounts
  and keys, plans and payment, content, acceptable use, marketplace, availability, suspension, open
  source, liability). Linked from the footer, the signup form, the dashboard footer, the 404 and error
  pages, the command palette and the sitemap; the CI container smoke test requests them.
- Command palette lists every public page (marketplace, estimate, pricing, REST API, support).

Marketplace and declarative stacks
- `opensmartroute.stack`: declarative *stacks* - one YAML/JSON document (`osr: "1"`, `kind: stack`)
  holding targets (plain or OCM), rules, settings, objective and `imports` of other stacks, catalogues
  or marketplace templates (`registry://slug@version`). `load_stack`, `validate_stack`, `plan_stack`
  (diff against a deployed stack), `dump_stack`, `starter_stack` and `Stack.router()` mirror the
  infrastructure-as-code check / diff / apply workflow; the CLI gains `osr stack init | validate |
  plan | apply` (`--registry`, `--against`, `--out`, `--route`). `examples/stack.yaml` is a complete
  support-desk example.
- Hosted marketplace (`platform/api/osr_platform/registry.py`, tag `marketplace`): listings of kind
  template, agent, skill, persona, tool, llm and prompt with slug, semver versions and changelog,
  publisher, licence, tags, Markdown readme, price, installs and star ratings. Public browse and
  detail (`GET /api/v1/registry`, `/registry/kinds`, `/registry/{slug}`, `/manifest?version=`,
  `/reviews`), publisher lifecycle draft -> review -> published -> archived (`POST /registry`,
  `PATCH`, `/submit`, `/versions`, `/archive`, `/unarchive`, `GET /me/listings`), installs and
  purchases (`POST /registry/{slug}/install`, `GET /me/installs`, one-time Stripe checkout confirmed by
  webhook), reviews (`PUT`/`DELETE /registry/{slug}/reviews`) and moderation under `/api/v1/admin/registry`
  (`approve`, `reject`, `flags`, `unpublish`). Manifests are validated against the OCM / stack schemas;
  template manifests may be sent as `{"yaml": "..."}`. Settings `OSR_PLATFORM_REGISTRY_AUTO_PUBLISH`
  (free listings go live on submit) and `OSR_PLATFORM_REGISTRY_SEED` (seed the repository's skills,
  the starter and example templates, a persona and prompts, all verified).
- Web: `/marketplace` (kind rail, search, filters, featured, pagination), `/marketplace/{slug}`
  (about, use-it snippets, manifest, reviews with histogram, versions, install / buy / rate),
  `/marketplace/publish` (four-step wizard, no code needed for prompts and personas) and
  `Dashboard -> Marketplace` (my listings with status and reviewer notes, edit page with new-version
  form, installed items). Marketplace links in the main navigation, footer, dashboard and sitemap.
  User docs: `docs/MARKETPLACE.md` (site page `/docs/MARKETPLACE`).
- OCM cost block accepts `context_tokens`; `capability_from_target` emits it.
- `PlatformAPI.optional_principal` dependency (anonymous or authenticated) and
  `Billing.listing_checkout_url` / `Billing.on_purchase` for one-time purchases.

Documentation site
- The hosted web app (`platform/web`) now renders the documentation itself as a static Next.js site at
  `/docs`: `scripts/sync-content.mjs` snapshots the repository Markdown (`README`, `CHANGELOG`,
  `CONTRIBUTING`, `SECURITY`, `docs/**`, `deploy/README.md`, `spec/ocm/README.md`,
  `.claude/skills/*/SKILL.md`) into `content/`; `src/lib/docs/catalogue.ts` is
  the single source of truth for sections, slugs and summaries; `src/lib/docs/render.ts` renders with
  remark/rehype (GFM, GitHub-compatible heading ids, Shiki highlighting, Mermaid, relative-link
  rewriting). Pages get a collapsible sidebar, scroll-spy table of contents, breadcrumbs, reading time,
  code copy buttons, previous/next links and an "Edit on GitHub" link. New pages: every Agent-Skills
  package (`/docs/skills/<name>`, with its frontmatter), `/docs/deploy`, `/docs/ocm`,
  `/docs/contributing`, `/docs/security-policy`.
  `/docs/search.json` feeds documentation pages and headings into the Ctrl/Cmd+K palette; the sitemap
  lists every page. The docs no longer depend on the platform API being up.
- The site is organised for end users of the hosted platform, the Python SDK and the pip package:
  sections Start, Hosted platform, Python SDK, Self-hosting, Concepts, Security, Agent skills and
  Releases, with a section tab bar and audience paths on the index. Internal material
  (`docs/PLATFORM_PLAN.md`, `docs/GO_TO_MARKET.md`, `docs/BRAND.md`, `docs/sales/`,
  `platform/README.md`) is no longer published on the site and stays in the repository.
- `docs/PLATFORM.md`: platform guide for end users (authentication, `/api/v1/route` fields and
  constraints, the `/api/v1/estimate` quote, execution, the OpenAI-compatible endpoint, feedback,
  plans and quotas, organizations and roles, organization SSO, tenants, the marketplace, the MCP
  server, dashboard pages, privacy, error handling). `docs/MARKETPLACE.md` is published under Hosted
  platform. `docs/GUIDE.md` lists every `osr` subcommand grouped by task.
- REST API reference generated from the platform's OpenAPI document: `/docs/api` (authentication
  schemes, base URL, conventions, endpoint groups) and one page per tag (`/docs/api/routing`,
  `/docs/api/openai`, `/docs/api/public`, `/docs/api/account`, `/docs/api/workspaces`,
  `/docs/api/auth`, `/docs/api/billing`, `/docs/api/mcp`, `/docs/api/marketplace`) with parameters,
  request and response schemas, curl and JSON examples; operations are searchable from the palette.
  Operator-only `/api/v1/admin/*` paths are left out whatever tag they carry.
- Version menu in the docs top bar built from `CHANGELOG.md` (current release, unreleased changes,
  links to each release section and PyPI).
- `.claude/skills/osr-platform`: Agent-Skills package for the hosted platform (API, web app, the
  documentation site and `azd` deployment); root `AGENTS.md` and `.github/copilot-instructions.md`
  point coding agents at the gates, the easy-to-break rules and the skill for each task.

Platform API
- The OpenAPI document declares `bearer` and `apiKey` security schemes (replacing the per-operation
  `authorization` / `x-api-key` header parameters), tag descriptions for every route group and a
  richer info block. `platform/api/openapi.json` is a committed snapshot regenerated with
  `python platform/api/scripts/export_openapi.py`; `--check` (run in CI and by
  `test_openapi_snapshot_is_current`) fails on drift.

Routing SLM and self-improvement
- `learning.RouterSLM`: a self-contained small routing model (hashed dual encoder, temperature
  calibration, target snapshots) that trains on `EvalRow`s, predicts a calibrated distribution and a
  quality/cost/latency utility per candidate, learns online from outcomes, and saves to a compact JSON
  file (`save` / `from_file`, `SLM_FORMAT`). `learning.distill_router()` compresses the whole ensemble
  (rules, capability fit, learners, judge) into one SLM from its traces or propensities.
  `learning.SLMStrategy` puts it into the ensemble (weight `OSR_WEIGHTS_SLM`) with online updates;
  `Router` accepts it like any other strategy and `osr --slm model.json route` loads one from the CLI.
- `adapters.ModelCatalogue` / `ModelCard`: live model catalogue from OpenRouter (prices, context,
  modalities, tool support) and Hugging Face model cards (downloads, likes, `model-index` benchmarks ->
  `quality_from_benchmarks`, `quality_from_popularity`), merged per model id and persisted; `targets()`
  filters by quality/price into `RouteTarget`s (`card_to_target` replaces injection-risky third-party
  descriptions), `cost_benchmark()`, `frontier()` (Pareto), `to_markdown()`; fail-soft `refresh()`.
- `adapters.WebKnowledge`: web-search discovery with keyless providers (`huggingface_search`,
  `duckduckgo_search`) and `brave_search` when `BRAVE_API_KEY` is set; `fetch_bytes` / `fetch_json` /
  `fetch_page_text` / `html_to_text` (https-only, byte-capped, risk-scored pages); a cached hit list
  that `discover_models()` feeds into the catalogue.
- `eval.DatasetCollector` / `DatasetSource` / `KNOWN_SOURCES`: collect routing datasets from the
  Hugging Face datasets-server (`fetch_hf_rows`, `iter_hf_rows`, `collect_dataset`) into a JSONL
  cache, plus `rows_from_feedback()` (outcomes -> labelled rows) and `synthetic_rows()` (ontology
  seed prompts scored by capability fit); `corpus()` dedupes and caps, `split()` is a deterministic
  holdout. `DEFAULT_SOURCES` are the publicly served pairwise battle sets `routellm-battles`
  (RouteLLM GPT-4-judged battles) and `arena-55k` (LMArena human preference); the RouterBench and
  RouterEval presets stay in `KNOWN_SOURCES` flagged `gated` (the Hub does not serve their rows
  without authentication). `from_pairwise_row()` turns a battle into an `EvalRow` (ties make both
  sides acceptable); `model_quality()` fits a Bradley-Terry model over the battles and returns each
  model's win probability against the field, which the catalogue applies as an `arena` quality
  prior. `ARENA_TIERS` / `tier_of()` / `tier_model_map()` fold the retired arena model names onto
  the operator's tiers (`osr collect --tier frontier=llm-frontier --tier mid=llm-mid --tier
  small=llm-small`, also on `osr improve`); `DatasetCollector.derive()` remaps an existing cache
  offline so tiered corpora never need a second download. `collect()` keeps partial pages when the
  Hub rate-limits mid-way and never replaces a fuller cache with a shorter one; `fetch_bytes` retries
  429/5xx with exponential backoff honouring `Retry-After` (`OSR_SLM_FETCH_RETRIES`,
  `OSR_SLM_FETCH_BACKOFF_S`, `OSR_SLM_FETCH_BACKOFF_MAX_S`) and pages pause `OSR_SLM_PAGE_PAUSE_S`.
- `adapters.fetch_leaderboard_quality()`: Open LLM Leaderboard results (IFEval, BBH, MATH, GPQA, MuSR,
  MMLU-Pro) per model via the datasets-server; `ModelCatalogue.apply_quality()` maps them (and the
  arena win rates) onto cards by exact id or `model_key()` (case/variant-insensitive tail), ranking
  sources popularity < catalogue < arena < leaderboard so a measured prior is never downgraded;
  `refresh(leaderboard=True)`, `targets(measured=True)` and `osr catalogue --leaderboard
  --quality-from DATA_DIR --measured`; `osr slm train --catalogue catalogue.json --source NAME` trains
  on measured, priced catalogue models and on selected caches only.
- `adapters.attach_chat_handlers()`: give every LLM target a `chat_handler` on one OpenAI-compatible
  client (`metadata.model` names the upstream model, or a default). The CLI does this at startup when
  `OSR_LLM_BASE_URL` is set (`OSR_LLM_API_KEY`, `OSR_LLM_API_KEY_ENV`, `OSR_LLM_MODEL`), so the
  container's `/v1/chat/completions` executes against OpenAI, Azure OpenAI, vLLM, Ollama, OpenRouter
  or LiteLLM instead of answering 502. `huggingface_search()` limits model hits to `text-generation`
  and falls back to per-keyword queries (the Hub matches repo names, not natural language);
  `fetch_json()` returns `None` for an empty 200 body (DuckDuckGo does that under load).
- Deployment: `OSR_SLM` (Docker entrypoint, Helm `config.slm` -> `/config/slm.json`) loads a routing
  SLM into the served ensemble; `OSR_LLM_BASE_URL` / `OSR_LLM_MODEL` are declared in the image.
- `learning.SelfImprover` / `ImprovementReport`: the closed loop - refresh catalogue, discover models
  and datasets on the web, gather evidence (feedback, collected datasets, synthetic), train a
  challenger on the train split, calibrate it on the holdout, and promote it over the champion only
  when holdout accuracy improves by `OSR_SLM_MIN_GAIN`; JSONL history, `run(interval_s)` loop.
- CLI: `osr catalogue`, `osr collect`, `osr slm train|eval|predict|info`, `osr improve` and the global
  `--slm` option; `settings.SLMSettings` (`OSR_SLM_*`) and `WeightSettings.slm`.
- Transformer encoders for the SLM. `learning.AttentionEncoder` is a pure-Python transformer block
  (hashed token embeddings, sinusoidal positions, multi-head self-attention, residual, attention
  pooling, hand-derived gradients) that `ContrastiveRouter(attention=...)` adds to the hashed query
  encoder so tokens can condition on each other; opt in with `OSR_SLM_ENCODER=attention`
  (`OSR_SLM_ATTENTION_HEADS`, `_HEAD_DIM`, `_MAX_TOKENS`, `_VOCAB`); the block is persisted in the
  model file (`core.encoder`, `core.attention`) and restored by `from_file`. `learning.EmbeddingFeaturizer`
  / `load_embedder()` append a frozen pretrained sentence-transformer embedding (extra `embeddings`, or
  any `Callable[[list[str]], list[list[float]]]`) as dense features, so `W` learns a linear head on top
  of it; `RouterSLM(embedder=...)`, `OSR_SLM_EMBEDDER=<model>` / `OSR_SLM_EMBEDDER_SCALE`, the file
  records the embedder name and `from_file(embedder=...)` takes a custom one back.
- Self-operation: `learning.Autopilot` runs `SelfImprover` cycles inside a live process on a schedule
  and on drift (`learning.DriftMonitor`, Page-Hinkley over the success indicator and quality of every
  outcome the router learns from), rate-limited by a minimum gap, hot-swapping an accepted challenger
  into the served `SLMStrategy` and rewriting the model file; failures are recorded, never fatal.
  `Router.observers` is the outcome hook it (and any drift monitor or metric) plugs into.
  `osr serve --autopilot [--autopilot-interval S --cache-dir DIR --catalogue FILE --source NAME
  --search QUERY --tier TIER=TARGET --offline --min-rows N]`, `create_app(router, autopilot=...)`
  exposes `autopilot` in `GET /stats` and `POST /autopilot/cycle`; settings
  `OSR_SLM_AUTOPILOT_INTERVAL_S`, `_MIN_GAP_S`, `_DRIFT_THRESHOLD`, `_DRIFT_MIN_N`, `_REMEMBER`;
  container / Helm: `OSR_AUTOPILOT=1`, `OSR_AUTOPILOT_ARGS`, `OSR_CACHE_DIR`, `OSR_CATALOGUE`.
  `status()["busy"]` says a (possibly multi-minute) cycle is in flight. `DatasetCollector.corpus()` /
  `cached()` skip the improver's own `improve-history.jsonl` and any non-row line in the cache dir -
  the second cycle used to fail with `EvalRow.__init__() missing 'text'` when the log shared the dir.
- Routing datasets: `osr collect` knows ten public sources verified against the datasets-server -
  the 2024-25 LMArena releases (`arena-100k`, `arena-140k`, `ppe-human`, `webdev-arena`), `mt-bench-human`,
  `reward-bench`, `ultrafeedback` (per-model judge scores) and `routellm-gpt4` (RouteLLM's own "is the
  cheap model good enough" labels), next to `routellm-battles` and `arena-55k`; gated ones stay listed but
  off by default. Parsers understand the newer layouts (typed conversation turns, `winner` labels, per-model
  score lists) and carry the code / language / math / hard-prompt tags into `context`; `tier_of()` has
  name rules and a parameter-count fallback for the 2024-26 model generations (`gpt-4o-mini` -> mid,
  `gemini-2.5-pro` -> frontier, `llama-3.1-8b` -> small ...) and `DatasetSource.tiers` folds every side
  onto your catalogue at parse time (`--tier` no longer needs an exhaustive model table). Collection is
  resumable: a `<name>.meta.json` records the raw offset, a run cut short by a 429 keeps its rows and the
  next `osr collect` continues from there (`DatasetCollector.progress()`); retries back off up to five
  times / 60 s. `corpus()` fills `OSR_SLM_MAX_ROWS` round-robin across sources so one large dataset cannot
  crowd the others out.
- SLM training is faster and generalises better. With numpy installed (extra `fast`, now in the serve
  image) `ContrastiveRouter.fit()` runs the same SGD on dense arrays - about 10x faster (18 000 rows in
  under a minute) - and inference projects through a dense mirror of `W`; `OSR_SLM_BACKEND=auto|numpy|python`
  picks the arithmetic, both paths seed identically and produce the same model file (tested to 1e-9), so a
  pure-Python deployment reads a numpy-trained model unchanged. The hashed featurizer caches token hashes.
  The model gains a learned per-target logit bias (`B`, the base rate a unit-norm dot product cannot
  express; older files load with none), inverse-time learning-rate decay (`OSR_SLM_LR_DECAY`, default 2) and
  regularised defaults (`epochs` 4, `l2` 1e-3): on the 18 000-row mixed corpus the previous defaults
  overfit below the constant "always the mid tier" baseline (0.62 vs 0.64 on a random holdout); the new
  ones sit at 0.64-0.66 with holdout loss 0.98-1.00 vs 1.11 and are stable across seeds.
- The SLM no longer harms mixed catalogues. Scored rows train a softmax over the *scored population* (the
  targets any row of the corpus scores, plus `OSR_SLM_NULL_TARGETS` learned "none of these" logits) instead
  of the whole catalogue, so a corpus of LLM battles leaves the embeddings of tools, skills and agents it
  never compared untouched (they used to be driven to p=0.0005 and any SLM took `examples/eval_dataset.jsonl`
  from 0.83 to 0.43; it is 0.80 now). `SLMStrategy` / `ContrastiveStrategy` abstain on targets the model
  has never compared (`ContrastiveRouter.B` records them; `RouterSLM.predict_proba()` with no explicit
  candidates ranks only those), score the rest centred (best 1.0, mean 0.5, so a flat softmax is "no
  preference" rather than a boost) and scale confidence by the share of the request's candidates they know.
  Chosen on three leave-one-source-out suites (PPE 0.561, MT-Bench 0.508, WebDev Arena 0.506 - see
  `docs/ROADMAP.md` v0.4).
- `eval.load_dataset()` raises `ConfigurationError` naming the file and line for a line that is not JSON, not
  an object or has no text (it used to surface as a `TypeError` traceback), and `osr catalogue | collect | slm
  | improve` print every SDK error as one `error:` line with its path / line / preset / backend detail and exit
  1. `ContrastiveRouter(backend=...)` with an unknown backend is a `ConfigurationError` too.

Agentic core (PLATFORM_PLAN Phase 0)
- `signals.EventSignal`, `WorkflowSignal`, `parse_event()`, `EventInfo` and the `EVENT_*` lexicons:
  textless requests (`context["event"]`, `context["workflow"]`) get domain / action signals so events
  route to workflows and agents; rules match `event:` globs and `workflow: true`.
- `strategies.SessionAffinityStrategy` / `SessionState`: keep multi-turn conversations on the target
  that owns the session unless quality drops; `Router.route(..., exclude=[...])` and fallback
  candidates; `PLAN_ROLES` is kind-aware (agent / workflow plans need no persona); per-call pricing
  on targets (`cost.usd_per_call`, `RouteTarget.cost_per_call`, `estimated_cost()`); `observe` hook.
- Proxy: `osr/auto:<variant>` presets (`cheap`, `fast`, `quality`, `private`, `agentic`, `llm`),
  `osr` request block (exclude, objective, plan, fallbacks), `X-OSR-App` attribution and
  `X-OSR-Target` / `X-OSR-Request-Id` response headers; metadata carries alternatives and cost.
- Executors: `adapters.http_handler()`, `mcp_tool_handler()` and queue-backed targets
  (`Queue` protocol, `InMemoryQueue`, `queue_handler()`, `PendingResult`) so agents, tools and human
  queues execute through the same plan runner.
- Open Capability Manifest: `spec/ocm/` (JSON Schema + README), `opensmartroute.ocm`
  (`validate_capability`, `target_from_capability`, `capability_from_target`, `load_capabilities`,
  `bind_endpoint`), `load_targets()` accepts manifests and mixed catalogues, `osr validate` and
  `osr export-ocm`.
- Routing Audit and savings: `eval.routing_audit()` / `AuditReport` / `load_audit_log()` replay
  logged traffic against a catalogue (savings vs baseline, policy violations, route mix);
  `osr audit logs.jsonl --baseline ... --markdown|--json [--min-savings]`;
  `enterprise.SavingsLedger` telemetry sink (`RouterBuilder.with_savings()`, `EnterpriseRouter.savings`)
  with per-request baseline-vs-routed entries and `SavingsReport.to_markdown()`.
- CLI: `osr route --constraint KEY=VALUE` (hard constraints) and `--kinds a,b`; `--event`, `--session`,
  `--exclude`.
- Platform: `GET /api/v1/savings`, `GET /api/v1/signals`, `GET /api/v1/admin/pql` (product-qualified
  leads); web `/roi` calculator, `/compare/{openrouter,litellm,portkey,diy}` pages and the dashboard
  savings view.
- Docs: sales enablement kit under `docs/sales/` (discovery guide, demo script, Routing Audit,
  pilot charter, ROI calculator, security pack, battlecards, pricing and proposal, case study and QBR).

Research track (every previously `Research` roadmap row now has code, tests and a module reference)
- `learning.ContrastiveRouter` and `ContrastiveStrategy`: RouterDC-style contrastive training on
  (query, target) pairs. Softmax cross-entropy over `q . e_t / tau` against a row's *acceptable set*
  (`acceptable_set(row, slack)`, every target within `slack` of the best score), so several right
  answers are not penalised; `fit(objective="distilled")` trains against Zooter-style reward-distilled
  `soft_labels(scores, temperature)` instead of one-hot labels; hashed encoders, pure-Python SGD,
  online updates from outcomes, `state()/load()`.
- `learning.PolicyGradientStrategy`, `decision_reward()`, `decision_regret()` and `RegretReport`:
  end-to-end REINFORCE routing (Router-R1 / RLCascadeRouter) on hashed request features with an EWMA
  baseline and entropy bonus, trained on the *decision* utility rather than a quality prediction;
  `decision_regret(rows, targets, choose, predict)` reports decision regret next to prediction MAE so
  the two losses can be compared on the same dataset.
- `eval.headroom`: `routing_headroom()` splits oracle minus best-single into the label-noise floor and
  measurable headroom; `target_diversity()` (pairwise disagreement, winner entropy / share);
  `min_catalogue(rows, targets, fraction)` (smallest subset keeping a fraction of the oracle);
  `scaling_curve()` (oracle / best-single / headroom against catalogue size);
  `learnability_by_difficulty()` (per-difficulty-bucket headroom and noise floor, the "least learnable
  where most valuable" check for agentic pools).
- `strategies.MemoryRouter`, `MemoryTier`, `MemoryItem`, `ImportanceGate`, `RecallResult`: BudgetMem-style
  memory-tier routing. Tiers carry capacity, write / read cost per token and latency; a hashed logistic
  gate decides what is worth writing and learns from `feedback(used_ids, recalled)`; value decays per
  `tick()`, lowest-value residents are demoted when a tier is full; `recall(query, budget_tokens)`
  fills a token budget by relevance x value.
- `strategies.ModalityStrategy`, `ModalityEscalation`, `EscalationResult`, `request_modalities()`:
  penalise text-only targets when a request carries images / screens / audio / video / files that a
  text surrogate (OCR, caption, transcript) only describes; text-first escalation keeps a Beta
  posterior on "the surrogate sufficed" per modality set and calls the multimodal target first when
  that probability is low.
- `strategies.SemanticCache`, `SemanticCacheStrategy`, `CacheEntry`, `CacheHit`: a semantic cache as a
  `DESTINATION` target. `SemanticCache.target()` serves near-duplicate answers (cosine over the router's
  embedder, TTL, LRU bound, source-based invalidation); the strategy scores only cache targets from the
  hit similarity and learns per-similarity-band hit quality from outcomes.
- `strategies.AnnotatorPool`, `AnnotatorSkill`, `QuorumPlan`, `quorum_accuracy()`,
  `HumanRoutingStrategy`: QUORUM-style routing among `HUMAN` targets. Per-domain annotator accuracy is
  estimated by Dawid-Skene EM over labels (with gold labels and direct quality reports as evidence,
  prior seeded from `quality_prior`); `select_quorum(domain, target_accuracy, budget)` picks the
  cheapest majority-vote quorum that reaches the target accuracy.
- `strategies.SpeculativeCascade`, `SpeculativeResult`, `expected_mode_costs()`: speculative
  draft / strong cascades. A Beta acceptance posterior per complexity bucket decides between
  `draft_only`, `speculate` (both in parallel, strong cancelled when the draft is accepted) and
  `strong_only` by expected cost + latency under the request's `Objective`.
- `learning.HistoryTargetStrategy`, `HistoryTargetModel` and `history_vector()`: multi-turn routing
  on history-target joint embeddings (MTRouter). A logistic model over `h * e_t` (recency-weighted
  hashed history and current text, catalogue target embedding) with shared weights and a per-target
  bias learns that the same follow-up routes differently depending on the conversation so far;
  inductive for unseen targets; `context["last_target"]` earns an incumbent bonus that becomes a
  penalty when `context["last_failed"]` is set; `state()/load()/merge()`, persisted by `AutoLearner`.
- `errors.deprecated(name, since=, removal=, replacement=)` and `errors.OpenSmartRouteDeprecationWarning`
  implement the public API deprecation policy documented in CONTRIBUTING.md.
- `signals.VerbalisedDifficultySignal` and `parse_difficulty()`: a small model's verbalised
  difficulty (`0.7`, `7/10`, "hard") from `context["difficulty"]` or a callable blends into
  `complexity` and raises `reasoning_need`. `signals.DraftResponseSignal` and `draft_features()`
  derive hedging, self-correction, query overlap and length ratio from a cheap draft answer.
- `signals.semantic_entropy()`, `semantic_clusters()`, `response_uncertainty()` (meaning clusters over
  sampled answers, hedging, optional P(True) judge), `signals.UncertaintyGate` (a `Cascade` quality
  gate / `MixtureOfAgents` trigger built on them) and `signals.EventTrigger` / `TriggerRule`
  (threshold rules that fire named actions).
- `math.DirichletProbe` (evidential Dirichlet head with digamma loss and KL regulariser, pure Python
  SGD, `predict()` returns probabilities and epistemic uncertainty `K/S`) and
  `strategies.HiddenStateStrategy(state_fn, targets, dim)` that routes on host hidden states and
  lowers its confidence on unfamiliar states.
- `math.EnergyModel` (per-target ridge fit of `Wh = e0 + e_in * prompt + e_out * output`, gCO2 via a
  grid factor) and `math.HardwareProfile` / `hardware_profile()` priors for A100, H100, L4, RTX 4090,
  16-core CPU and an edge NPU.
- `strategies.expand_elastic(parent, [BudgetVariant(...)])` creates `<id>@<budget>` siblings of an
  elastic model; `strategies.TokenBudgetStrategy` scores budget fit (truncation risk vs unused
  capacity).
- `strategies.EdgeCloudStrategy`: Thompson posterior of edge success per complexity bucket, decode
  time against the latency SLO, upload penalty for the cloud tier and a hard exclusion when the
  request's data boundary forbids a public target.
- `strategies.AuctionStrategy`, `AuctionResult`, `default_bid()`: error-aware reverse auction where
  each bidder's claimed success is corrected by its observed bias; second-price payment.
- `strategies.ProtocolPolicy`, `ProtocolRule`, `DEFAULT_RULES`, `failure_risk()`: risk / budget /
  task-type rules choose `single`, `cascade`, `aggregate`, `debate` or `handoff`; a per-protocol
  ledger reports which protocol paid.
- `strategies.SelfEscalation` and `wrap_stream()`: Bayesian competence posterior updated per streamed
  chunk with an optimal-stopping rule; `SelfEscalation.for_target()` seeds the prior from
  `quality_prior`.
- `strategies.MixtureOfAgents`, `majority_vote()`, `AggregateResult`: call the top-k alternatives when
  the decision is unsure and the budget allows, aggregate by semantic majority (or a supplied
  synthesiser) and emit one `Outcome` per participant.
- `learning.UserAdaptiveStrategy` (per-user Beta posteriors shrunk toward neighbour users and the
  global posterior; `observe_user()` warm start), `learning.SkillAffinity` (profile-bucket skill
  relevance for `select_skill_set(relevance=)`), `profile_vector()`, `profile_bucket()`.
- `learning.MixtureCureModel` (Weibull mixture-cure on cumulative trajectory risk with censoring, grid
  MLE) and `learning.HandoffPolicy` (permanent hand-off on eventual-failure probability or horizon
  hazard; releases the task's `TaskPins` pin).
- `opensmartroute.discovery`: `schema_match()` / `extract_entities()` / `SchemaAwareStrategy`
  (coverage of required `input_schema` properties by typed entities in the request),
  `CachePreservingSelector` (per-session prefix-stable tool ordering with lazy eviction and
  `prefix_hit_ratio()`), `SkillGraph` (`requires` / `conflicts` / `composes` edges plus learned
  co-usage; `compose()` returns a dependency order).
- `adapters.recommend_servers()`, `ServerCard`, `ServerRecommendation`: rank MCP servers for a task
  with BM25 + hashing-cosine fusion over server cards and hard constraint filters.
- `adapters.load_semantic_router_config()` / `SemanticRouterImport`: import a vLLM semantic-router
  `config.yaml` (categories, model scores, reasoning flags, pricing, system prompts) as targets and
  rules.
- `load_skills` exposes `osr-requires`, `osr-conflicts` and `osr-composes` frontmatter as
  `metadata["requires" | "conflicts" | "composes"]` for `SkillGraph`.
- `tests/test_roadmap_research.py` covers every item above; `tests/public_api.json` lists the new
  names.

Decorator SDK
- `opensmartroute.sdk`: `ComponentRegistry` plus the decorators `@strategy`, `@signal`,
  `@policy_rule`, `@middleware`, `@telemetry` and `@target` / `@tool` / `@skill` / `@agent`. Plain
  functions are adapted through `FunctionStrategy`, `FunctionSignal` and `FunctionMiddleware`;
  classes are registered as factories. `registry.router()` / `registry.builder()` assemble a
  `Router` / `RouterBuilder` from everything declared; `include("pkg.mod[:hook]")` and
  `discover()` (entry-point group `opensmartroute.plugins`) load plugins. The process-wide registry
  is exported as `opensmartroute.components` and the top-level decorators bind to it.
- `RouterBuilder.with_components(registry, targets=, strategies=, signals=, policy=, middleware=,
  telemetry=)` wires a registry into an existing builder.
- `strategies.default_strategies(seed=, settings=, state_dir=)` builds the default ensemble.

Settings
- `opensmartroute.settings`: immutable `Settings` grouped into `RoutingSettings`,
  `PolicySettings`, `RulesSettings`, `CapabilitySettings`, `BanditSettings` and `WeightSettings`;
  `configure()`, `get_settings()`, `Settings.from_env()` (`OSR_<GROUP>_<FIELD>` overlay with typed
  parsing and `ConfigurationError` on bad values) and `Settings.env_keys()`. `Router`,
  `RouterBuilder`, `Policy`, `RulesStrategy`, `CapabilityStrategy` and `BanditStrategy` accept
  `settings=`; every previously hardcoded threshold, weight and scale now resolves from it.
- `Router.weight_of(strategy)` reports the effective ensemble weight.
- `osr settings [--json]` prints the effective tunables, their `OSR_*` keys and which are overridden.

Agent skills
- `.claude/skills/`: eight Agent-Skills packages (`osr-routing-catalogue`, `osr-decorator-sdk`,
  `osr-enterprise-builder`, `osr-evaluation`, `osr-security-hardening`, `osr-integrations`,
  `osr-deploy-serve`, `osr-contributing`) that teach coding agents such as Claude Code how to use and
  extend the library. They are valid `TargetKind.SKILL` targets: `tests/test_skills.py` loads them
  with `load_skills`, checks the routing hints against the signal ontology and asserts the router
  fills the plan's skill slot with the right package for a representative request.
- `osr skills [ROOT] [--json]` validates and lists SKILL.md packages (default root
  `branding.SKILLS_DIR` = `.claude/skills`); `osr --skills ROOT ...` adds them to any routing command.

Documentation coverage
- `docs/REFERENCE.md`: generated API reference listing every module under `src/opensmartroute` and
  every exported name with its kind, signature and one-line summary; re-exports link to their
  definition. Produced by `scripts/api_reference.py` (`write` | `check` | `report`), stdlib only.
- Every exported class, function, constant and type alias now carries a one-line summary (docstring,
  or a same-line comment for constants). `tests/test_docs.py` fails on an undocumented name, a stale
  reference, a module missing from the reference, or a `docs/*.md` file not linked from the README.

Policy composition
- `Policy` is an ordered chain of `PolicyRule` callables (`enabled`, `allow_list`, `deny_list`,
  `allowed_kinds`, `region`, `DataBoundaryRule`, `pii`, `tenant`, `cost_budget`, `latency_slo`,
  `input_tokens`, `context_window`, `tools`, `modalities`, `LanguageRule`, `JailbreakRule`).
  `Policy.with_rules(*rules)` appends rules (subclasses that override `check` are folded in as a
  single rule); `Policy.rule_names` and `default_rules()` expose the chain.

Branding
- `opensmartroute.branding`: single source of truth for brand-derived names (`BRAND`, `PACKAGE`,
  `CLI`, `ENV_PREFIX`, `ERROR_CODE_PREFIX`, `METADATA_PREFIX`, `STATE_DIR`, `ENTRY_POINT_GROUP`)
  with `env_key()`, `error_code()`, `metadata_key()`, `logger()`, `user_agent()` and `version()`.
  Error codes, `osr-*` frontmatter keys, the HTTP `User-Agent`, MCP `clientInfo`, the OpenAI proxy
  alias and the FastAPI title/version derive from it.

Learning
- Judge-score calibration: `LLMJudgeStrategy(calibrate=True, min_fit=20, calibrator=)` keeps an
  `IsotonicCalibrator` per judge, pairs each raw judge score with the served target's observed
  outcome and re-fits on every `update()`; `Router.learn()` forwards outcomes to the judge even
  when it is only used for escalation. Raw scores stay visible in the rationale (`raw=`); state
  round-trips through `state()` / `load()`.
- `learning.warm_start_from_matrix(rows, strategies, weight=)`: offline full-information reward
  matrix `(prompt, {target: reward})` replayed into every learner (LinUCB ridge per arm, IRT,
  Thompson posteriors, task table, Markov rewards) before going online.
- `MarkovStrategy(state_from="auto" | "task_type" | "domain", decay=)`: conversation state is the
  learned task type joined with the domain (not only the domain); per-request state memory so an
  outcome credits the state that was scored. `MarkovChain(decay=)` forgets transition counts,
  `RoutingMDP(decay=)` keeps exponentially weighted rewards.
- Federated merge on every learner: `MarkovChain.merge`, `RoutingMDP.merge`, `MarkovStrategy.merge`,
  `BanditStrategy.merge`, `TaskTableStrategy.merge`; `learning.merge_learners(local, remote)` pairs
  same-named strategies across replicas. `AutoLearner.refresh()` implements the writer / reader
  pattern for replicas sharing one `StateStore`.
- Cascade steps feed every learner: `CascadeStep.cost_usd`, `CascadeResult.total_cost_usd`,
  `CascadeResult.outcomes(request)` (one per-step `Outcome`, rejected steps are failures) and
  `Router.learn_cascade(request, result)`. Learners keep their per-request memory while
  `Outcome.step` is set so multi-step trajectories credit every step.
- Strategies condition on the plan role: `Router._build_plan` tags sub-route signals with
  `signals.extra["role"]`; `BanditStrategy` contexts, `TaskTableStrategy` rows (`skill:<task>` plus
  `family:skill:<fam>`), `MarkovStrategy` states and every learner's request memory are keyed by
  role (`strategies.base.role_of` / `memory_key`), so a target's record as a skill or persona never
  leaks into its record as the primary answerer. `TaskTableStrategy` now learns from role outcomes.

Economics
- Energy and carbon as cost dimensions: `RouteTarget.unit_energy` (`cost["wh_per_1k_tokens"]`),
  `RouteTarget.unit_carbon` (`cost["gco2_per_1k_tokens"]`, or energy x `cost["gco2_per_wh"]`) and
  `Objective(energy=, carbon=)` weights applied to the log-normalised values in the utility.

Resilience
- `AutoLearner.load()` quarantines corrupt or incompatible learner state instead of failing: the file
  is renamed `*.corrupt-<ts>` (or the store key copied under `quarantine/`), the learner starts blank
  and `AutoLearner.quarantined` lists `(strategy, reason)`. A failing `StateStore.get` is recorded,
  not raised.
- `FileAuditSink` resumes its hash chain from the last line on disk across restarts and gains
  `FileAuditSink.verify(path) -> (ok, n, first_problem)` detecting edited, removed or unparseable lines.
- `LLMJudgeStrategy` filters judge output item by item: non-dict items, unparseable / NaN scores and
  hallucinated or injected target ids are skipped without discarding the rest of the ranking;
  non-string judge returns are a failure mode with zero confidence.
- `scripts/bench.py --scale` sweeps 16 / 64 / 256 / 1024 targets with and without retrieval narrowing
  and reports p50 / p95 / p99; `docs/ARCHITECTURE.md` documents the measured envelope.
- `tests/test_resilience.py`: audit-chain verification, judge JSON failure modes, state-corruption
  recovery (files and stores), multi-replica hand-off.

Deployment
- `deploy/Dockerfile` (non-root, multi-stage, `OSR_*` environment to `osr serve`), `deploy/entrypoint.sh`
  and the Helm chart `deploy/helm/opensmartroute` (ConfigMap-driven catalogue, hardened pod security
  context, HPA, PDB, NetworkPolicy, optional PVC with a multi-replica warning).

Roadmap exit criteria
- `eval.criteria`: every offline-measurable exit criterion in `docs/ROADMAP.md` as a simulation that
  drives the real `Router` / learners and returns a `CriterionResult` - `cold_start_ratio()` (v0.5),
  `multi_round_vs_best_single()` (v0.6), `match_at_1_at_scale()` with `synthetic_tool_catalogue()`
  (v0.7), `conformal_coverage()`, `knapsack_never_exceeds_cap()` and `ope_within_live_ci()` (v0.8),
  plus `bootstrap_ci()` and `run_all()`. `scripts/exit_criteria.py [--full] [--json]` runs them and
  exits non-zero when one fails.
- `osr eval --multi-round [--threshold] [--max-rounds]` compares `MultiRoundExecutor` against the
  best single target on a dataset whose rows carry per-target `scores`.
- Cold-start exploration: `Router(explore_rate=, explore_min_samples=, explore_seed=)` and
  `RoutingSettings.explore_rate` / `explore_min_samples` occasionally serve the least-seen viable
  candidate until it has enough outcomes; propensities include the exploration mass and
  `signals.extra["explored"]` marks the request.
- `MultiKnapsackBandit(on_capped="release" | "abstain")` and `capped(scores)`: when every arm would
  breach a hard cap the bandit can abstain instead of releasing the cheapest arm.
- `MultiRoundExecutor(failures_before_switch=)` forwards the switch policy to `ProgressRouter`.
- Thread safety: `Router.lock` (re-entrant) guards strategy scoring, every learner update, task
  pins, request memory, retrieval narrowing and the exploration RNG; the LLM judge is scored outside
  it. `AutoLearner(lock=)` shares that lock (`EnterpriseRouter` / `RouterBuilder` pass it), snapshots
  are deep-copied and sequence-numbered so a slow `save()` never overwrites a newer one, and a
  separate I/O lock orders writers against `load()` / `refresh()`. `SemanticCache`, `MemoryRouter`,
  `AnnotatorPool` and `ModalityEscalation` lock their own state.
- `SpeculativeCascade(concurrency=, draft_timeout_s=, history_size=)`: bounded worker pool sized for
  concurrent speculative runs, draft timeout, draft exceptions escalate to the strong target
  (`details["draft_error"]` / `["draft_timeout"]`), late strong failures are swallowed after an accepted
  draft, `draft_failures` / `strong_failures` counters and `stats()`.
- `ModalityEscalation.run` escalates when the text target raises (`details["reason"]`) instead of failing.
- `MultiRoundExecutor`: rounds whose handler raised become `Round(error=...)` and the loop continues
  on another target; one judged learn per round (no provisional + correction double count);
  `deadline_ms`, `return_best`, `credit_task`, `RoundResult.stop_reason` / `best` / `errors` /
  `total_cost_usd`; `arun()` for coroutine handlers and an `async` judge. `ProgressRouter.run_step`
  (`learn=`) routes via `router.route`, records a failed step before re-raising, estimates step cost
  from the target's unit cost when the handler reports none; `arun_step` twin.
- `tests/test_e2e_scenarios.py`: real-scenario suite with no stubs - the shipped `examples/`
  catalogue and rules driven end to end through an in-process OpenAI-compatible provider fleet over
  HTTP (PII boundary, tenant deny lists and data boundaries, persona plans, metrics, hash-chained
  audit, learner state surviving a restart), a provider outage opening and recovering a circuit
  breaker, the FastAPI service over an `EnterpriseRouter` (OpenAI proxy, feedback, stats, error
  mapping), the `osr` CLI (`route`, `eval --frontier`, `--min-accuracy` gate, `targets`, `stats`),
  `ProgressRouter` / `MultiRoundExecutor` agentic loops, a real MCP server subprocess over stdio, and
  24 concurrent requests through the full middleware stack.
- `EnterpriseRouter.credit_task()`: delivers the delayed task reward through the router and persists
  the re-credited learner state; `MultiRoundExecutor(EnterpriseRouter)` now closes the task-credit loop.
- Execution: a primary target without a handler (an instructions-only skill or persona) runs on the
  plan's `llm` slot - its instructions are disclosed in the system prompt, the model answers, and the
  model is credited with a `role="llm"` outcome - instead of failing with "has no handler".
- Execution enforces the PII boundary set up by `GuardMiddleware(redact=True)`: targets whose
  constraints allow PII receive the original text (`context["pii_restored"]`), every other target
  keeps the placeholders (`context["pii_redacted"]`), and the mapping never reaches a handler.
- `create_app()` accepts an `EnterpriseRouter` (middleware, health, telemetry and audit apply to every
  HTTP request; `/stats` includes the health snapshot) and maps `SecurityError` / `ValidationError`
  to 400, `NoRouteError` to 422, `TargetUnavailableError` to 503 and `ExecutionError` to 502.
- `DomainActionSignal` detects arithmetic and unit conversions (`17% of 2,450`, `12 * 4`,
  `5 miles to km`, percent / calculate / square root ...) as the `math` domain, so calculator-style
  tools and `max_complexity` math rules fire; lexicon keywords made of symbols (`%`) now match.
- `examples/targets.yaml`: the public cloud models and the research agent carry
  `constraints.pii_allowed: false`, making PII a hard policy stop instead of only a rule preference.

Hosted platform (`platform/`, separate from the zero-dependency SDK): `platform/api` (package
`osr-platform-api`, FastAPI) and `platform/web` (`osr-platform-web`, Next.js 16)
- `osr_platform.app.create_platform_app()` / `osr-platform-api`: the routing API as a service:
  `/api/v1/route` (decision, signals, ranked breakdown, policy rejections; `plan` / `execute`),
  `/feedback`, `/targets`, `/catalogue`, `/usage`, `/keys`, `/tenants`, `/stats`, `/audit`, the
  OpenAI-compatible `/v1/models` + `/v1/chat/completions` (`model: "auto"` or a pinned target), and
  `/api/v1/docs` serving the repository Markdown (README, CHANGELOG, `docs/`) rendered to HTML with a
  table of contents and Mermaid blocks as JSON. `OSR_PLATFORM_WEB_URL` / `OSR_PLATFORM_CORS_ORIGINS`
  point `/` and Stripe redirects at the web app and allow it as a browser origin.
- Web app: landing page with a live catalogue, documentation (sidebar, table of contents, Mermaid),
  pricing, a playground (decision view with ranked utility bars, signals, policy rejections, plans,
  execution; OpenAI-compatible chat with `auto` or pinned model), signup / sign-in (key shown once,
  stored in the browser only) and a dashboard (overview with usage chart, API keys, usage by target
  and endpoint, tenants editor, audit trail with chain verification, plan and billing). Next.js route
  handlers proxy `/api/*`, `/v1/*` and `/openapi.json` to `OSR_API_URL`, so the browser only talks to
  the web origin; server components render from the API at request time.
- API keys (`osr_live_...`, SHA-256 at rest, `Authorization: Bearer` or `X-API-Key`), plans
  (free / pro / enterprise with per-minute and per-day quotas, key and tenant caps, feature gates;
  `OSR_PLATFORM_PLAN_OVERRIDES`), usage metering per key / endpoint / target with tokens and cost,
  admin API (`X-Admin-Token`) and an optional Stripe checkout + signed-webhook billing hook that is
  inert until `OSR_PLATFORM_STRIPE_*` is configured. State is SQLite (WAL, with a rollback-journal
  fallback for SMB mounts).
- Editions: `OSR_PLATFORM_EDITION=community` runs the core `Router`; `enterprise` runs `RouterBuilder`
  with auto-learning persistence, health circuit breakers, `GuardMiddleware`, metrics and a
  hash-chained `FileAuditSink`, and unlocks tenants (per-tenant data boundary / region / deny lists /
  cost caps applied as hard constraints), `/stats` and `/audit` (filtered per account).
- `OSR_PLATFORM_PROVIDERS` maps targets to OpenAI-compatible providers (Azure OpenAI, OpenAI, vLLM,
  Ollama ...) through `adapters.openai_compat`; targets without a provider stay routable as decisions.
- `platform/api/Dockerfile` and `platform/web/Dockerfile` (non-root, health checks; the API keeps a
  `/data` volume), `infra/` Bicep (Container Apps `osr-api` + `osr-web`, Azure Files, ACR, Log
  Analytics, Azure OpenAI with gpt-4.1 family deployments), `azure.yaml` for `azd up`, and the
  `Platform` workflow (API tests, web lint/typecheck/build, two-container smoke, OIDC `azd` deploy).
  Reference deployment: `rg-osr-prod` in Sweden Central.
- Public catalogue and rankings, measured on the deployment rather than curated: `GET /api/v1/models`
  (model cards with capabilities, constraints, pricing per 1k / 1M tokens, executable flag, requests,
  tokens, cost, success rate, latency, domains and week-over-week trend), `GET /api/v1/models/{id}`
  (daily series, health, effort siblings), `GET /api/v1/rankings?days=&domain=` (share of tokens and
  requests, trend, per-domain leaders) and `GET /api/v1/stats/public` (headline figures). Usage rows
  now store `request_id`, `model`, `domain` and `complexity` (additive SQLite migration) so every
  figure is reproducible from the same table that meters billing.
- `GET /api/v1/activity`: newest-first per-account request log with `endpoint` / `target` / `key`
  filters and `before=` cursor paging.
- `POST /v1/chat/completions` extensions: `models` (candidate list; the router scores only those and
  falls back down the ranking when an upstream call fails, up to three times), `stream: true`
  (`text/event-stream` chunks, `[DONE]` terminator) and an `osr` object (`objective`, `constraints`,
  `tenant`, `plan`, `fallbacks`). The `opensmartroute` metadata gained `plan`, `fallback_from`,
  `cost_usd` and `latency_ms`; responses carry `X-Request-Id`, `X-OSR-Target`, `X-OSR-Confidence`.
- Web: `/models` explorer (search, domain, executable, kind and sort), `/models/{id}` pages with traffic
  chart, routed-domain bars, capabilities, constraints, examples and copyable call snippets for the
  live base URL, `/rankings` leaderboard (window, metric and domain filters), dashboard `/activity`
  with expandable request rows and feedback snippet, playground compare mode (up to four targets in
  parallel with cheapest / fastest badges and fallback markers; `?model=` deep link from model pages),
  and a landing page fed by the public stats (hero figures, most-routed targets, domain leaders,
  candidates + streaming sample, three-step getting started). Sitemap lists models and rankings.
- Platform single sign-on: `GET /api/v1/auth/providers`, `GET /auth/{provider}/start?next=` and
  `POST /auth/{provider}/callback` (PKCE, HMAC-signed state, GitHub / Google / Microsoft / GitLab presets
  and custom OIDC issuers via `OSR_PLATFORM_SSO_*`), `osr_sess_` browser sessions with 30-day TTL
  (`GET /auth/sessions`, `POST /auth/logout?everywhere=`, `DELETE /auth/sessions/{id}`), linked
  identities (`GET /auth/identities`, `DELETE /auth/identities/{provider}`; refuses to remove the only way
  in), `/api/v1/me` reports `via` and `identities`, `/api/v1/info` lists `sso` providers. New SQLite
  tables `identities` and `sessions` (additive migration).
- Web: "Continue with GitHub / Google / ..." on login and signup, `/auth/callback` exchange page (shows the
  first API key once when the account is created), `/dashboard/account` (profile, linked identities,
  browser sessions with revoke and sign out everywhere). Shared UI toolkit under `components/ui`
  (Radix-based button variants, dialog, dropdown menu, tooltip, select, switch, progress, avatar, table,
  sonner toasts) and a Recharts chart kit under `components/charts` (`TimeBars`, `TimeArea`,
  `RankedBars`, `Donut`, `Sparkline`) with one palette and shared tooltip/legend/empty states.
  Redesigned header (compact nav, Ctrl/Cmd+K catalogue search, account menu), light hero with a prompt
  box that opens the playground pre-filled (`/playground?q=`), rankings charts (share of requests,
  traffic by domain), dashboard usage stacked bars and quota progress, and a table view on `/models`.
- Platform users, workspaces and roles (B2C and B2B): new SQLite tables `users`, `memberships`, `invites`
  and `sso_connections`, `accounts.kind` (`personal` / `organization`) and `accounts.slug`; existing
  databases are migrated on start (every account becomes a personal workspace owned by its user).
  `POST /signup` and first SSO sign-in create the user plus a personal workspace; `GET/POST /workspaces`,
  `POST /workspaces/{id}/switch`, `PATCH/DELETE /workspace`, `GET /workspace/members`,
  `PATCH/DELETE /workspace/members/{user_id}` (owner > admin > member, last owner protected),
  `POST/DELETE /workspace/invites` (link shown once, 14-day TTL, seats = `plan.max_members`: free 3, pro 10,
  enterprise 500), `GET /auth/invites/{token}` and `POST /auth/invites/{token}/accept`. Key and tenant
  mutations need admin; API keys act as owner of their workspace. `/me` adds `user`, `role`, `workspaces`;
  usage and activity rows carry `user_id`. Organization SSO (enterprise plan feature `sso`):
  `GET/PUT/DELETE /workspace/sso` stores the company IdP (Google, Microsoft, GitLab, GitHub or any OIDC
  issuer, allowed email domains, default role), served as provider `org-<slug>` with just-in-time
  membership; `GET /auth/discover?email=` returns the sign-in options for an email domain.
- Web: workspace switcher in the dashboard sidebar, `/dashboard/members` (invite, roles, remove, pending
  invitations), `/dashboard/workspace` (create organization with its first key, rename/slug, organization
  SSO connection, leave), `/invite/[token]` acceptance page, and the sign-in page discovers company SSO
  from the typed email ("Continue with <Company SSO>").
- Custom domains: Bicep parameters `webAliasDomains`, `apiCustomDomain`, `customDomainCertificates`
  (managed certificates, TXT validation for the apex), outputs for the static IP and domain verification
  id; `scripts/domains.ps1` configures Cloudflare DNS, redirects (`.com` to `.ai`, `www` to apex) and
  SSL settings and binds the domains in two provision phases (see `platform/README.md`).

### Changed
- Web: the UI toolkit under `platform/web/src/components/ui` is now the shadcn/ui `new-york` kit
  (`components.json`, `radix-ui` umbrella package, `class-variance-authority`, `cmdk`, `vaul`, `tw-animate-css`)
  with the brand tokens mapped onto the kit's CSS variables in `globals.css`. Header (navigation menu with
  grouped product/resources panels, mobile sheet, command search), footer, dashboard shell (sidebar), docs
  sidebar (collapsible sections, mobile sheet) and version menu (dropdown) are rebuilt on it. Hand-rolled
  tables, filter chips, selects, switches, search inputs, stat cards, accordions and sliders across the
  marketing pages, models explorer/catalogue, marketplace, rankings, playground, estimator, ROI
  calculator and dashboard pages use the kit primitives (`DataTable`, `StatCard`, `FilterChip`,
  `SearchInput`, `Select` wrapper, `Switch`, `Progress`, `Accordion`, `Slider`). The per-scope
  `@radix-ui/react-*` packages, `cn` and `next-themes` dependencies are removed.
- `Router(slot_quality_floor=, narrow_above=, narrow_to=)` and
  `RouterBuilder.with_retrieval(narrow_above=, narrow_to=)` default to `None` (= settings) instead
  of literals; `Router.calibrator` defaults to `TemperatureScaler(settings.routing.softmax_temperature)`
  and plan-slot margins use it instead of a private softmax.
- `RouterBuilder.with_auto_learning()`, `with_health()` and `with_queue_awareness()` no longer
  inject hardcoded weights; the defaults come from `WeightSettings`.
- `MultiKnapsackBandit`: the sliding-window spend meter is O(1) per step (running sums over a
  bounded deque); shadow prices are updated in budget units (`avg / B - 1`) so a budget expressed in
  tokens no longer blows the penalty up; the "release the cheapest arm" fallback only triggers when
  *every* arm is blocked, not when utilities happen to be negative.
- Documentation: the README is a short overview (pitch, install, quick start, what it routes, how it
  decides, production, security, documentation index, roadmap); the detailed walkthroughs (targets,
  execution, real providers, learning, decorator SDK, enterprise builder, research-track modules,
  CLI, routing latency, repository layout) moved to `docs/GUIDE.md`. `docs/ROADMAP.md` marks every
  research-track row as implemented with its module; `docs/RESEARCH.md` and `docs/MATH.md` describe
  the corresponding techniques and formulas (sections 10 to 16).

### Fixed
- `PageHinkley` accumulated `x - mean - delta`, so a perfectly stationary quality stream alarmed
  after `threshold / delta` samples and `AutoLearner` reset healthy targets; the tolerance now has the
  textbook sign (`+ delta`) and a constant stream never drifts.
- `Bandit.mean` is abstract and `LinUCB.mean()` returns the intercept estimate instead of a stub.
- `OpenAICompatClient` derives its `User-Agent` from the package version instead of a literal.
- `enterprise.ops.ABTest.observe` no longer divides by zero when the control arm has no samples yet.
- `server.py`: request bodies were parsed as query parameters (HTTP 422) because
  `from __future__ import annotations` stringified the locally defined pydantic models; removed.
- `execute()` re-raised an `ExecutionError` from a provider handler (upstream 4xx, malformed body)
  without recording a failed outcome, so learners and health never saw those failures.
- `EnterpriseRouter.learn()` with auto-learning bypassed the router's feedback store, task-credit
  buffer and pins (only the strategies were updated); `RouterBuilder.with_feedback()` no longer
  double-records through a learner sink.
- `CacheMiddleware` keyed only on text / constraints / objective, so a pinned (`candidates=`) or
  `plan=True` call could be served another call's cached decision; per-call routing options are now
  part of the key.

## [0.4.0] - 2026-09-04

### Added

Execution and plans
- `Router.run()`, `AsyncRouter.run()` and `EnterpriseRouter.run()` route and execute the resulting
  plan through the new `opensmartroute.execution` module: the `skill` slot's handler runs as a
  pre-processor (may return a rewritten `RouteRequest`, a `str` attached as `context["skill_output"]`,
  or `None`), persona / skill / primary `instructions` are composed into `context["system"]`, the
  primary handler (LLM, skill or agent harness) is called, and one `Outcome` per participant is
  recorded and learned from. Returns `ExecutionResult(response, text, target_id, steps,
  system_prompt, effective_request, outcomes)`.
- `RouteTarget.instructions` (persona prompt, skill body or agent standing instructions, disclosed
  only when the target is part of the plan), `RouteTarget.effort` (reasoning-effort variant) and
  `RouteTarget.family` (siblings share breaker and budget state).
- `Outcome.task_id`, `Outcome.role` and `Outcome.step` so delayed, task-level rewards can be joined
  to every call in a trajectory and plan slots learn separately from the primary.
- `RouteRequest.profile` (user / tenant attributes) and `RouteRequest.history` (session turns) feed
  `ProfileSignal` and `HistorySignal`.
- `Router(slot_quality_floor=0.5)`: a persona / skill slot is only filled when its best candidate's
  quality estimate clears the floor.

Strategies and learning
- `TaskTableStrategy` (static task-type table with Wilson intervals; `fit_task_table()` from outcomes),
  `DeferStrategy` (learning-to-defer for `TargetKind.HUMAN`), `EffortStrategy` and `decide_effort()`
  (think vs. no-think), `ProgressRouter` and `MultiRoundExecutor` (progress-guided step routing and a
  Router-R1 style route / execute / judge loop), `CascadePlanner` with `BeliefTracker`
  (cascade as a finite-horizon MDP or POMDP with a stop action).
- `learning.TaskCredit` (uniform / discounted / last / blended credit assignment from a terminal
  task reward), `learning.TaskPins` (admission-time pinning), `ExampleMiner`, `RequestMemory`,
  `SimilarityFallback`, `target_embedding()`, `nearest_targets()` and `warm_start()` for cold-start
  targets.
- `math.calibration`: `TemperatureScaler`, `IsotonicCalibrator`, `ConformalCalibrator` (split
  conformal candidate sets with a coverage guarantee). `Router(calibrator=, conformal=)` exposes
  calibrated confidence and `RouteDecision.candidate_set`.
- Forgetting (`decay < 1`) in `IRTModel` and `BradleyTerry`; `BradleyTerry.merge()` for federated
  sufficient statistics; Sherman-Morrison rank-1 updates and surfaced posterior variance in `LinUCB`.

Signals
- `signals.ontology.TaskOntology` (families, types, subtypes with seed templates) and
  `signals.models` (`HashedFeaturizer`, `HashedClassifier`, `HashedRegressor`, `synthesize_dataset()`)
  behind `TaskTypeSignal`, `LearnedDifficultySignal`, `ReasoningNeedSignal`, `OutputLengthSignal`,
  `SensitivitySignal`; weights ship as JSON (`SignalModelBundle`) and load with
  `Router(extractors=learned_extractors(SignalModelBundle.from_file(path)))` or `osr --models path`.
- `osr train --out models.json [--from rows.jsonl] [--synth] [--gadget detector.json]`.

Catalogue interop and scale
- `opensmartroute.retrieval`: `BM25Index`, `DenseIndex`, `Retriever`, `rrf()` (reciprocal-rank fusion),
  `select_skill_set()` (submodular skill-set selection under a token budget), `tool_search_target()` and
  `execute_tool_target()` meta-tools. `Router(retriever=, narrow_above=500, narrow_to=50)` switches to
  retrieve-then-rank for large catalogues.
- `adapters.mcp`: `tools_from_mcp()`, `connect_mcp()` (stdio JSON-RPC), `enrich_description()`,
  `sign_manifest()` / `verify_manifest()` (HMAC-SHA256 or Ed25519), `description_risk()`;
  `osr mcp-manifest sign|verify`.
- `adapters.a2a`: `fetch_agent_card()`, `agent_from_card()`, `skills_from_card()`, `a2a_handler()`.
- `adapters.frameworks`: `langgraph_node()`, `langgraph_condition()`, `maf_router_executor()`,
  `openai_tool_spec()`, `route_and_execute()`.
- `adapters.personas`: `load_personas()` / `persona_target()` from Markdown, JSON, CSV and Copilot
  `*.agent.md` / `*.chatmode.md` files.
- Agent-harness adapters: `AgentHarness`, `HarnessResult`, `CallableHarness`, `HTTPHarness`
  (408 / 429 / 5xx map to `TargetUnavailableError`), `SubprocessHarness`, `harness_handler()`.
- SKILL.md loader (`load_skill`, `load_skills`, `skill_from_markdown`) for
  [agentskills.io](https://agentskills.io/specification) folders; works with or without PyYAML.
- OpenAI-compatible proxy in the HTTP server: `POST /v1/chat/completions` (`model: "auto"` routes,
  executes and learns) and `GET /v1/models`; `GET /healthz`.

Evaluation
- `eval.baselines`: random, cheapest, best-prior, most-expensive, static task table, oracle,
  `multi_sample_oracle()` and `noise_floor()`; `osr eval --baselines`.
- `eval.robustness`: `repeat_flip_rate()`, `paraphrase_robustness()`, `profile_swap_fairness()`,
  `coreset()`, `diversity()`; `osr eval --robustness`.
- `eval.frontier`: `frontier3()`, `hypervolume()`, `ablation_report()`; `osr eval --frontier3 --ablation`.
- `eval.ope`: IPS, SNIPS and doubly-robust off-policy estimates with weight clipping and effective
  sample size; `osr ope logs.jsonl`.
- `eval.datasets`: presets for RouterBench, LLMRouterBench, RouterEval, xRouteBench and RouterXBench;
  `osr eval --preset`.
- `calibration_report()` (ECE, Brier, reliability bins); `osr eval --calibration`.
- `osr eval --min-accuracy X` exits with status 2 below the threshold; CI uses it as the routing
  accuracy gate.

Enterprise
- `enterprise.stores`: `RedisStateStore`, `SQLStateStore` (DB-API 2.0; PostgreSQL, SQLite, MySQL),
  `VersionedStateStore` (schema envelope with forward migrations), `EncryptedStateStore`
  (AES-256-GCM, key rotation; `crypto` extra), `BatchedStateStore` (write-behind), `NamespacedStateStore`.
  `RouterBuilder.with_state_store()` persists learner state through any of them.
- `enterprise.ops`: `ShadowMiddleware` and `ABTest` (shadow or A/B routing judged by Wald's SPRT),
  `FairShareMiddleware` (dominant-resource fairness across tenants), `InflightTracker` and
  `QueueAwareStrategy` (Erlang-C / Kingman wait estimates from live in-flight counts).
  `RouterBuilder.with_shadow()`, `.with_fair_share()`, `.with_queue_awareness()`,
  `.with_calibration()`, `.with_retrieval()`, `.with_router_options()`.
- `opensmartroute.settings`: frozen, environment-overridable `Settings` (`OSR_<GROUP>_<FIELD>`) for
  every tunable constant; `opensmartroute.branding` centralises package identifiers.
- `deploy/`: non-root reference `Dockerfile`, `entrypoint.sh` and a Helm chart (Deployment, Service,
  Ingress, HPA, ConfigMap). Releases publish `ghcr.io/isathish/opensmartroute:<version>`.

Security
- `security.gadget.GadgetDetector` (learned confounder-gadget classifier; `InputGuard(learned=True)`),
  `security.injection.injection_risk()` and `inspect_injection()`, `security.limits.ResourceLimiter`
  and `ResourceLimitMiddleware` (per-task caps on steps, tool calls, depth, tokens, cost, wall-clock),
  `security.provenance.OriginPolicy` (sensitive parameters of state-changing tools must originate
  from the user turn), `security.safety.run_safety_suite()` (red-team routing cases);
  `osr safety [--learned-guard]` exits with status 2 on any failure.

Project
- Public API snapshot test (`tests/public_api.json`, `tests/test_public_api.py`): adding or removing an
  exported name fails CI until the snapshot is updated deliberately.
- Release automation: `scripts/release.py` (version consistency, changelog sections, release
  preparation), `Prepare release` and `Release` workflows (release PR, tag, GitHub release with
  changelog notes, PyPI trusted publishing with PEP 740 attestations, SLSA build provenance, GHCR
  image), CodeQL for Python and workflows, Dependabot for actions and Python dependencies.
- CI matrix covers CPython 3.10 to 3.13 on Linux plus 3.12 on Windows, checks formatting, builds and
  smoke-tests the container image.
- `examples/end_to_end.py`, `examples/skills/sql-reporting/SKILL.md`; `instructions` on the example
  personas and skills.

### Changed
- `Router.execute()` / `AsyncRouter.execute()` keep returning the raw handler response but now go
  through the plan-aware executor. `EnterpriseRouter` gains `execute()` (returns `ExecutionResult`).
- `load_targets` and `load_rules` are exported from the package root.
- Project metadata: repository URLs point at `isathish/OpenSmartRoute`; classifiers declare
  CPython 3.13 and `Typing :: Typed`; the sdist no longer ships brand assets and fonts.

### Fixed
- `config.load_document` and the eval dataset loader read files as UTF-8 explicitly; on Windows the
  locale default (cp1252) failed on the shipped `examples/targets.yaml`.

## [0.3.0] - 2026-09-04

### Added
- `opensmartroute.adapters`: stdlib `OpenAICompatClient` (timeouts, exponential backoff with
  `Retry-After`, typed error mapping, key never logged) with `judge_fn`, `embedder`, `chat_handler`
  glue; lazy `sentence_transformers_embedder` and `OpenTelemetryTelemetry`.
- `opensmartroute.config`: `load_targets` / `load_rules` / `load_document` with `ConfigurationError`
  reporting file, entry and reason. `TargetRegistry.from_file` now delegates to it.
- `scripts/bench.py` â€” reproducible routing-latency benchmark; `scripts/brand_build.py` â€” logo system
  generator with WCAG 2.2 audit and platform-standard exports.
- Brand identity: rounded-geometric wordmark (Quicksand) and Poppins text, vendored under OFL; light
  and dark palettes verified â‰¥ 4.5:1 text / â‰¥ 3:1 graphics.
- `docs/ROADMAP.md` with per-release exit criteria; `docs/RESEARCH.md` rewritten as a survey with a
  formal problem statement, taxonomy, systems comparison and ideaâ†’module map.

### Fixed
- `HealthPolicy` consumed a rate-limit token for **every** admissible candidate on every route; it now
  only checks availability, and `EnterpriseRouter` reserves one token for the chosen target.
- `/feedback` endpoint dropped `complexity`, so IRT never learned from HTTP feedback.

### Changed
- `cli._load_rules` (private, but used by examples) replaced by public `config.load_rules`.
- Removed the earlier gradient ring / wordmark SVGs in favour of the generated system.

## [0.2.0] - 2026-09-04

### Added
- `opensmartroute.math`: Thompson/UCB1/LinUCB/Îµ-greedy bandits, cost-aware Lagrangian wrapper,
  IRT (2PL) model, Bradleyâ€“Terry + Elo, Markov chain + routing MDP (value iteration),
  EWMA/Welford, Pageâ€“Hinkley and ADWIN-lite drift, Wilson interval, Pareto/TOPSIS,
  Erlang-C / Little's law / Kingman queueing.
- `opensmartroute.learning`: `IRTStrategy`, `PreferenceStrategy`, `LinUCBStrategy`,
  `MarkovStrategy`, `AutoLearner` (fan-out + drift detection + atomic persistence).
- `opensmartroute.realtime`: circuit breaker, token-bucket rate limit, rolling budget,
  `HealthRegistry`, `HealthPolicy`, `HealthStrategy`.
- `opensmartroute.enterprise`: `RouterBuilder`, `EnterpriseRouter`, middleware chain
  (`CacheMiddleware`, `TenantMiddleware`, `TimeoutMiddleware`), telemetry ports
  (`LoggingTelemetry`, `MetricsTelemetry`), `StateStore` ports, hash-chained `FileAuditSink`.
- `opensmartroute.security`: `InputGuard` (confounder-gadget detection), `Redactor`,
  `GuardMiddleware`, `sanitize_for_prompt`, `load_secret`.
- `opensmartroute.aio.AsyncRouter`.
- Typed exception hierarchy in `opensmartroute.errors`; `py.typed` marker.
- Pinned rules (`pin: true`) and `primary: false` plan-only targets.
- CI (ruff, mypy, bandit, pytest, eval gate), SECURITY.md, CONTRIBUTING.md.

### Changed
- Lexicon matching is now whole-word (regex `\b`) â€” fixes false positives like `sql` in `australia`.
- Cost/latency normalisation is log-scaled; default `Objective` weights lowered.
- `NoRouteError` now derives from `OpenSmartRouteError` and carries `details["rejections"]`.

## [0.1.0] - 2026-09-04

- Initial release: core types, registry, signals, policy, rules/capability/similarity/bandit/
  llm-judge/cascade strategies, router with plans, feedback store, eval harness, CLI, FastAPI server.
  Predates the repository history: the code is the initial commit tagged `v0.2.0`.
