Policy Not Prose

Never hard-code a decision's DATE or its AUTHOR into committed source, a comment, an XML doc comment, or documentation prose. State the rule; link to the policy record. The record carries the date and the name — once.

A rule written as "Maintainer, 2026-09-07: do it this way" embeds three different things in one sentence: the rule, when it took effect, and who decided. Only the first belongs in the place you are reading. The other two are data about the rule, and prose is the worst available store for them.

Why

Prose copies drift. The same decision gets restated in an AGENTS.md, two doc pages and a skill. Change the decision and you must find all four; you will find three. The fourth goes on instructing people for months, and it is indistinguishable from the live ones because they all look equally authoritative.

Names go stale faster than rules. People change roles, hand over areas, and leave. The rule outlives the attribution, so a name in a comment converts a durable instruction into something that reads as gossip about a decision nobody can now ask about. It also invites the wrong question — "is this still true, or just what someone said once?"

Prose cannot be queried. "Which policies are in force, and since when?" is a reasonable thing to ask a system. If the answers live in sentences scattered across repos, there is no answer — only a grep and a guess. A register answers it in one read.

The shape

A policy is a record with five fields and a link:

field what it is
id a stable slug — what other pages cite
value the decision itself, in as few words as carry it
status in force, or proposed when it was cited before it existed — the field the self-healing step below turns on
in force since the date it started applying
set by the role or identity that set it — a role where one exists

Everything else — source, comments, AGENTS.md, docs, skills — states the rule and cites the id. No dates, no names.

- # A What's New entry is written per RELEASE, not per change (maintainer, 2026-09-20)
+ # A What's New entry is written per RELEASE, not per change — policy `whatsnew-cadence`

🚨 What this does NOT forbid

This rule is about policy markers and attributions, not about dates as such. A date that is evidence stays exactly where it is:

Those are facts about events, and an event without its date is not a fact. The test is simple: would this date need changing if the decision changed? If yes, it is a policy marker and belongs in the register. If no, it is evidence and belongs where it is.

For review

A diff that introduces a hard-coded policy date or a person's name into source, a comment, an XML doc comment or doc prose is a review finding. The fix is never to delete the information — it is to move it: add or cite a register entry, and leave a link behind.

The finding is self-healing — it never blocks

A reviewer who finds a hard-coded date or name does not hand back a chore. They run a two-step that always terminates:

  1. Look for a policy that already covers it in the register below. Found → replace the prose with the citation. Done, no issue, one line changed.
  2. Not found → file an issue to introduce the policy, add a proposed row to the register naming that issue, and cite it in place.

The citation is written first and the policy catches up. That is the whole trick: the text is never blocked on the policy existing, and the reviewer is never asked to settle a policy question in a review thread — which is the thing that turns a two-line finding into a three-day argument. Because step 2 adds the register row immediately, a citation never dangles; what is pending is the ratification, not the reference.

This is why the rule converges without a migration. Nobody sweeps the corpus, but every page that gets edited for its own reasons leaves behind one more citation and, where it was missing, one more policy. The register fills from actual traffic — the decisions people actually touch — rather than from an archaeology project, and the pages nobody edits are, by definition, the pages nobody is misled by.

File the issue and carry on. Do not stop the change you came to make in order to ratify a policy you just discovered was implicit — that is the incidental-findings rule, and it applies here exactly as it does everywhere else.

This is the hamster wheel, applied to policy. Triage → implement → test, with every step filing what it finds back into triage rather than absorbing it: the same loop, and the same reason. A change that stopped to ratify every implicit policy it brushed against would land late, review badly, and abandon what it set out to do. The exit has to be cheap or the wheel stops turning — so the exit here is one register row and one issue, and then you carry on with what you were doing.

🚨 Forward-only. No backward migration.

This rule applies to what is WRITTEN FROM NOW ON. It is not a licence to sweep the existing corpus, and a pull request whose purpose is to retrofit old pages is out of scope.

The existing prose is not a defect. It records decisions that were taken and communicated the way the house wrote at the time, it is accurate, and rewriting it would touch a large number of pages to change nothing a reader relies on — while burning the review attention that new work needs. A migration would also be the more dangerous edit, because mechanical rewriting of attributions is exactly the kind of change that quietly alters meaning in the one page nobody re-reads.

