Compiled Against A Platform The Instance Does Not Run

On 2026-09-14, six NodeTypes on memex.meshweaver.cloud stopped compiling forty minutes after a satellite merge. Every gate was green, and correctly so. This page is about the window they do not cover, and about a compiler diagnostic that is easy to read in the wrong direction.

What is and is not gated

It would be wrong to say in-mesh source is ungated — it is compiled in CI, exactly as the mesh compiles it:

gate what it compiles / measures against which platform
The Module Platform Link Gate a module assembly's linked types the platform actually running — it refuses a generation the process cannot load
node-repo-compile-check.ymlcompile-check.py (compile-check / Compile every NodeType (vs core), a required context on most satellites) every NodeType's resolved Source, concatenated with hoisted usings, the way the mesh does it the reference set of a resolved platform image — for MeshWeaver.Plugins, scripts/resolve-platform.py picks the newest SEALED set of core's main-cd
minMeshVersion floor (Module Adoption Policy) a version string the author writes advisory since 2026-09-07, and per module rather than per source node

The second row is the one that matters here, and it is not a hole: it was introduced precisely so that API-drifted Source could no longer merge green and park on a live mesh. It ran on the commit in question and it passed — measured below.

The gap is narrower, and it is a gap of target, not of coverage:

No gate compares the delivered source against the platform image the RECEIVING INSTANCE is running. The link gate does exactly that, for bytes. Its counterpart for source is not wired on the path the content actually travels.

The measured window — 2026-09-14, memex.meshweaver.cloud

