Required Module Authority

A Deployments/<name> record is meant to be the single authoritative description of an instance. For required modules it was not, and the reason is a property of ASP.NET configuration rather than of anything in this repository: Modules:Required is an ARRAY, and configuration merges arrays BY INDEX.

A later provider replaces the entries it NAMES and leaves every other index of an earlier one standing. An array can therefore express "replace entry N" and can never express "these and only these".

What that does to a record

The record's requiredModules is rendered as Modules__Required__0, __1, … by DeploymentPortalConfig.ModuleEntries into the portal ConfigMap (or, under Aspire, the container environment). The image's own list ships in Memex.Portal.Distributed/appsettings.json, in another repository. Two providers, in that order.

The record says What the instance actually requires
a list longer than or equal to the image's the record's list (the intended reading)
a list shorter than the image's the record's entries plus the image's tail, which the record never names
an empty list the image's list in full — nothing is rendered, so nothing is overridden

The last row is the one an operator hits first, and it is the opposite of what they meant: emptying requiredModules does not relax the requirement, it restores it.

Measured on pearl.meshweaver.cloud, 2026-09-16 (#4476). Deployments/pearl named five modules and never named Social; the portal demanded Social anyway, because the image's index 5 was never overridden. The record was then edited to requiredModules: [] at 06:08:24Z, and the pod created by the re-provision that followed reported at 06:18:28Z that five required modules were still missing.

Why "put your entry at the first free slot" is not a fix

The remedy that predated this page — RequiredModuleSlots, a boot module at an explicit index past the image's list — needs the caller to know how long the image's list is. Nothing in the record, the chart, the Aspire adapter or the operator can read that: it ships from another repository and it grows.

It has grown. The list was seven entries when the shadow check was written and is nine today (Social, Blazor.Chat, Markdown.Collaboration and AI were each added because losing one silently is a measured outage). Deployments/memex-cloud still names slot 7 for MeshWeaver.Mcp.dll — and index 7 of today's image is MeshWeaver.Markdown.Collaboration.dll, so Memex#131 has silently recurred on the public instance (measured on the control instance, record v85, 2026-09-16; tracked as Systemorph/Memex#378). A free slot is not a property of the deployment; it is a property of an image the deployment cannot see, and an answer that expires silently is not an answer.

So the advice is retired, and the rule that replaces it is a REFUSAL, not a better index. DeploymentPortalConfig.PositionalModuleSlotProblems(record) reports every requiredModuleSlots entry on a record that does not claim the complete set — naming the slot, the module and the restatement — because no index such a record can pick is safe: its meaning is whatever the image shipped that day. Under the claim the same slot is reported by nothing, and that is the whole difference: the image's list does not apply at any index, so a slot can shadow nothing. The rule is route-neutral (a shadow is a shadow under Aspire too), which is why it is its own surface rather than folded into the chart-scoped ceiling check beside it — a record can hit one, both or neither, and ThePositionalRuleAndTheChartCeiling_AreIndependent pins all four combinations.

🚨 Two guards, no caller — read this before trusting either. Neither PositionalModuleSlotProblems nor ChartModuleSlotProblems is asked by anything that deploys. The one renderer that asks a record "why can you not be deployed" is HelmValues.Problems (MeshWeaver.Plugins, Hosting/Deployment/Source), and it asks only its own slot-collides-with-the-list check — which compares the slot against the RECORD's list, the half that was never the problem. So today both rules are pinned in core and reach no deploy. Making the renderer ask them is the satellite half, and it needs a pin bump that carries this change: MeshWeaver.Plugins#2045.

The claim

A record states that its own entries are the complete set:

{
  "requiredModules": ["MeshWeaver.Blazor.Radzen.dll", "MeshWeaver.Speech.dll"],
  "requiredModulesAuthoritative": true
}

which renders one extra key beside the entries:

Modules__RequiredIsAuthoritative = true

That key is a scalar, so no index merge can touch it — which is the whole point. It is also the only thing on the wire that an empty list can still say: with no entries rendered at all, "require nothing" and "this record has no opinion" are otherwise the same bytes.

The reading is MeshBuilderModuleActivation.RequiredEntries(configuration), and it is read off provider order, the same mechanism ShadowedRequired already used:

No count, no padding, and no knowledge of the image's length. A later provider may withdraw the claim by setting it to false; the renderer therefore never emits false, only true or nothing.

Both delivery routes carry it, and both carry the same slots — the contiguous requiredModules list and every explicit requiredModuleSlots entry. (The operator's catalog config file used to render the contiguous list alone and drop the slots, so two renderers of one record described different required sets. Harmless while both were read as an overlay; beside the claim they would have stated two different complete sets, and the route that dropped a slot would have said the module is not required at all.)

🚨 The chart's block has a ceiling, and under the claim an unrendered slot means NOT REQUIRED. The portal ConfigMap names every Modules__Required__N key literally — a Helm range renders correctly and is invisible to the key-literal guards — so the list stops somewhere. DeploymentPortalConfig.MaxChartRenderedRequiredModuleSlot states where, a test reads the number back out of the template so the two cannot drift, and ChartModuleSlotProblems names a slot above it instead of letting the render drop it.

That check is scoped to the chart and deliberately not folded into SpecProblems: the Aspire route injects whatever PortalConfig emits, ceiling and all, so it delivers such a slot correctly, and a route-neutral "why the spec cannot bring an instance up" answer that named it would be false for an Aspire run. It is also the nastiest shape here — a slot above the ceiling works on a laptop and disappears in the cluster — so a Helm renderer asks it alongside the spec problems.

The boot path and the /health required_modules check must ask this question the same way — a probe that disagreed with the log line before it would be worse than no probe — so both go through RequiredEntries, and MissingRequired is defined in terms of it.

Why it is opt-in

The image's list is the platform's floor, not a default the record replaces: the AI engine, the chat renderer and the collaboration pack are named there because a portal that lost one rolled out green and served with the feature simply gone (2026-08-26/27).

Every record in the fleet names five modules against the image's nine. Taking a partial list as authoritative by default would have un-required four modules on every instance the day it shipped — a silent loss of exactly the guarding the list exists for. So the claim is explicit, and a record that does not make it behaves as it always did.

The two instruments

A claim nobody knows about would leave the default trap live and silent, so the running host reports both halves of the merge through the boot report (stderr, pre-DI, so it is in the pod log an operator already has open):

Instrument What it sees The shape it catches
ShadowedRequired an entry the deployment replaced — supplied by an earlier provider, overwritten at its index, and named nowhere else Modules__Required__5 = Mcp over an image whose index 5 is Social (Memex#131)
UnstatedRequired an effective entry the deployment's own overlay never reached a record naming five over an image naming nine — the instance requires four modules its record does not name (#4476)

The two are different questions. A replaced module is not MISSING (nobody asks for it any more), so the readiness contract has nothing to say; an unreached module is not replaced either, so the shadow check is silent. Both are empty when a deployment states the claim — it stated the whole set, so nothing is unstated, and replacing the image's leading indices is the point — and UnstatedRequired is also empty when only one provider supplies entries at all, so a Monolith, a test mesh or the CLI never sees the image's own list reported back to it.

Doing it

Fluent: record.WithRequiredModules(…).WithRequiredModulesAuthoritative().

Where it is pinned

Test Holds
ConfiguredModuleActivationTest (test/MeshWeaver.Compiler.Pipeline.Test) the reading, over a real two-provider configuration — the short list, the empty claim, the explicit slot, blanking, the no-claim default, the non-root section
RequiredModuleAuthorityTest (test/MeshWeaver.Deployment.Contract.Test) the rendering — both routes agreeing slot for slot, never-false, the JSON round-trip, the ceiling read back out of the chart, and a slot above it reported rather than dropped — scoped to the chart, since the Aspire route delivers it — plus the POSITIONAL rule on the live memex-cloud shape (slot 7, no claim → reported; the same record with the claim → silent), the two surfaces' independence, and one normalization across entries and problems

🚨 The cross-assembly key assertion is in the FIRST of those, not the second, and deliberately. The record renders from MeshWeaver.Deployment.Contract (zero MeshWeaver references by design — it ships inside the published Aspire package) and the host reads from MeshWeaver.Mesh.Contract, so the key is spelled twice and a rename on one side would silently stop the other from ever seeing the claim — rendered-and-never-read is indistinguishable from not rendered. But MeshWeaver.Deployment.Contract.Test references only the renderer's assembly, so the same two constants compared there would be the renderer against a literal: a check that cannot fail for the reason it exists. ConfiguredModuleActivationTest sees both assemblies (through MeshWeaver.PluginCatalog), so the comparison lives there.

Still open

The /health required_modules check lives in Memex.Portal.Distributed (MeshWeaver.Plugins) and still reads the raw Modules:Required array. Until it adopts RequiredEntries, a record that makes the claim would have the probe and the boot path disagree; for a record that does not make it — every record today — the two readings are identical. The core half lands first because the satellite compiles against a pinned core. Tracked as MeshWeaver.Plugins#1963, to be done with the pin bump that carries this change.

And no fleet record states a complete set yet: memex, memex-cloud and pearl each name five against the image's nine, and memex-cloud additionally shadows one. Restating them is an operator change on the records in Systemorph/Memex, tracked as Systemorph/Memex#378 — the claim makes it expressible; it does not make it happen.

🚨 The restatement is safe BEFORE the instance runs an image that reads the claim, and that is what makes it the one edit to make. Re-measured 2026-09-17: Deployments/memex-cloud is at v92 with the same slot 7, and the instance runs 3.0.0-ci.8411 — an image from before this change, so it has no reader for the claim and its deployed chart has the old ceiling. A record that restates the image's nine entries at their own indices and adds MCP at 9 renders Modules__Required__0..9, which the old chart carries (its ceiling was 9) and the old image index-merges to exactly the intended set; Modules__RequiredIsAuthoritative rides along inert until the pods roll onto an image that reads it, and then says the same thing the entries already do. The alternative interim — moving the slot from 7 to 9 — fixes this instance for as long as the image's list is nine entries, which is the property that has already expired twice.

See also

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.