> ## Documentation Index
> Fetch the complete documentation index at: https://docs.findmydata.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Release notes

# Release notes

This document is for everyone tracking Find My Data releases — operators upgrading a deployment, engineers looking for what changed and why, and evaluators who need an honest statement of what is validated live versus built-but-gated. It defines the versioning policy and records each release, newest first. The current state of every capability (including what is mocked or feature-gated) is always [STATUS.md](../STATUS.md); this file records what changed per release.

## Part 1 — Versioning policy

Releases are versioned **`year.month.day.x`**:

* **year** — two digits (`26` for 2026)
* **month, day** — no leading zeros (`7`, not `07`)
* **x** — same-day release sequence, starting at `0`

| Example      | Meaning                                                                           |
| ------------ | --------------------------------------------------------------------------------- |
| `26.7.15.0`  | First release on 2026-07-15                                                       |
| `26.7.15.1`  | A second release later the same day (applies update-001)                          |
| `26.7.15.2`  | A third same-day release (vendor control-plane service)                           |
| `26.7.15.3`  | A fourth same-day release (Account portal)                                        |
| `26.7.15.4`  | A fifth same-day release (entitled downloads + supply chain)                      |
| `26.7.15.5`  | A sixth same-day release (support portal + diagnostic bundles)                    |
| `26.7.15.6`  | A seventh same-day release (production IdP / Clerk)                               |
| `26.7.15.7`  | An eighth same-day release (Stripe billing / paid conversion)                     |
| `26.7.15.8`  | A ninth same-day release (demo mode + dev-environment runbook)                    |
| `26.7.15.9`  | A tenth same-day release (per-seat Stripe pricing + live Products)                |
| `26.7.15.10` | An eleventh same-day release (live Plain support mirror)                          |
| `26.7.16.0`  | First release on 2026-07-16 (org member management + live commercial services)    |
| `26.7.16.1`  | A second same-day release (**the current release** — member-management portal UI) |
| `26.12.3.0`  | First release on 2026-12-03                                                       |

The version string lives in two places, kept in sync:

* `PRODUCT_VERSION` in `packages/server/src/controlplane/routes.ts`
* the `VERSION` file at the repository root

It is reported as `productVersion` in the control-plane fleet-health telemetry payload (inspectable at `GET /api/controlplane/telemetry-preview`; see [ADR-0014](adr/ADR-0014-roadmap-features.md), decision 5).

The `version` fields in `package.json` files (currently `0.1.0` in the root and in `packages/server`, `packages/shared`, `packages/web`) are internal workspace versions and are **not** the release version.

## Part 2 — Release 26.7.16.1 (2026-07-16)

A same-day increment over 26.7.16.0 that adds the **member-management UI** to the
Account portal, so the routes shipped in 26.7.16.0 are usable from the browser.

**What's new**

* **Members panel** in the portal org view (`account/ui.ts`): a "Team members"
  table (each row shows email + name + role, the signed-in user marked "you"), a
  "Pending invitations" table with a **Revoke** control, and an **invite-a-teammate**
  form (email + owner/member). Owner-only controls (Make owner/member, Remove,
  Revoke) render only for owners; the last owner shows a non-actionable "last owner"
  label — mirroring the server's invariant. All actions call the 26.7.16.0 routes
  with CSRF and refresh in place.

**Verification:** full server suite (333 tests) passing; typecheck + lint clean.
Verified **live in a browser** against a local dev-mode portal: register → open org
→ invite a teammate (pending invitation appears) → revoke (removed), with the last
owner correctly non-removable and zero portal console errors.

**Upgrade notes (26.7.16.0 → .1):** no migration, no config change — UI only.

## Part 2b — Release 26.7.16.0 (2026-07-16)

The first release on 2026-07-16. It adds **org member management** to the Account
portal and records the commercial services going **fully live** (the day's infra
work carries no product-code change of its own).

**What's new**

* **Org member management** (`account/store.ts` + `service.ts` + `app.ts`,
  [ADR-0024](adr/ADR-0024-org-member-management.md)): an org owner can invite
  teammates by email and manage roles, lifting the single-owner limitation from
  ADR-0017. Two roles (`owner` | `member`); the last owner can never be removed or
  demoted (an org is never orphaned). Invitations are an email allowlist
  (`org_invites`) that **auto-accepts on sign-in** — composing with Clerk JIT
  (verified email) and dev auth; an invite only ever adds to the inviter's own org.
  Every route is membership-guarded (non-member → 404), mutations are owner-only +
  CSRF with the target bound to the same account (no cross-org IDOR), and each
  records a PII-light control event.

**Live commercial services (2026-07-16):**