when what
2026-09-12T09:30:22Z core commit c84c6c055 — this becomes the image both portals run (rolled 12:34Z as 3.0.0-ci.8411)
2026-09-12T17:28:05Z core PR #4102 (00fa2b69f9) adds PlanTierRefusal (src/MeshWeaver.Mesh.Contract/Security/PlanTierRefusal.cs) and RegistryPackageSource.ListCatalog (src/MeshWeaver.PluginCatalog/RegistryPackageSource.cs:95)
2026-09-14T06:19:40Z MeshWeaver.Plugins merges dd9c96b4c8 (PR #1837), changing the in-mesh Code node Store/Publishing/Source/RegistryPackages.cs to call both
on that commit, Compile every NodeType (vs core) is completed/success (48 success, 11 skipped, 0 failures) — the resolved platform was a sealed set that carries 00fa2b69f9
2026-09-14T06:59:28Z the package lane has delivered the source to memex-cloud, still on c84c6c055. First failing compile — forty minutes after the merge

Both portals answered 3.0.0+c84c6c05503228860df03c4a8b596e497e6d218c to ten /api/version samples that day (six on memex.meshweaver.cloud, four on memex.systemorph.com), so this was not one unlucky replica. And the direction was settled on the surface, not on ancestry, exactly as the procedure below asks: PlanTierRefusal.cs is a file 00fa2b69f9 ADDED, and git cat-file -e c84c6c055:src/MeshWeaver.Mesh.Contract/Security/PlanTierRefusal.cs reports it absent at the running commit.

Blast radius, from the incident's retained samples (ten, against occurrences: 68, between 06:59:28Z and 11:26:05Z, across three pods): Store/Catalog, Store/Plugin, Store/Enrollment, Store/Provision, Store/Maintenance, Hosting/InstanceRequest. 🚨 A floor, not a count — the sample list is a bounded ring, and the failing replica's own /health bake-report read previouslybroken=11.

Why Roslyn ran at all

A portal does not normally compile module content: PrebuiltAssemblySeeder adopts the CI-baked NodeType assembly when the framework identity matches, and Roslyn is the fallback for a prebuilt that is missing or declined. Here the bake was produced against the newer platform, so its identity did not match c84c6c055, the prebuilt was declined, and the fallback compiled the new source against the old reference set.

🚨 That fallback is an opt-out. Modules:RequirePrebuilt (PrebuiltAssemblySeeder) turns a declined or missing prebuilt into a named, early error — it fails the install/update naming the package, the registry, the framework identity and the architecture — instead of quietly attempting a compile that cannot succeed. It defaults OFF, because compiling is the right fallback on dev, CI and bake meshes. A production portal is exactly the place its own doc comment says should opt in: "the runtime artifact of a module is a baked DLL; a silent compile there is a distribution failure being papered over." The failures above are Roslyn compile failures, which is itself evidence the key was not set on that deployment. Turning it on would not have prevented the skew — it would have named it at install time instead of parking six NodeTypes with a CS-number.

Reading the diagnostic — and the two ways to read it wrong

CS0246  The type or namespace name 'PlanTierRefusal' could not be found …
CS1061  'RegistryPackageSource' does not contain a definition for 'ListCatalog'
        and no accessible extension method 'ListCatalog' …

What the CS1061 does establish. RegistryPackageSource resolved, so the platform assembly is on the reference set and the fault is in its surface, not in a source node that failed to land. That is the useful half, because it separates this from Missing Declared Sources and Install Completeness, where a bare CS0246 has a completely different cause and a completely different remedy.

🚨 What it does NOT establish is the DIRECTION. The same diagnostic appears when the platform is ahead and the member was removed or renamed, leaving stale content behind; and an extension method can be missing merely because its declaring class is out of scope. CS0246 on its own is weaker still. So "the platform is behind, roll it" is a conclusion to reach, never to read off the compiler.

Confirm before concluding:

  1. The member is declared by the platform assembly, and find the core commit that introduced it — git log -S '<symbol>' --oneline -- src/.
  2. The running commitcurl -s https://<portal>/api/version, sampled more than once, since repeated calls land on different replicas.
  3. Compare the surface, not just the history. git merge-base --is-ancestor <adding> <running> is an ancestry check, not a proof: a false also arises from diverged histories or a cherry-pick, and a true does not prove the member survived to the running commit. Read the file at the running commit — git show <running>:<path> — and prefer the instance's own answer where one exists.

And distinguish it from FrameworkStale, which is a different condition — there the bytes were baked against a different framework identity and a rebuild cures it, which is what that overlay promises. A NodeType parked on a genuine platform skew will never rebuild green; only a roll to a set carrying the adding commit will move it.

Why the NodeType sweep named none of the six

search 'nodeType:NodeType content.compilationStatus:Error partitions:all' limit:200 answered 8 at 2026-09-14T12:43Z, all of them Hosting/* — see Search Coverage and Refusal.

With its denominator that reading is 8 of N ≥ 200 over M unknown: the companion search 'nodeType:NodeType partitions:all' limit:200 came back count: 200, truncated: true, so N is a floor, and the envelope carried no coverage field at all — the image serving this portal predates the provider half that reports which partitions were read. By the sweep's own rule, M unknown does not pass on its own. Here it was actively misleading, for two independent reasons, neither of them a bug in the sweep:

  1. It is RLS-filtered, so it is not the denominator. nodeType:NodeType partitions:all returned no AgenticOffice/* rows for this identity, while the same deployment's system-side /health counted 594 instances of three AgenticOffice types.
  2. compilationStatus is one shared field over a per-replica compile. A replica still serving an adopted prebuilt leaves the field Ok while another replica's fallback compile fails. This is a property of the MECHANISM, and it stands — but the number first cited for it does not show it: /health's bake-report read previouslybroken=11 on the failing replica against the sweep's 8, and previouslybroken reads the SAME shared field (NodeTypeBakeStatus classifies CompilationStatus.Error as PreviouslyBroken first). So 11 against 8 is reason 1 — a system denominator against an RLS-filtered one — not an independent per-replica verdict (corrected on #4320). The genuinely per-replica signals are /health's content-types (a read that degraded on THIS replica) and bake-report's Baked / BytesMissing split — the only states decided by probing this replica's own assembly store. FrameworkStale, NeverBuilt and PreviouslyBroken are decided from the shared record alone (NodeTypeBakeStatus.ClassifyDetailed returns them before the store is asked).

🚨 And /health answers about one replica you did not choose: six calls to the same host landed on at least five replicas, whose bake sweeps ranged from 2026-09-12T12:55Z to 2026-09-14T08:44Z and whose content-types lists were disjoint. For a per-replica census with no guesswork, use the control instance's { "requestedAction": "Sample" } — see Operating From The Portalonce it has replicas to walk: its replicas[] comes from kube-state-metrics, which has no series for the portal namespaces, so every Ops/Status/* reads notScraped: true / replicas: [] until #4218 is fixed, and a Sample today carries no per-replica /health body at all. What does protect a roll is the new replica's nodetype_bake readiness gate — when it is registered and armed (the host registers it only under gateBake, and an unarmed gate admits traffic) — which refuses readiness for a type that REGRESSED on its image and passes one already at Error, so such a type is never named by it.

What would close this class

Stated as options. None has been scoped by the maintainer, and the last one is a legitimate answer.

Reconnecting…
The connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.