Runtime feature releases: current-code matrix (Increment A)¶
This is the evidence half of Increment A in the runtime feature releases plan.
It records, for each requirement of the flag architecture roadmap
(P1–P10) and each element of Chris's 15 October 2026 functional proposal, what main actually does today.
Baseline. syrf origin/main at e5a3155c4 (2026-10-15). cluster-gitops main at 5bdaa00d6, read
through the GitHub API only. Every row was checked against the code, not against older milestones or PR
descriptions. Line numbers refer to that baseline. No test was run for this matrix; "Done" means the code
and its tests exist on main, not that the behaviour has been proven in a deployed fleet.
Status key. Done: the requirement is met as written. Partial: some of it exists, the gap is
named. Missing: nothing on main meets it.
Summary¶
| Status | Count |
|---|---|
| Done | 5 |
| Partial | 19 |
| Missing | 32 |
| Total rows | 56 |
The gaps that matter most for the plan:
- Production does not run any of this code. Production's API and PM run
104eca9, which has no runtime-flag store, controller, shared reader or migration entry point (row X.1). Everything below describesmain, which production reaches only through the CD plan's catch-up promotion. - On
main, production ignores stored policy on both the read and the write side, and the shared reader refuses to start in production. Enabling production policy is three code changes plus a deployment mode, not one controller check (rows P10.1–P10.3). - No authority envelope. The policy document has
_id,revision,overridesandupdatedAtUtconly: no environment identity, lineage (storeEpoch), catalog or baseline digest, or mutation ID (P3.2, P3.3). - No fleet acknowledgement. Nothing records which host applied which revision, and nothing knows how many hosts there should be, so "Active" cannot be computed (P3.10).
- Propagation is polling only (API 30 s, shared reader 15 s). The legacy API provider keeps its last values forever on failure. The shared reader fails closed on the first failed, missing or inconsistent read; its 60 s age limit applies only if refreshes stop altogether (P3.4, P3.5).
- The migration entry point never checks which environment it is writing to. It takes the database name from configuration and writes. The pre-deploy Job is enabled in no environment, and previews cannot enable it in its current form (E.1, E.2).
- Nothing exists yet for release records, outbox, availability, discovery or protected content (rows R.1–R.10).
- No compatibility report or production machine identity. Staging has OpenIddict and a single-scope machine-client precedent; production is still on Auth0 with no machine client accepted by the API (S.1–S.4).
P1: generated contract and write safety¶
| # | Requirement | Status | Evidence on main |
Gap |
|---|---|---|---|---|
| P1.1 | .NET catalog, defaults and metadata generated from the YAML schema | Partial | RuntimeFeatureFlagCatalog.cs is a handwritten allow-list (Flag(...)/ReloadRequired(...), lines 102–139, 273–282); a catalog test compares it with env-mapping.yaml (operating guide, "Step 2") |
Not generated. Owner, scopes, stale behaviour and grain are not declared anywhere |
| P1.2 | Machine-checked consumer manifest; CI fails on a new unclassified read | Done | consumer-manifest.json (141 consumers), checked by pnpm run validate:generated (guide) |
Static evidence only, as the guide says |
| P1.3 | Expected revision mandatory on every mutation route | Done | Controller RuntimeFeatureFlagsController.cs:115-116; store EnsureExpectedRevision RuntimeFeatureFlagStore.cs:451-457; group path :99-100 |
None |
| P1.4 | Activation class and lifecycle exceptions declared per flag | Partial | RuntimeFeatureFlagActivation has immediate and pageReload only (RuntimeFeatureFlagCatalog.cs:28-32) |
No stale-state behaviour, allowed scopes, safe state or production eligibility |
P2: shared snapshot seam¶
| # | Requirement | Status | Evidence on main |
Gap |
|---|---|---|---|---|
| P2.1 | One immutable snapshot per process, captured once per operation, in API and PM | Partial | SharedRuntimeFeatureFlagReader publishes a frozen SharedRuntimeFeatureFlagSnapshot and Capture() reads it once (SharedRuntimeFeatureFlagReader.cs:16-20, 56-63); used by API FeatureGatedAttribute.cs:52-53 and PM SharedProjectStatisticsFlagSource.cs:34-46 |
It reads the legacy override document only. The legacy API provider still writes values into the mutable FeatureFlags singleton (RuntimeFeatureFlagProvider.cs:83-146) and is what most API consumers use |
| P2.2 | Minimum diagnostic endpoints | Done | API GET /api/runtime-feature-flags/provider-status (RuntimeFeatureFlagsController.cs:65-88); PM GET /api/runtime-feature-flags/reader-status (RuntimeFeatureFlagReaderController.cs:10-16); diagnostic health check (SharedRuntimeFeatureFlagRegistration.cs:74-104) |
Local process only, labelled as such |
| P2.3 | Shared evaluator (dependencies, compatibility, statistics constraints) usable by every host | Missing | Normalisation lives in the API store (RuntimeFeatureFlagStore.cs:337-391); the shared snapshot's Evaluate is "override or deployed default" (SharedRuntimeFeatureFlagReader.cs:19) |
PM cannot evaluate dependencies; a dependency-normalised value differs between API and PM |
P3: usable propagation slice¶
| # | Requirement | Status | Evidence on main |
Gap |
|---|---|---|---|---|
| P3.1 | State and audit committed atomically | Done | WithTransactionAsync around ReplaceOneAsync + InsertOneAsync (RuntimeFeatureFlagStore.cs:88-92, 112-115, 182-186) |
Write and read concern are not explicit at the call site; staging's cluster address carries no w=majority option (cluster-gitops staging.values.yaml:17) |
| P3.2 | Authority envelope: environment ID, storeEpoch, catalog, baseline and policy digests |
Missing | BuildState writes _id, revision, overrides, updatedAtUtc (RuntimeFeatureFlagStore.cs:489-496). The shared reader rejects any document with storeEpoch (SharedRuntimeFeatureFlagReader.cs:112) |
Whole envelope, plus the reviewed legacy-to-lineage migration (README §3) |
| P3.3 | Mutation ID resolves a lost response without a second mutation | Missing | RuntimeFeatureFlagMutation has value, revision, reason and dependency fields only (RuntimeFeatureFlagModels.cs:57-62). The admin UI refreshes instead of replaying (guide) |
Mutation ID, unique index, receipt lookup |
| P3.4 | Change-stream watch with race-free bootstrap | Missing | Flags poll only: API PeriodicTimer(30 s) (RuntimeFeatureFlagProvider.cs:172), shared reader 15 s (SharedRuntimeFeatureFlagRegistration.cs:69). Change-stream code exists for other collections: MongoContext.GetCachedCollectionChangeStream (MongoContext.cs:180-228) and the realtime ChangeFeedSource.cs:69-77 |
Not wired to policy. See the freshness assessment for whether it is needed |
| P3.5 | Periodic reconciliation; a stale observation fails closed for risky admission | Partial | Shared reader: 15 s poll, 5 s read deadline; a single failed, missing or equal-revision/different-digest read sets Unavailable or Inconsistent at once, and Evaluate returns no decision for any state but Current (SharedRuntimeFeatureFlagReader.cs:65-69, 84-104); MaximumAge 60 s marks an observation Stale only when refreshes stop (:45, 56-63) |
Legacy API provider logs and keeps the last values indefinitely (RuntimeFeatureFlagProvider.cs:177-199). No per-flag stale behaviour |
| P3.6 | Revision regression and equal-revision/different-digest detected | Partial | Shared reader marks Inconsistent (SharedRuntimeFeatureFlagReader.cs:90-95) |
Legacy provider accepts an equal revision with any content (RuntimeFeatureFlagProvider.cs:56); so does the browser (runtime-feature-flags.service.ts:111) |
| P3.7 | Deployment-controlled provider mode with parity-checked rollback | Partial | RuntimeFeatureFlags:SharedReaderMode = Disabled/Shadow/Membership/StatisticsServing (SharedRuntimeFeatureFlagRegistration.cs:24-40) |
README proposed ProviderMode=Legacy|Shadow|Shared; the name differs. Rollback parity is a manual runbook step (guide), not a check |
| P3.8 | Browser bootstrap seeds the service with the same envelope | Missing | main.ts:284-291 applies the snapshot; the service's counter starts at -1 (runtime-feature-flags.service.ts:74) |
A slower first poll can apply an older revision than bootstrap did |
| P3.9 | Browser recovery and boundaries: reconnect fetch, revision as a decimal string, context generation, safe fallback | Partial | Five-minute poll plus SignalR hint, 3 s timeout, two retries (runtime-feature-flags.service.ts:32, 86-99) |
No reconnect subscription; revision is a JSON number (:21); no context generation; if the first fetch fails the browser keeps deployed defaults (:99-103, main.ts:292-301), which is permissive once production can stop a default-On flag |
| P3.10 | Fleet acknowledgement against an expected-host inventory | Missing | The guide says there is no fleet registry (manage-feature-flags.md, provider-status paragraph). syrf_metadata records instances and versions (DatabaseMetadataWriter.cs:160-168), not applied policy |
Host acknowledgement store, expected inventory, Active/Degraded computation |
| P3.11 | Each API pod notifies its own SignalR clients after it applies a revision | Missing | Only the writing pod broadcasts (RuntimeFeatureFlagsController.cs:142-143, 149-160); there is no backplane |
Browsers on other pods wait for the five-minute poll |
P4: authoritative baseline import¶
| # | Requirement | Status | Evidence on main |
Gap |
|---|---|---|---|---|
| P4.1 | Reviewed import of an accepted baseline; pods never reseed | Missing | Deployed values come from configuration at startup (RuntimeFeatureFlagCatalog.cs:262-271, store constructor :67) |
Baseline bundle, digest, import command |
| P4.2 | Clearing an override previews the baseline it reveals | Partial | The admin page shows a Deployed column and a Default action (guide) | No side-effect-free preview; the revealed baseline is per-pod configuration |
P5 and P5a: consumers¶
| # | Requirement | Status | Evidence on main |
Gap |
|---|---|---|---|---|
| P5.1 | Migrate one host and flag family at a time with captured reads | Partial | API disableMembership route gate (FeatureGatedAttribute.cs:52-53); PM statistics serving and four consumer selectors (SharedProjectStatisticsFlagSource.cs:34-46) |
Every other consumer is on the legacy provider or startup FeatureFlags. README §13 lists only the membership slice; the PM StatisticsServing mode is newer |
| P5.2 | signalRActive backend/browser lifecycle audit |
Missing | Still special-cased: pinned to deployment while reviewer tracking is on (RuntimeFeatureFlagModels.cs:151, 163-168) |
Audit and classification |
| P5.3 | Grouped controls (not in the roadmap) | Done | RuntimeFeatureFlagGroups.cs; atomic group mutation RuntimeFeatureFlagStore.cs:95-118 |
Ahead of the roadmap; keep |
| P5a.1 | "Use precomputed statistics" operable at runtime in API and PM | Partial | StatisticsServing mode in PM; group statistics-consumers-v2 (guide) |
Non-production only; stops reads but cannot start deployment-disabled serving |
| P5a.2 | "Calculate and maintain statistics" as a coordinated runtime transition | Missing | Maintenance gates pinned to deployment (RuntimeFeatureFlagProvider.cs:71-74, ProjectStatisticsRuntimeActivation.cs) |
Whole requirement; stays its own track |
P6: audit and administration depth¶
| # | Requirement | Status | Evidence on main |
Gap |
|---|---|---|---|---|
| P6.1 | Audit history read API and UI | Missing | Audit is written (RuntimeFeatureFlagStore.cs:72, 184-186) and never read by any endpoint |
Paginated, authorised history |
| P6.2 | Side-effect-free preview with a plan digest | Partial | The 422 dependency plan and exact confirmed set (RuntimeFeatureFlagStore.cs:238-253) |
No preview endpoint, no digest binding revision, catalog and actor |
| P6.3 | Restore as a forward revision from history | Partial | Default/clear exists | No restore-from-history |
| P6.4 | Real-actor authorisation under impersonation; both identities audited | Partial | Audit records real and impersonated identities (RuntimeFeatureFlagsController.cs:162-174, RuntimeFeatureFlagStore.cs:516-521). But the authorisation decision uses currentUser.GetUserId(), which returns the impersonated user (CurrentUserService.cs:26-31), and under support impersonation the claim groups are empty (:48-50). Authorisation is the target's authority, safe today only because impersonation cannot escalate |
Decide which identity is authorised for production policy; plan RT-13 asserts it per impersonation path |
| P6.5 | Separate capabilities: change policy, emergency stop, inspect targeting | Partial | One capability, ApplicationCapability.ManageRuntimeFeatureFlags, mapped to Administrator (ApplicationAuthorityPolicy.cs:42, 85) |
Split capabilities |
| P6.6 | Emergency stop distinct from Off | Missing | The UI offers On, Off and Default only (guide) | Stop semantics, safe state, separate permission |
P7–P10¶
| # | Requirement | Status | Evidence on main |
Gap |
|---|---|---|---|---|
| P7.1 | Project-scoped allow/deny | Missing | The guide: "This MVP deliberately does not include percentage or audience targeting" | Whole phase |
| P8.1 | User, investigator and combined targeting; beta consent | Missing | Same | Whole phase |
| P9.1 | Stable basis-point cohorts | Missing | Same | Whole phase |
| P10.1 | Production mutation permitted under an explicit policy | Missing | IsMutationAllowed allow-lists non-production environment names (RuntimeFeatureFlagStore.cs:467-487); the controller returns 403 (RuntimeFeatureFlagsController.cs:109-110) |
Deployment-controlled production mode (see ADR-025) |
| P10.2 | Production reads honour stored policy | Missing | BuildEffectiveValues ignores overrides when mutation is not allowed (RuntimeFeatureFlagStore.cs:352); descriptors hide them (:199-202) |
The proposal mentions only the mutation refusal; the read side must change too |
| P10.3 | Shared reader runs in production | Missing | Enabled modes throw unless the environment is in a non-production list (SharedRuntimeFeatureFlagRegistration.cs:31-36) |
Same production mode switch |
Proposal elements¶
| # | Element | Status | Evidence on main |
Gap |
|---|---|---|---|---|
| E.1 | Environment binding verified before any migration write | Missing | MigrationEntryPoint.RunAsync reads ConnectionStrings:MongoConnection:DatabaseName and opens it (MigrationEntryPoint.cs:124-131); RuntimeEnvironment is used only for metadata (:138-141). syrf_metadata.meta.environment is a best-effort SetOnInsert from any pod start (DatabaseMetadataWriter.cs:239-246) |
Binding record and pre-write check (plan §6.3) |
| E.2 | ADR-023 migrations can provision the release stores | Partial | Runner, lock file and PreSync Job template exist; latest PM migration is 0008 (PmMigrations.cs:115-119, migrations.lock.json). Only CreateIndexMigration and UnsetFieldsMigration kinds exist |
The Job is enabled in no environment (no migrations.enabled in cluster-gitops; pre-deploy plan M22): staging needs pre-deploy R1, production its catch-up enablement, previews the DatabaseLifecycle switch to up (the template refuses migrations.enabled with preview.waitForDatabase, _migrations-job.tpl:39). A second catalogue in the same database would see the first one's syrf_migrations records as unknown (MigrationRunner.cs:83, 151). Expected binding values cannot come through env:, which the Job strips of SYRF_MIGRATION_* (_migrations-job.tpl:51-55) |
| E.3 | Global policy identity separate from release ID | Missing | Only a global revision exists |
Release ID |
| R.1 | Feature definition with stable identity, separate from flag keys | Missing | Closest: RuntimeFeatureFlagGroup(Key, Name, Description, Keys) (RuntimeFeatureFlagGroups.cs:6) |
Feature catalog |
| R.2 | Release intent, record and append-only events | Missing | None | Whole store |
| R.3 | Durable outbox for announcements | Missing | Pattern exists: pmActivityClaimRevocationOutbox (MongoDB reference) |
Release outbox |
| R.4 | Availability response per trusted context | Missing | Anonymous GET /api/runtime-feature-flags returns global values only (RuntimeFeatureFlagsController.cs:59-63) |
Context-aware endpoint |
| R.5 | Discovery preference (seen/dismissed) | Missing | Closest: ReviewerWorkspaceSettings.FirstRunHintSeen (ReviewerWorkspaceSettings.cs:54, 67), scoped to the review workspace |
Store and endpoint |
| R.6 | What's New driven by confirmed milestones | Partial | Static whats-new.json, shipped with the web image; date is a hand-typed string such as "18th August 2026" (whats-new.json:6) |
Dates follow web deployment, not activation. The feed how-to still describes a latest/past shape; the feed uses updates |
| R.7 | Guide shows availability and milestone dates | Partial | Public Jekyll site; dates appear in page titles (user-guide/whats-new/whats-new-bulk-pdf-upload.md, front matter) |
No authenticated or availability-aware view |
| R.8 | Protected (private pilot) content, assets and search | Missing | user-guide/ is a public static site |
Protected surface |
| R.9 | Declared editorial timezone for date-only headings | Missing | Storage and scheduling convention is UTC (Quartz schedules set TimeZoneId = "UTC", e.g. ProjectStatisticsMaintenanceSchedule.cs:21; statistics charts render UTC, observed-history.ts:689). No declared display zone for date-only headings; feed dates carry no zone |
Decision OD-9 (UTC or Europe/London for display) |
| R.10 | Staging rehearsals cannot create production records | Missing | No release records exist; staging and production databases are separate (cluster-gitops staging.values.yaml:13-17, production.values.yaml:19-20) |
Binding and environment checks on every write |
| C.1 | CD plan G2 production render comparison | Missing | Planned only; .github/promotion/ does not exist on main |
Built by the CD plan; made conditional by this plan |
| X.1 | Production runs the runtime-flag mechanism described above | Missing | cluster-gitops production/api/config.yaml pins 9.44.1-restore.104eca9 (chart api-v9.44.0) and PM 11.43.1-restore.104eca9; git grep -il 'RuntimeFeatureFlag\|MigrationEntryPoint' 104eca9 -- src returns nothing |
The CD plan's catch-up promotion; every production row above describes main |
| S.1 | Environment compatibility report through a scoped automation view | Missing | Only the anonymous snapshot exists (RuntimeFeatureFlagsController.cs:59-63); the statistics staging preflight script combines that snapshot, /health/live versions and cluster-gitops files itself (statistics STATUS) |
Report contract, revision vector, endpoint (plan §14) |
| S.2 | Environment-bound machine identity with a read-only scope | Partial | Staging: OpenIddict client-credentials (Identity.Endpoint/Program.cs:142); the single-scope syrf-statistics-evidence client (StatisticsParityEvidenceClient.cs:4-23) and the statistics:operate credential, accepted on specific routes (API Program.cs:481-485). Production: Identity disabled (cluster-gitops production/identity/config.yaml) and no OpenIddict validation configured, so only the Auth0 JWT scheme authenticates there; it would authenticate an Auth0 machine token for the API audience, but no authorisation path for a machine principal exists in production, and the tenant's grants are not visible from the repository |
Production automation identity and its authorisation path (plan OD-21) |
| S.3 | Workload observations from a scoped collector | Partial | syrf_metadata instance records (DatabaseMetadataWriter.cs:160-168); staging-only read-only Kubernetes identity syrf-stats-preflight (#3806) |
Collector publication route and production identity |
| S.4 | Deployment compatibility constraint enforced on policy writes | Missing | None | Constraint record and write-path check (plan §14.5) |
Superseded assumptions¶
The full list, with what replaces each, is in plan §3.