Modules the portal image seeds
Most modules reach an installation from a plugin registry: the Store bundle is how they arrive
and how they update, and a copy in the portal's app closure (/app/<Name>.dll) breaks that
outright — ModuleLandingService refuses a same-identity bundle that would shadow an /app binary,
so the module stops being updatable at all.
A self-registry install has no registry to receive from. It is the registry, and it serves a
source checkout, where no module BINARY can ever land. Such an install is therefore left with no
delivery path whatsoever for a registry-served module — and because Modules:Assemblies is
skip-and-continue by design, the portal comes up healthy with the feature simply gone.
The cure is a seed: the image carries a copy under modules/<Name>/, a different layer from the
app closure. It is not a second producer and not a pin — the lander's same-identity refusal is keyed
on a collision in the app/base root, which a modules/ row never creates, and since
MeshWeaver#2548 a usable store-installed entry overrides a same-named baseline in place. A
registry-backed portal goes on adopting the registry's bytes; the seed is the floor beneath them.
It takes TWO rows, and either alone is inert
| Row | Where | What it does |
|---|---|---|
<MeshModuleClosure Include="…/<Name>.csproj" /> |
Memex.Portal.Distributed.csproj and Memex.Portal.Monolith.csproj |
Puts the BYTES at modules/<Name>/<Name>.dll |
"<Name>.dll" under Modules:Assemblies |
each host's appsettings.json |
ASKS boot to load them |
Boot's module set is exactly Modules:Assemblies ∪ the landed activation sidecar, and a sidecar
entry is written in one place only — ModuleLandingService.LandCore, reached only when a BUNDLE
lands. On a self-registry install no bundle ever lands, so without the second row the seed is
bytes nothing resolves: they sit on disk and no [ModuleLoad] line names them. Without the first
row the entry names bytes that are not there and is skipped silently.
🚨 A package install record is not a third input. preInstalled: true gives a package an install
record on every deployment and the boot reconcile reports it up to date — while the module is never
activated.
PortalSeedLaneTest (in Memex.Hosts.Test) pins both rows, from both ends, for every module listed
in its SeededModules table, and ViewPackDeliveryPathTest pins the same contract for the two form-
control packs. Add a module to the table without its rows and the test fails; delete a row without
the table entry and it fails too.
Which modules are seeded, and why each one is
Nine rows were added because the module's absence broke something structural; the tenth is Markdown.Export. Being registry-served is not the criterion — most registry-served packages are correctly not seeded, and a seed is how a specific, measured loss is stopped.
| Module | Put here by | What its absence cost a self-registry install |
|---|---|---|
MeshWeaver.Hosting.Instance |
— | The "this instance" app, which has to exist BEFORE any package can be installed |
MeshWeaver.AI |
#1244 | No AI engine: no chat runtime, no Provider/* catalog |
MeshWeaver.Blazor.Chat |
#1244 | Every chat surface through the escaped-HTML fallback |
MeshWeaver.Markdown.Collaboration |
#1262 | No markdown editor, no collaborative view |
MeshWeaver.Mcp |
#1468 | POST /mcp answering 404 — no MCP client could reach a local mesh |
MeshWeaver.Blazor.EntityViews |
#1483 | Every form control rendered as its own ToString() |
MeshWeaver.Blazor.Graph |
#1483 | MeshNodePickerView, same shape |
MeshWeaver.Hosting.Cosmos |
— | Storage: a Store-installed module needs storage to already work |
MeshWeaver.Hosting.Snowflake |
— | Same |
MeshWeaver.Markdown.Export |
#1597 | No PDF, DOCX or HTML export of any document, for anyone — including a course certificate |
Cosmos and Snowflake are seeded but deliberately not baseline-activated: a storage backend is
chosen by hosting configuration, not loaded on every boot. They are the anti-vacuity controls for
the Modules:Assemblies reader in PortalSeedLaneTest.
🚨 The cost of a seed is measured against the PORTAL host's /app, never the platform image's
This is the finding that decided #1597, and it reverses an estimate that had stood on the issue for five days.
The closure lane gives a seeded module a full standalone publish and then prunes it: everything the app publish already carries at the same relative path goes, and so does everything the shared framework provides. What survives is the module's own assembly plus its truly private deps. So the cost of a seed is not a property of the module — it is the difference between the module's closure and the app closure it is pruned against.
Markdown.Export was priced at ~17.9 MB across 18 DLLs by diffing its closure against the
platform image's /app. Measured against the portal host's /app — the one it is actually
pruned against — it is 4.6 MB:
MeshWeaver.Markdown.Export.dll |
304 KB |
| private deps kept | ClosedXML 1652 KB · SixLabors.Fonts 1109 KB · ClosedXML.Parser 188 KB · RBush 34 KB · ExcelNumberFormat 30 KB |
| version-divergent duplicates kept | Newtonsoft.Json 696 KB · System.Security.Cryptography.Pkcs 259 KB · System.Composition.AttributedModel 20 KB |
.xml / .pdb / .deps.json |
360 KB |
| total | 4.6 MB — the second-smallest of the ten seeded modules |
Nine of the ten libraries the estimate priced — DocumentFormat.OpenXml, .Framework,
HtmlAgilityPack, UglyToad.PdfPig, DocSharp.{Common,Docx,Markdown}, Markdig,
System.IO.Packaging — are already in the portal host's /app and prune away. Only the
ClosedXML stack is genuinely new. For scale, the same build's modules/ tree is ~214 MB and
MeshWeaver.Blazor.Chat alone is 43 MB.
Measuring it yourself
A plain dotnet build of a host runs the same lane into $(OutDir)modules/ (a dev dotnet run
performs no publish, so it has to), and the lane prints exactly what it kept:
rm -rf src/Memex.Portal.Distributed/bin/Release
dotnet build src/Memex.Portal.Distributed/Memex.Portal.Distributed.csproj \
-c Release -warnaserror -p:MeshWeaverRoot=$HOME/code/MeshWeaver/
du -sk src/Memex.Portal.Distributed/bin/Release/net10.0/modules/*
Look for LayoutMeshModuleClosures: … pruned N app-closure + M shared-framework duplicate(s); kept: …
in the log. On the #1597 build that read 1,398 app-closure and 155 shared-framework duplicates pruned
across all ten modules. Do not estimate this number from a different image's /app.
🚨 A comment is not a delivery
For five days Memex.Portal.Gui.csproj said Markdown.Export "ships via the modules/<Name>/ closure
layout in MeshModulesPublish.targets" while no host declared a row, and that targets file declares
no module rows at all — its @(MeshModuleClosure) is entirely caller-supplied and restored from the
caller. A reader who took the comment at face value concluded the module was already seeded and the
issue stale. The same wording sat above the AI provider packs and the PostgreSQL indexing module,
which are equally unseeded; all three now name what actually delivers them.
The hosts' .csproj rows are the only statement of this that can be falsified by the tree, and
PortalSeedLaneTest is what falsifies it. Read the hosts; never a comment.