* **Account portal deployed to Fly.io** at **[https://account.findmydata.io](https://account.findmydata.io)**
  (+ `findmydata-account.fly.dev`), production + Clerk `idp` mode, TLS issued.
* **Stripe billing enabled** — secrets set on Fly + a signed webhook at
  `/account/v1/billing/webhook` (verified live: an unsigned/bad-sig POST → 400).
* **Plain support mirror enabled** — the machine-user key is set as a Fly secret.
* **Docs live** at **[https://docs.findmydata.io](https://docs.findmydata.io)** (Mintlify, auto-deploys from
  `docs/`).
* **Clerk production instance** on `findmydata.io` — DNS records added; when its
  Frontend API finishes provisioning, the portal flips from the Clerk dev instance
  (`deploy/fly/account.fly.toml` already carries the production issuer + key).

**Verification:** full server suite (331 tests) — 11 new member-management tests
(store idempotency, last-owner invariant, owner-only guards, invite auto-accept,
route integration incl. CSRF + non-member 404). Typecheck + lint clean. Reviewed
adversarially (tenant-isolation / invite-identity / content-boundary lenses).

**Upgrade notes (26.7.15.10 → 26.7.16.0):** no migration — the `org_invites` table
is created idempotently. No config changes required for existing single-owner orgs.

## Part 2c — Release 26.7.15.10 (2026-07-15)

A same-day increment over 26.7.15.9 that wires the Account portal's support cases
to **Plain** (plain.com) and verifies the path live.

**What's new**

* **Plain support mirror** (`account/support-plain.ts`): when a support case is
  opened in the portal, it also opens a Plain thread so the team works it in Plain.
  The portal's own case store stays the source of truth for the customer; Plain is
  the agent-side workflow. Feature-gated: off unless `FMD_PLAIN_API_KEY_REF`
  resolves. See [ADR-0023](adr/ADR-0023-plain-support-mirror.md).
* **Off the response path**: the case commits and returns 201 first; the mirror
  runs as a guarded background task, so a slow/hung/unreachable/erroring Plain (or a
  transient DB error while recording the outcome) can never delay or fail case
  creation. Each outcome records an event (`support_case_mirrored_plain` /
  `support_case_mirror_failed`).
* **Content boundary**: only case metadata crosses (opener email/name, subject,
  severity, portal case id) — never customer content, and **not the org display
  name** (only the opaque account id). The thread body is a fixed content-free
  summary.
* **Live verification** created a real customer + thread in the Find My Data Plain
  workspace. This caught and fixed a real endpoint bug — the previously hardcoded
  `core-api.plain.com` host does not resolve; the correct global endpoint is
  `core-api.uk.plain.com` (overridable via `FMD_PLAIN_API_URL`). The Plain
  machine-user key is scoped to least privilege (`customer:create`,
  `customer:edit`, `thread:create`).

**Adversarial review:** content-boundary, correctness, and robustness lenses with
per-finding verification; four confirmed findings fixed — org-name boundary leak
(removed), a GraphQL null-field `TypeError` mislabeled as unreachable (optional-
chained to surface the real error), the response-path latency (moved off-path), and
non-2xx status classification. Endpoint idempotency (duplicate cases/threads on
manual double-submit) is a pre-existing portal property, noted as a follow-up.

**Verification:** full server suite (320 tests) passing — 9 Plain tests including a
"case creation not blocked by a hung Plain" regression, the null-field regression,
and a content-boundary assertion — plus a live end-to-end thread creation.
Typecheck + lint clean.

**Upgrade notes (26.7.15.9 → .10):** no migration. Operator sets
`FMD_PLAIN_API_KEY_REF` (a Fly/Key Vault secret) to enable the mirror; leaving it
unset keeps cases local-only. See [docs/deploy-fly.md](deploy-fly.md).

## Part 2d — Release 26.7.15.9 (2026-07-15)

A same-day increment over 26.7.15.8 that makes billing **per-seat** and records
the live Stripe pricing.

**What's new**

* **Per-seat (per-user) pricing**: checkout takes a seat count → the Stripe
  Checkout line-item quantity, with adjustable-quantity on so the buyer can tune
  it; the licensed seat count is stored on the account (`cp_accounts.stripe_seats`).
  The webhook extracts and records it on upgrade.
* **Stripe Products created** (test mode) with pricing benchmarked to comparable
  DSPM/data-governance tools: **Standard $14/user/mo**, **Enterprise $29/user/mo**
  (all connectors + priority support). Price ids wired in `.env.example`; rationale
  * go-live steps in [docs/billing-pricing.md](billing-pricing.md).

**Verification:** 311 server tests passing (seat quantity + storage covered),
typecheck + lint clean.

**Upgrade notes (26.7.15.8 → .9):** no migration (the `stripe_seats` column is
added idempotently). Operator sets `FMD_STRIPE_*` and creates the webhook per the
billing doc.

## Part 2e — Release 26.7.15.8 (2026-07-15)

A same-day increment over 26.7.15.7 that adds a fail-closed **demo mode** and the
**demo + dev environment** runbook.

**What's new**

* **Demo mode** (`FMD_DEMO_MODE=true`, `kernel/config.ts`): the deployment
  **refuses to boot** unless it's safe as a public demo — the connector must be
  `mock` (synthetic data, never a live tenant), and live label writes + directory
  sync are forbidden. `demoMode` is surfaced at `GET /api/ready`; the web UI shows
  a persistent "Demo environment — synthetic data only" banner. See
  [ADR-0022](adr/ADR-0022-demo-and-dev-environments.md).
* **Runbook** ([docs/demo-and-dev-environments.md](demo-and-dev-environments.md)):
  hosting the disposable demo (Azure Container Apps + scheduled reset + Turnstile)
  and protecting `dev.findmydata.io` with **Cloudflare Access + Tunnel** (free ≤ 50
  users, no open ports) — with a cost summary.

**Verification:** 311 server tests passing (5 new demo-mode guards), server + web
typecheck + lint + build clean; the demo banner renders from `/api/ready`.

**Upgrade notes (26.7.15.7 → .8):** no migration. `FMD_DEMO_MODE` is off by
default; setting it enforces the synthetic-only guarantees above.

## Part 2f — Release 26.7.15.7 (2026-07-15)

A same-day increment over 26.7.15.6 that adds **Stripe billing / paid conversion**
— an evaluation converts to a paid subscription without reinstalling.

**What's new**

* **Stripe Checkout** (`account/billing-stripe.ts` + routes): an org owner starts
  an upgrade → the portal creates a subscription Checkout Session → Stripe-hosted
  payment (PCI stays with Stripe). The upgrade UI shows on the org card.
  See [ADR-0021](adr/ADR-0021-stripe-billing.md).
* **Signed webhook = source of truth**: the entitlement change happens only on a
  verified `checkout.session.completed` — HMAC-SHA256 over the raw body, constant-
  time compare, 300s replay tolerance. It calls `updateAccountPlan` on the shared
  registry, so the deployment picks up the paid edition on next renewal.
  `customer.subscription.deleted` reverts to evaluation; re-deliveries are
  idempotent.
* **Authorized + gated**: checkout is member-guarded + CSRF (price resolved
  server-side from the edition); billing is feature-gated (needs both secrets +
  a price map); the Stripe secret key stays in the environment.

**Verification:** 304 server tests passing (14 new incl. a signed webhook that
actually upgrades the account), typecheck + lint clean; upgrade UI verified in a
browser; adversarially reviewed.

**Activation (operator):** create Products/Prices in Stripe, add a webhook to
`…/account/v1/billing/webhook`, then set `FMD_STRIPE_SECRET_KEY_REF`,
`FMD_STRIPE_WEBHOOK_SECRET_REF`, and `FMD_STRIPE_PRICE_STANDARD` /
`FMD_STRIPE_PRICE_ENTERPRISE`.

## Part 2g — Release 26.7.15.6 (2026-07-15)

A same-day increment over 26.7.15.5 that implements **production identity for the
Account portal with Clerk** — the last piece for a production-grade portal.

**What's new**

* **Clerk IdP** (`account/idp-clerk.ts`): the portal verifies a Clerk session
  token (RS256 JWT) locally against Clerk's **public JWKS** — no Clerk secret key.
  Pins RS256 (rejects `alg:none`/HS256), checks the signature before any claim,
  and validates issuer / expiry / not-before / authorized-party / a verifiable
  email. See [ADR-0020](adr/ADR-0020-production-idp-clerk.md).
* **`POST /account/v1/session/idp`**: exchanges a verified Clerk token for the
  portal's own opaque session, JIT-provisioning the user by email. The rest of
  the portal (CSRF, membership guards) is unchanged and IdP-agnostic.
* **Mode guards**: `idp` mode requires `FMD_CLERK_ISSUER` + `FMD_CLERK_PUBLISHABLE_KEY`
  (both public); dev self-service routes return 501 in idp mode; production still
  refuses dev auth. The UI loads Clerk only in idp mode.

**Verification:** 290 server tests passing (12 new incl. forgery/tamper/expiry/
alg/azp), typecheck + lint clean; adversarially reviewed (JWT-verification lens).

**Activation (operator):** create a Clerk application, add
`"email": "{{user.primary_email_address}}"` to the session-token customization,
then set `FMD_ACCOUNT_AUTH_MODE=idp`, `FMD_CLERK_ISSUER`, and
`FMD_CLERK_PUBLISHABLE_KEY`. No customer-side migration.

## Part 2h — Release 26.7.15.5 (2026-07-15)

A same-day increment over 26.7.15.4 that builds the **support portal + consent-
based diagnostic bundles** — completing the customer lifecycle (…activate →
**support** → convert).

**What's new**

* **Diagnostic bundle** (deployment side, `support/diagnostics.ts`): a strict-
  allowlisted, redacted operational bundle — versions, non-secret config/feature
  flags, migration status, health **bands**, and error **categories**
  (`RETRY_CLASSES`) with banded counts. No content, names, paths, identities,
  secrets, or error messages; pseudonymous deployment id. Same `.strict()` +
  denylist discipline as fleet telemetry. See [ADR-0019](adr/ADR-0019-support-portal-diagnostic-bundles.md).
* **Preview-before-send**: `GET /api/support/diagnostics/preview` returns the
  exact bundle a support upload would contain — generated locally, nothing sent.
* **Support cases** (Account portal): create/list cases, and **explicitly** upload
  a previewed bundle to a case. The vendor **re-validates the bundle against the
  same strict schema on ingest**, so customer content can never be stored;
  bundles are retention-limited (`FMD_ACCOUNT_SUPPORT_RETENTION_DAYS`, default 30)
  and reads are access-logged. Membership-guarded (not entitlement-gated — support
  works for expired accounts).

**Verification:** 277 server tests passing (12 new), typecheck + lint clean;
adversarially reviewed (multi-lens + per-finding verification).

**Upgrade notes (26.7.15.4 → .5):** no customer-side migration. The deployment
gains a read-only `/api/support/diagnostics/preview`; the portal's support routes
are additive.

## Part 2i — Release 26.7.15.4 (2026-07-15)

A same-day increment over 26.7.15.3 that builds **entitled downloads + software
supply chain** — the download step of the customer journey.

**What's new**

* **Release catalog** in the vendor registry (`cp_releases`: version, channel,
  sha256, size, min-edition, yank), managed by Bearer-admin Control endpoints
  (`/control/v1/admin/releases[...]`). See [ADR-0018](adr/ADR-0018-entitled-downloads-and-supply-chain.md).
* **Entitled downloads** in the Account portal: a licensed org lists its
  edition-permitted releases and mints a **short-lived HMAC-signed URL** — issued
  only after membership + account-active + edition checks. The download endpoint
  is token-authenticated (no session), re-checks yank/expiry so a **yank takes
  effect immediately**, is **path-traversal safe** (basename only), and returns
  the `x-checksum-sha256`. Feature-gated on `FMD_DOWNLOADS_DIR`; production
  requires an explicit URL-signing secret.
* **Supply chain**: the release workflow now attaches a **CycloneDX SBOM**
  (`scripts/sbom.ts`, full install tree) and a `SHA256SUMS` over every artifact.

**Verification:** 263 server tests passing (14 new in `account/downloads.test.ts`

* Control admin release lifecycle), typecheck + lint clean; SBOM generator
  produces valid CycloneDX; adversarially reviewed (multi-lens + per-finding
  verification).

**Upgrade notes (26.7.15.3 → .4):** no customer-side migration. Downloads are
optional and off unless `FMD_DOWNLOADS_*` is configured on the Account portal.

## Part 2j — Release 26.7.15.3 (2026-07-15)

A same-day increment over 26.7.15.2 that builds the **Account portal**
(`account.findmydata.io`) — the human commercial front door on top of the Control
service. Run with `bun run --cwd packages/server account`.

**What's new**

* **Account portal** (`packages/server/src/account/`, entrypoint
  `account-main.ts`): self-service registration → 30-day evaluation → a one-time
  activation code, plus authenticated views of your organizations, deployments +
  live license status, activation codes, and control events. Self-contained
  brand-consistent UI at `/account`. See [ADR-0017](adr/ADR-0017-account-portal.md).
* **Human identity, distinct from Control**: opaque server-side sessions, CSRF on
  state changes, membership-guarded org views (404 for non-members). Dev auth is
  email-only; production requires an external IdP (`FMD_ACCOUNT_AUTH_MODE=idp`),
  which disables dev-login/register until wired.
* **One registry, two views**: the portal shares the vendor database with the
  Control service — registration/evaluation reuse `createAccount` +
  `createActivationCode`. Interop is tested: a portal-minted code activates on
  Control and the deployment appears in the portal.
* **Content boundary unchanged**: the portal knows emails + org names only.

**Verification:** 249 server tests passing (12 new in `account/account.test.ts`
incl. the portal↔Control interop), typecheck + lint clean; the register →
evaluation → dashboard flow verified in a browser.

**Upgrade notes (26.7.15.2 → .3):** no customer-side migration. The portal is
optional and separate (shares the vendor DB); new `FMD_ACCOUNT_*` env vars apply
only to it.

## Part 2k — Release 26.7.15.2 (2026-07-15)

A same-day increment over 26.7.15.1 that builds the **vendor control-plane
service** (`control.findmydata.io`) — closing the loop that ADR-0015 left as
"deployment-side only". This is a separate deployable (`bun run --cwd
packages/server control`) that holds the entitlement signing key and the
pseudonymous account registry; the customer product never runs it.

**What's new**

* **Vendor Control service** (`packages/server/src/vendor/`, entrypoint
  `control-main.ts`): its own isolated database, its own port, machine endpoints
  for `activate`, `renew`, `telemetry`, and deployment `status`, plus
  admin-gated (`Bearer`) account and activation-code management. See
  [ADR-0016](adr/ADR-0016-vendor-control-plane-service.md).
* **Signed activation + renewal**: an activation code is exchanged for a
  freshly-signed entitlement bound to a pseudonymous deployment and the account's
  plan (edition/features/capacity/term). Renewal re-issues with an extended term,
  `supersedes` the prior token, and revokes it (replay resistance).
* **Live online path on the deployment**: `POST /api/controlplane/activate-online`
  (activation code) and online renewal against `FMD_CONTROL_PLANE_URL`. A token
  returned by Control is still verified locally against the deployment's own
  public key and re-bound to the tenant before it is trusted — a spoofed Control
  cannot inject a license (regression-tested). Online renewal falls back to a
  local re-verify if Control is unreachable.
* **Content boundary preserved**: telemetry ingest re-validates the same
  `.strict()` fleet-health schema; production config guards refuse HMAC/ephemeral
  keys and a missing admin credential.

**Verification:** 237 server tests passing (30 new in `vendor/vendor.test.ts`),
typecheck + lint clean; the service boots and serves a full activation flow.

**Upgrade notes (26.7.15.1 → .2):** no customer-side migration. The vendor
service is optional and separate; new `FMD_CONTROL_*` env vars apply only to it.
Deployments gain the optional online activation/renewal path when
`FMD_CONTROL_PLANE_URL` is set (offline paste activation is unchanged).

## Part 2l — Release 26.7.15.1 (2026-07-15)

A same-day increment over 26.7.15.0 that applies handoff **update-001** (commercial web, account, licensing, support, demo, Cloudflare). It builds the deployment-side licensing on the existing control plane; the vendor-hosted Account/Control/Support/Demo services and live Cloudflare changes are captured as design + roadmap. Verification: **204 tests passing**, typecheck/lint/build clean.

* **Versioned, signed entitlement (built).** `entitlementVersion 2` carrying edition, feature flags, capacity meters, term, connector families, offline/renewal policy, issuer, and a rotating key id. Verified locally by **Ed25519** (production — the deployment holds only the vendor public key) or **HMAC** (dev), selected by the token header.
* **Activation / renewal / deactivation (built).** Offline paste-a-token activation is the auditable no-outbound path; renewal re-verifies within a grace window; every event is logged (`licensing_events`, migration 0013) and audited. Live outbound activation to `control.findmydata.io` is gated on `FMD_CONTROL_PLANE_URL`.
* **Non-destructive license enforcement (built).** Starting a new scan requires an active license with the `scanning` feature (`LicenseError`, 402). Unlicensed is permissive; expired/over-capacity never restricts reading data, audit, or export.
* **Fleet-health telemetry (extended).** Adds edition + the vendor deployment pseudonym while staying strict-allowlisted (no content, names, or raw asset counts).
* **Marketing landing page (built, minimal).** One self-contained static page (`site/index.html`), brand-consistent, honest (customer-hosted, not a consumer locator).
* **Cloudflare domain runbook (docs).** `docs/cloudflare-runbook.md`, inspect-first + reversible; no live DNS/Access changes made.
* **Decisions.** [ADR-0015](adr/ADR-0015-commercial-web-and-control-plane.md) records the commercial-web + human/machine-plane separation and the content-boundary invariant.

**Upgrade notes (26.7.15.0 → .1):** migration 0013 applies automatically on boot. New env vars are all optional (`FMD_ENTITLEMENT_KEY_ID`, `FMD_ENTITLEMENT_PUBLIC_KEY_REF`, `FMD_CONTROL_PLANE_URL`); with none configured the product remains unlicensed-permissive (unchanged behavior).

## Part 2m — Release 26.7.15.0 (2026-07-15)

The first formally versioned release. It consolidates everything built to date: the full vertical slice, live Microsoft 365 (CDX) validation, enterprise operations, eight roadmap features, and two adversarial-review hardening passes. Runtime is **Bun 1.3.14** (no Node.js). Verification at release: **196 tests passing**, typecheck/lint/build clean.

Honesty legend used below — this product never overstates its state:

* **Live-validated** — exercised against the live Contoso CDX tenant (`m365x93338811`), read-only.
* **Built, mock-validated** — fully implemented and tested against the deterministic mock tenant/fixtures; the live path is feature-gated and reports `blocked`/`disabled` honestly until configured.
* **Not built** — explicitly absent (listed in [STATUS.md](../STATUS.md) and the [roadmap](roadmap.md)).

### Platform foundation (vertical slice)

The complete product loop, runnable locally with zero credentials: onboard → AI-drafted taxonomy with human approval → scoped owner assignment → durable inventory/delta scans → explainable classification with evidence → owner review → scoped Analyst → approval-gated remediation → hash-chained audit.

| Commit    | Change                                                                                                                                                                                                                                                                     |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `95ba279` | Initial commit: handoff documentation baseline                                                                                                                                                                                                                             |
| `6ec4fc3` | Platform kernel: SQLite schema + migrations, config with production lockout, redacting logger, per-tenant hash-chained audit, capability authz, dev auth ([ADR-0003](adr/ADR-0003-persistence-single-sqlite-behind-role-interfaces.md))                                    |
| `9f2ec1e` | Governance: provider-neutral AI gateway (mock/Azure OpenAI/Anthropic), onboarding, policy citations, draft taxonomy → human approval, owner assignment ([ADR-0005](adr/ADR-0005-ai-provider-gateway.md))                                                                   |
| `5e04e7b` | Connector contract v1, mock M365 tenant (3 scopes, 15 files, scripted deltas + 410-resync), durable scan pipeline with stage caching, seed CLI ([ADR-0007](adr/ADR-0007-scan-pipeline-and-stage-caching.md), [ADR-0008](adr/ADR-0008-connector-contract-and-mock-m365.md)) |
| `15fcc7a` | Intelligence: explainable ensemble, Evidence/Assertion provenance chains, duplicate clusters, 8-component risk, findings, source-free rescore ([ADR-0006](adr/ADR-0006-evidence-assertion-provenance.md))                                                                  |
| `9301fb1` | Review loop: scoped queue, capability-gated redacted excerpts, six decision kinds, confirmation propagation with explanations (never auto-confirms)                                                                                                                        |
| `a4d9d46` | Analyst: typed plans, scoped executor over the search projection, canonical question, injection resistance, full audit ([ADR-0009](adr/ADR-0009-analyst-typed-plan.md))                                                                                                    |
| `47ae6dc` | Remediation: state machine, separation of duties, idempotent execution, drift re-preview, verification from source ([ADR-0010](adr/ADR-0010-remediation-state-machine.md))                                                                                                 |
| `c36784c` | Web UI vertical slice: login, owner home, review with keyboard decisions, landscape, asset detail with connections graph, findings, Analyst, actions, governance, operations, audit                                                                                        |
| `b87ba97` | Docs (README, [architecture](architecture.md), [threat model](threat-model.md), [permissions manifest](permissions-manifest.md), [capacity model](capacity-model.md) + load harness, [CDX runbook](cdx-test-runbook.md)) and the acceptance suite                          |

State: **built, mock-validated** by design — the mock tenant is the default and CI path ([ADR-0012](adr/ADR-0012-cdx-strategy.md)). Demo org is the fictional Meridian Grove Holdings; all fixture people and documents are synthetic.

### Live Microsoft 365 validation

| Commit    | Change                                                                                                                                             |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `b54445b` | Live Microsoft Graph connector (client credentials, `driveItem` delta, permissions, labels, content) + real CDX-tenant validation                  |
| `b52c406` | Docs: record of the live CDX read-only run ([runbook §0](cdx-test-runbook.md), [ADR-0013](adr/ADR-0013-live-graph-connector.md))                   |
| `58037a5` | Fix ambiguous-column bugs in `/api/landscape` and `/api/findings` surfaced by live data; read-model API tests                                      |
| `cf0644c` | Pluggable extractor registry: text family, DOCX/XLSX/PPTX, HTML, RTF, best-effort born-digital PDF (magic-byte sniffing over a bounded ZIP reader) |
| `c83b922` | Live Purview label read/write path with a separate remediation identity                                                                            |

What is **live-validated** (read-only, CDX tenant, app registration `FMD-CDX-Reader`): client-credentials auth, 34 drives discovered, 174 real assets via delta, deduplicated permission sets, real-content classification, and re-score-without-refetch proven live (1.4s vs 151s). After the extractor registry landed, content extraction rose from 62/174 to **172/174** files (only 2 genuine binaries unsupported). Live data also surfaced a real over-classification bug — generic evidence no longer creates type-specific candidates — fixed in the live-connector commit (`b54445b`).

What is **built, mock-validated, live path gated**: Purview label read (`FMD_FEATURE_PURVIEW_LABEL_READ`) and write (`FMD_FEATURE_PURVIEW_LABEL_WRITE`, separate remediation identity, refuses to run unconfigured). Neither has been exercised live; the write path additionally requires protected/metered `assignSensitivityLabel` API enablement (see the [permissions manifest](permissions-manifest.md)). Labels observed on CDX were honestly reported `unknown` because the reader app lacks label permissions.

**Not built**: Graph write path for the main connector (read-only app), Entra ID sign-in (seam exists; OIDC flow returns 501).

### Enterprise operations

| Commit    | Change                                                                                                                                                |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0ab27ac` | Distributed scanner fleet: registration, capability advertisement, heartbeat, stale/offline sweep with in-flight-work takeover                        |
| `630c50d` | Metrics/observability: counters, gauges, latency histograms + DB-derived gauges (queue depth/age, cursor lag), surfaced in Operations; redaction-safe |
| `8e956bf` | Docs: [roadmap](roadmap.md) of remaining design-doc work by release                                                                                   |
| `cf792be` | Admin setup wizard (`/setup`), settings with read-only runtime config (secrets shown as booleans only), and `Connector.probe()` connectivity test     |

State: fleet, metrics, and admin UI are **built and tested** (fleet runs in-process or standalone via `bun run worker`). The connector probe is **live-validated** — it acquired an app-only token and read a CDX site. Container/K8s packaging and signed work envelopes remain roadmap items.

### Near-real-time: change notifications

| Commit    | Change                                                                                                                                                                                                                                 |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `80a75db` | Graph subscription lifecycle (create/renew/lifecycle events), unauthenticated webhook receiver with validation-token handshake + per-subscription clientState HMAC (constant-time, never stored raw), dedup, delta-feed reconciliation |

A notification is treated as a *hint*, never trusted content ([ADR-0014](adr/ADR-0014-roadmap-features.md), decision 1). State: **built, mock-validated**; a dev simulator drives the full receive→reconcile path, and a simulated notification was validated against live CDX — it triggered a real read-only delta reconcile with 4.9s measured lag. Live Graph subscriptions require a public HTTPS webhook URL (`FMD_WEBHOOK_PUBLIC_URL`) plus `FMD_FEATURE_CHANGE_NOTIFICATIONS`; without them the path reports `blocked`/`disabled`.

### Governed learning

| Commit    | Change                                                                                                                                                                                                                                                                                                                                                                     |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `f476906` | Dataset curation + model-release lifecycle + UI (`/models`): review decisions → dataset candidates → human curation → consented cluster-aware train/eval snapshot → proposed ensemble config → held-out evaluation (candidate vs. current precision/recall/F1) → approval with separation of duties (approver ≠ proposer, enforced in code) → promote + rescore → rollback |

State: **live-validated** — the loop ran end-to-end on live CDX data: real HR candidates, SoD blocked self-approval, promotion rescored 174 assets. Nothing material auto-applies. Evaluation currently sweeps the ensemble decision threshold; detector-definition releases would add a feature recompute (documented follow-on).

### Semantic intelligence

| Commit    | Change                                                                                                                                                                                                                                    |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `c5ed73b` | Provider-neutral embeddings through the AI gateway + approximate vector candidate index: cosine LSH (8 bands / 24 bits) with exact rerank, no external vector store; owner-scoped `/api/assets/:id/semantic-neighbors` + asset-page panel |

State: **live-validated** with the deterministic feature-hash default provider — 172 embeddings indexed on CDX; queries return owner-scoped related HR documents. The Azure OpenAI embeddings adapter is implemented but config-gated and not exercised; the Anthropic adapter honestly reports no first-party embeddings. Per-model namespaces keep mock and provider vectors from ever mixing.

### New modalities: Teams and Exchange

| Commit    | Change                                                                                                                                                                                              |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `b34569d` | Teams connector: teams → channels → messages; channel file attachments canonicalize to the backing SharePoint `driveItem` (no second identity); message detector signals; graph topology            |
| `c7ea0a3` | Exchange connector: selected mailboxes (least privilege), per-folder message delta; an attachment is a *copy*, so a content match to a SharePoint asset is recorded as **similarity, not identity** |

State: both **built, mock-validated, live path feature-gated**. The Teams mock proves canonicalization creates zero duplicate assets; the Exchange mock proves attachment similarity (merit-letter attachment → `identical_text` 0.95, zero assets created). Live paths require `FMD_FEATURE_TEAMS_CONNECTOR` + `ChannelMessage.Read.All` (protected API) and `FMD_FEATURE_EXCHANGE_CONNECTOR` + `Mail.Read` on selected mailboxes (`FMD_EXCHANGE_MAILBOXES`) respectively — neither has been run live. Message bodies are reduced to bounded, identifier-redacted previews; only detector signals are retained ([ADR-0014](adr/ADR-0014-roadmap-features.md), decision 2).

### Deployment & commercial

| Commit    | Change                                                                                                                                                                                                                                                                                                                     |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `e9a458b` | Azure deployment IaC (`infra/main.bicep`): managed identity, Key Vault (RBAC) for secret references, storage + file share, Log Analytics, Container App (API + embedded worker); secrets never in the template                                                                                                             |
| `5a7526c` | Control plane: signed entitlement verified locally with an offline grace window + usage-vs-limit evaluation; strictly allowlisted fleet-health telemetry (licensing + health bands only — no content, names, paths, or raw asset counts; `.strict()` schema fails closed) with a transparency preview and customer opt-out |

State: **built**, not live-exercised. The Bicep template has not been compiled with `az bicep build` (no Azure CLI in the build environment), and no actual Control endpoint exists to receive telemetry (deliberately out of scope; nothing leaves the deployment). The honest scale path — Postgres, separate worker app, private endpoints — is documented, not implied as done.

### Hardening

| Commit    | Change                                                                                                                                                                                                                                                                                                                                   |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `be07175` | First adversarial review pass: fixed all **26 confirmed findings** (of 30 raw; 4 refuted) from a 5-lens multi-agent review — isolation, correctness, durability, and honesty fixes, each with regression tests ([review-findings.md](review-findings.md))                                                                                |
| `b745abf` | Second adversarial review pass (3 lenses) on the eight roadmap features: fixed all **5 confirmed findings** — lifecycle-webhook clientState bypass, unauthenticated write-amplification, Teams external-attachment idempotency, `/api/ready` blocked-vs-live honesty, message-preview identifier redaction — each with a regression test |
| `15240f2` | Docs: [STATUS.md](../STATUS.md), roadmap, permissions manifest, and [ADR-0014](adr/ADR-0014-roadmap-features.md) recording the roadmap-feature decisions                                                                                                                                                                                 |

After the second pass, `/api/ready` distinguishes `live` / `blocked` / `disabled` by whether a secret reference actually resolves — no capability claims to be live until its credentials are present.

### Known limitations

See [STATUS.md](../STATUS.md) — specifically its "Mocked / feature-gated (honest list)" table and "Known limitations" section — for the authoritative, current list (single-writer SQLite scale triggers, in-memory mock-tenant state, Teams/Exchange domain-ownership mapping, read-only finding transitions in the UI, and others).

### Upgrade notes

* **Database**: all **12 migrations** (`packages/server/migrations/0001` … `0012`) apply automatically and transactionally on boot — both the API process (`main.ts`) and the standalone worker (`worker-main.ts`) run `migrate()` before serving; applied migrations are tracked in `schema_migrations` and never re-run. No manual migration step.
* **Environment variables**: every variable introduced since the vertical slice is **optional**. Feature flags (`FMD_FEATURE_PURVIEW_LABEL_READ`, `FMD_FEATURE_PURVIEW_LABEL_WRITE`, `FMD_FEATURE_ENTRA_DIRECTORY_SYNC`, `FMD_FEATURE_CHANGE_NOTIFICATIONS`, `FMD_FEATURE_TEAMS_CONNECTOR`, `FMD_FEATURE_EXCHANGE_CONNECTOR`) default to off; `FMD_WEBHOOK_PUBLIC_URL`, `FMD_EXCHANGE_MAILBOXES`, `FMD_ENTITLEMENT_TOKEN`, `FMD_ENTITLEMENT_SECRET_REF`, and the `FMD_GRAPH_REMEDIATION_*` identifiers are unset by default. Unconfigured capabilities report `blocked`/`disabled` honestly rather than failing. Full reference: [installation guide](installation-guide.md), "Configuration reference".
* **Runtime**: Bun 1.3.14. No Node.js is required or used ([ADR-0001](adr/ADR-0001-runtime-language-repo-layout.md)).