So a reviewer checks the direction of travel: does this diff ADD a hard-coded policy date or name? If it does, that is the finding. If it merely fails to remove existing ones, that is not. Old pages migrate only when they are being edited anyway for their own reasons, and only the lines already being touched.

The register

A row is in force once ratified, or proposed when a reviewer cited it before it existed — a proposed row names the issue that will settle it, so the citation resolves from the moment it is written.

id value status in force since set by
whatsnew-cadence A What's New entry is written per RELEASE, not per change. A merge updates its doc page and mints no dated file. in force 2026-09-20 maintainer
issue-taxonomy-scope Classification covers OPEN issues only. Closed issues are not classified, not counted, and appear in no query. in force 2026-09-20 maintainer
release-blocker-gate A release may not be cut while any sev:B or sev:H bug is open in the seven repositories that carry the taxonomy; sev:M and sev:L never gate a cut. Enforced by the release.cut standard in the Governance package. in force 2026-09-20 maintainer
data-sync-approval Adding or widening the synchronisation of data needs a global admin's approval. in force 2026-09-20 maintainer
platform-backwards-compatibility Platform builds are backwards compatible within a major and a declared compatibility epoch — a ladder: a platform roll keeps the old plugin bytes (no rebuild, no re-seal) and plugins roll independently against the running platform. Compiled bytes are keyed on c<major>e<epoch>, never on a per-build identity (provenance only), and carry a platform range: floor (the producing build) ≤ running ≤ ceiling (open by default), else declined loudly naming both versions; platform assemblies bind whenever running ≥ compiled-against. Only a DECLARED break (an epoch or major bump in platform-compatibility.json, with a ceiling for the previous epoch) steps off the ladder and obliges a coordinated rebuild and seal; never seal or pin around a compatibility break — fix compatibility. Manual: Doc/Architecture/ModuleVersioning. in force 2026-09-25 maintainer
version-shapes Exactly two version shapes: X.Y.Z-ci.<n> and clean X.Y.Z. No rc, preview or labelled line is ever minted. in force 2026-09-07 maintainer
query-fanin-stall-terminal A query provider that neither emits, completes nor errors TERMINATES its merged query with a named error instead of hanging: the fan-in's Initial gate is a bound, not a wait. Every consumer that turns a mesh read into a decision reads that error as an availability failure — an access-control read fails CLOSED and reports that it could not be established, never denied and never granted. in force 2026-09-21 maintainer
dynamic-content-type-registration-pass Every replica registers the content type of each already-baked dynamic NodeType it has not activated, in a PACED, REGISTRATION-ONLY pass after the boot's bake settles and OFF the readiness path: it loads the existing bytes and builds the configuration on a transient probe, and never compiles or writes a NodeType record. The cost is the ~13.5 s of assembly opening #1660 removed from boot, moved to that background pass. Never from a read seam. Mechanism: DynamicContentTypeRegistrar (MeshWeaver.Hosting); manual: Dynamic Content Type Registration. in force 2026-09-25 maintainer
open-vocabulary-string-constants A vocabulary that is persisted, serialised, or extended by a module is a static class of const string named exactly as the enum would have been — never a C# enum. It stays OPEN: any other party may add its own values from its own constants class, so the platform's set is never treated as exhaustive. Consumers compare against constants and always carry a branch for a value this build does not know. in force 2026-09-21 maintainer
thread-graceful-error Wherever user code is executed, innermost in a thread, it must gracefully error: every failure path ends in a stamped terminal state on the node. A failure the thread's own hub cannot stamp — because it is the hub that died — is observed, cleaned up and relaunched under a bound, and what a relaunch cannot fix is filed into bug triage. The relaunch/dispatch side is bounded by a configurable pool cap (maxConcurrentAgents, default 50) whose queue is a page, not a log. in force 2026-09-20 maintainer
roll-migrates-first A schema change ships as a new image, and every roll runs that image's database migration FIRST — outside the portal, as its own run-once Job that must report succeeded — before the image moves. The operator half is hosting-migrate; the Roll plan calling it is the Plugins half. in force 2026-09-21 maintainer
db-migration-planned A database schema change is planned before it merges, and every step is enforced by code. Every migration is EXPAND-ONLY: the previous image must run correctly against the new schema, because old pods keep serving until new pods are fully ready (maxUnavailable: 0). A destructive change ships as a two-release expand→contract pair. A pull request that bumps DbVersion.Latest declares Db-migration: V<N> — <kind>; <compat>; rolls migrate-first and passes a rehearsal against a real Postgres (MeshWeaver.Plugins). Every release publishes its ExpectedDbVersion (_releases/_db/<version>, OCI expectedDbVersion). Every roll path runs the target's migration Job before the image moves, or refuses naming both numbers; the operator's run.sh interlock enforces this for every plan, whatever Hosting generation composed it. One documented exception remains until its lane is fixed: the break-glass HelmRelease dispatch (Systemorph/Memex helm-release.yml) applies the migration Job alongside the Deployment; use Reconcile instead. Manual: Doc/Architecture/PlanningADatabaseMigration. in force 2026-09-25 maintainer
first-admin-no-learning-path The instance's FIRST global administrator is seeded with no learning path (User.PinnedPaths empty); an ordinary new user is seeded with the four documentation sections. The learning path is a create-time SEED, never a value a later onboarding write re-imposes. in force 2026-09-21 maintainer
setup-secrets-three-valued The first-run setup dialog's secrets step shows a declared vault mapping whose object does not exist as its own state, missing, decided by the object's versions listing: 200 is exists, 404 is missing, anything else is not checked and never either answer. Missing rows sort first. in force 2026-09-21 maintainer
platform-backwards-compatibility Platform builds within one major and one compatibility epoch are backwards compatible: plugin bytes built against platform N run UNCHANGED on platform N+1 — the ladder P1+p1 → P2+p1 → P2+p2 → P2+p3 → P3+p3, each step changing one side. Only a DECLARED epoch bump (src/MeshWeaver.Compiler/platform-compatibility.json, every broken member listed) breaks it; a plugin whose floor or producing platform is newer than the running one is declined loudly. Proven on every platform build, never assumed: the Platform compatibility … (ladder) check links the deployed plugin set against each platform pull request and hangs off the required check. in force 2026-09-25 maintainer
cluster-upgrade-governed The AKS Kubernetes upgrade — control plane and every node pool — runs only as the governed UpgradeCluster Hosting/InstanceAction on the control instance, behind a mesh approval; never an ad-hoc az aks upgrade, az aks nodepool upgrade or kubectl. ONE approval covers the whole upgrade: control-plane minors chained one at a time, every pool upgraded once straight to the final version, the pool hosting the portals last, a health gate before each pool, and a stuck portal roll refuses it. Mechanism: hosting-aks-upgrade (operator), Hosting/ClusterUpgrade (MeshWeaver.Plugins — plan, approval, executor), the UpgradeCluster lane in Systemorph/Memex aks-ops.yml. in force 2026-09-25 maintainer
module-sync-per-manifest-hash An instance ALWAYS syncs every module it has; nothing holds a whole Space or partition behind a per-identity seal. Each module is judged alone by the content hash (moduleVersion) in its manifest.lock: unchanged ⇒ nothing written; changed ⇒ it syncs to the incoming commit; a declared platform floor above the running platform ⇒ that one module is declined, loudly and by name, and holds no sibling. The seal decides only whether a NodeType ADOPTS prebuilt bytes or COMPILES from the synced source — never whether, or at which commit, sources arrive. Manual: Doc/Architecture/ModuleSyncPerManifestHash. in force 2026-09-25 maintainer
init-timeout-retires-activation A hub that demand routing re-creates (WithReactivationOnDemand — every per-node hub) whose DataContext initialization TIMES OUT is disposed and logged at Error, never latched FAILED for the life of the process; the NEXT ACCESS re-creates it. No timer and no background retry: the parked backlog is answered terminally (ErrorType.Failed) so no re-ask latch retries on its own. A non-transient init FAULT keeps the latch. Mechanism: DataContext.SettleInitializationGate; account in What the DataContext Init Time-Box Bounds. in force 2026-09-25 maintainer
dependent-suites-gate A core change reaches main only after MeshWeaver.Plugins' suites that can reach it pass against the CANDIDATE commit: every merge-queue entry (and a PR labelled dependent-suites) requests the run, and Consolidate test results fails unless the verdict is success. Only drift blocks — a failure that reproduces at the candidate's first parent never does. Supersedes, for this one test request, the rule that core dispatches to no repository; the release wave stays memex's. in force 2026-09-25 maintainer
oauth-bounded-live-credentials The MCP OAuth token exchange keeps a BOUNDED number of live credentials per (user, client_id) — the N newest, N configurable as Mcp:OAuth:MaxLiveCredentialsPerClient, default 5 — and evicts the oldest beyond it, logging each eviction. Not one per client: processes of one installation share a client_id, and one-per-client made each new sign-in revoke every sibling session (#5074). Mechanism: OAuthCredentialEviction (Memex.Portal.Shared); manual: MCP Authentication. in force 2026-09-25 maintainer
bake-gate-readiness-only A ROLL GATE — today the NodeType bake gate, nodetype_bake — holds READINESS only and never fails the startup probe. The startup probe proves only that the process booted; the gate's verdict is read by /ready alone, so a refusal stalls a roll (the pod stays alive, out of the Service) and never kills anything, and a restarted previous-image pod keeps serving. /health still runs and prints the gate; its status excludes it. The startup-critical checks (db_version, PostgreSql) stay on the startup probe; required_modules is a roll gate too (required-modules-readiness-only). Mechanism: ProbeEndpoints.RollGateTag + ServiceDefaults.TagRollGates, chart invariant 10b, RollGateReadinessOnlyTest; manual: The Bake Gate Only Stalls a Roll. in force 2026-09-26 maintainer
required-modules-readiness-only The required-modules check (required_modules) is a ROLL GATE under the same rule as bake-gate-readiness-only: its Unhealthy verdict (a required module the image should ship is absent, or a present one did not install against this platform) stalls the roll and keeps the pod out of the Service, and never fails the startup probe or liveness. Its Degraded verdict (a store-delivered module not here yet) is a 200 and holds nothing, as before. /health still runs and prints it; its status excludes it. Core applies the tag by name, whatever the host's registration carries. Mechanism: ProbeEndpoints.RequiredModulesCheckName in ServiceDefaults.RollGateChecks, RollGateReadinessOnlyTest; manual: The Bake Gate Only Stalls a Roll. in force 2026-09-26 maintainer
silos-pool-autoscaler-max The silos node pool (every portal) has its cluster-autoscaler maximum raised by one, 4 → 5, so a portal roll's surge pod is not left Pending while memex-cloud's KEDA scale-out holds its maximum. Node-pool bounds change only through the governed InfraDeploy action over Systemorph/Memex infra/estate.bicep — what-if first, then an approved deploy — never az aks nodepool update. Owed: the Memex change declaring the pool, the approved deploy, and the agentPools write grant for hosting-operator (the same Azure Kubernetes Service Contributor Role grant UpgradeCluster owes). proposed — maintainer
severity-closes-on-verification A sev:B or sev:H issue closes on post-roll production verification of the running portal, never on the merge that lands its fix: a merge puts the fix on main, and what the label gates is whether the defect is gone from production. sev:M, sev:L, chore, enhancement and documentation issues close on a merge as before. Cited by the Closing keywords (no accidental close) gate, which refuses a body whose closing keyword targets an issue carrying either label. Ratification owed — filed for triage as rbuergi/Feedback/20260922-ratify-severity-closes-on-verification-policy, which carries the measurement that six merges closed a sev:H in the two days before the gate existed. proposed — —

Cited by: Release Process · The Self-Update Schema Wall · Issue Taxonomy and the Release Readiness Gate · Adding a Data Sync Needs a Global Admin · Thread Supervision · Access Control · Closing Keywords and Issue State · The Platform Compatibility Ladder · The Bake Gate Only Stalls a Roll · Deployment (AKS).

🚨 A proposed row is not a weaker in force — it is an honest one. The first draft of this register listed release-blocker-gate as in force while the standard that enforces it was still an unmerged pull request; the row was moved to proposed naming what was owed, and back to in force only once that standard had merged. That is the precise failure this page exists to prevent, committed in the page that defines the rule. If a row's mechanism does not exist yet, the row says proposed and names what is owed.

Adding a policy here is cheap and reversing one is cheap. That is the point: a register entry can be changed in one place and every citation follows, which is exactly what a sentence copied into four files cannot do.