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:Assembliesthe 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.

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.