The portal GUI is an optional reference

Memex.Portal.Gui is referenced by both portal hosts from an ItemGroup guarded by a condition, and both hosts compile with it and without it. This page records why, what the GUI-less build is, and the two rules that keep the arrangement honest.

The problem it solves

MeshWeaver.MemexTemplate — the dotnet new pack, one of the two surviving NuGet packages — is generated by tools/generate-memex-template.cs from BOTH repositories and published to public nuget.org. Six projects are copied verbatim, four of them out of this private repository.

Memex.Portal.Gui was the seventh and could not join them:

Both hosts referenced it unconditionally, so the generator refused to run at all without --with-gui: "the hosts reference it unconditionally, so a template without it cannot build". That refusal is why dotnet pack tools/MeshWeaver.MemexTemplate.Pack could not succeed and why the package stopped at 3.0.0-rc7.

A 2026-08-26 decision to include the GUI rested on the premise "the UI is public in core today, the move is what would make it private". The move happened; the premise expired. MeshWeaver#3653 settled it the other way: make the GUI optional in the hosts. Nothing private ships.

What a GUI-less build is

Not a portal, and the generated README says so rather than promising pages the package cannot contain.

Memex.Portal.Gui carries the Blazor shell and the whole portal compositionConfigureMemexServices, ConfigureMemexPortal, StartMemexApplication, the first-run wizard's catalog provider, and the App component the hosts pass to it. That composition is ~357 lines of which ~14 touch Blazor; the rest is platform (REST, gRPC-web, SignalR, MCP, auth, static assets). It lives there because its callers do, and it cannot move to core: it binds MeshWeaver.Blazor.Portal, MeshWeaver.Hosting.{Sqlite,PostgreSql,SignalR,Grpc} and MeshWeaver.InstanceSync, all of which are in this repository. So there is no arrangement in which a template restricted to public core projects composes a full portal.

What is left is what core alone composes, and that is what each host's #else branch does:

GUI present GUI absent
Blazor shell, pages, dev login yes no
Authentication schemes, MCP, SignalR, gRPC-web, InstanceSync yes no
SQLite / PostgreSQL storage backends, the first-run wizard yes no — the host stops and says which
Module ENDPOINTS yes no — see below
The mesh, module static assets, health probes, /alive yes yes

🚨 The headless pipeline maps no module endpoints, and that is a correctness choice rather than an omission. MapMeshModuleEndpoints puts every module's routes in a group that defaults to RequireAuthorization(), and MeshWeaver.Mcp's additionally demands a named policy. With no authentication schemes and no authorization middleware — both registered by ConfigureMemexServices, which is in the GUI half — mapping them trades a missing route for a 500 on the first request. A bare AddAuthentication() does not rescue it either: an authorization failure with no challenge scheme throws instead of answering 401. Choosing an authentication story for a scaffolded solution is the consumer's decision.

🚨 /alive keeps its check in BOTH columns, and that took a move. ProcessProgressProbe — the GC-saturation measurement behind the only live-tagged health check — was declared in Memex.Portal.Gui although it reads nothing but CLR counters. Left there, a GUI-less build had an empty live tag set, which is vacuously healthy: exactly the state in which two replicas pegged four cores in GC and served hung pages for three and a half hours on 2026-08-25 while passing every probe. It now lives in Memex.Portal.Distributed beside its one consumer, its test moved to Memex.Hosts.Test (compile-linked, not referenced — a ProjectReference to a host drags its whole closure into that bin, which the same csproj refuses for the storage modules), and the registration is unconditional. A liveness signal is not part of an optional half.

A host started with no Graph:Storage stops immediately with the reason. It deliberately does not carry on: without a store the ordinary startup dies four stages later in a permission-evaluator assertion that names none of this, and it cannot serve the wizard instead because the wizard's catalog and every backend it could offer are in the GUI half.

🚨 The #else branch is not a second copy of the composition. A parallel composition would drift the moment the real one changed and nothing here would notice, because nothing in this repository ever runs that branch — it exists so the public template compiles.

The two rules

1. The reference is conditional, and the constant comes from the same property. Each host derives MemexPortalGui from Exists(...) over two candidate paths — the GUI is a sibling here (src/Memex.Portal.Gui) and sits at the solution root in the generated template, one level further up from the aspire/ host — then uses that one property to add both the ProjectReference and DefineConstants MEMEX_PORTAL_GUI. Writing the condition twice is how a reference and the source that uses it start disagreeing about whether the GUI is in the build.

-p:MemexPortalGui=false forces the absent case: a global property wins over the assignment, so the GUI-less configuration can be built on a tree that HAS the GUI. That is the only honest way to verify it — deleting the directory would prove something about a tree nobody has.

2. The generator holds the hosts to it. optionalProjects names the projects the template may be generated without. Before any rewrite (after it the evidence is gone), the generator asserts that every reference to one is guarded by a Condition on the element or an ancestor, and refuses by name if it is not. It then drops the reference rather than rewriting it into a package — which is exactly what MSBuild does with that condition unsatisfied.

MemexTemplateGeneratorTest covers both halves: GeneratesWithoutTheGuiProject is the shape the pack target actually publishes (previously untested — every other case passed --with-gui), and HostReferencesTheGuiConditionally pins the csproj arrangement where a reader can act on it, rather than only inside a pack run.

Still open

The generated template has never been build-verified end to end, and #3653 does not change that. Two pre-existing defects survive in the copied hosts and are visible in any generated tree:

RewriteCsproj retargets ProjectReference and Sdk elements only; Import and Compile are untouched. Fixing those is a separate change from making the GUI optional, and neither is a reason to keep publishing private source.

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.