The home surface — apps launch, spaces list

The user home has exactly two regions and one rule between them.

Region What it lists Where it comes from
Apps What you launch — a plugin, a course, a hub, Threads the viewer's own {owner}/_App/* records, one partition-local query
Content The spaces you can reach, plus your own home items namespace: … nodeType:Spacenamespace:{owner} … is:content

Nothing belongs to both regions. An app that also lists as content says the same thing twice, in two visual languages, and the reader has to work out that they are the same thing.

The root level of the content list is an ALLOW-list

UserActivityLayoutAreas's root leg is nodeType:Space — nothing else. It used to be a deny-list (-nodeType:Store/Plugin -nodeType:Store/Catalog -nodeType:User) and the difference is not cosmetic: a deny-list is only ever as complete as the last person who remembered to extend it, and the home is precisely the surface that cannot know what someone will install next.

Measured on memex.meshweaver.cloud, 2026-08-30: the home listed Posts under a "Posts Hubs" heading and Event under "Event Hubs". Both are partition roots of perfectly ordinary NodeTypes (SocialMedia/PostsHub, Marketing/EventHub) that nobody had thought to add to the list. With an allow-list a new root NodeType has to opt in, and the failure mode of forgetting is "my thing is not on the home page" rather than "everyone's home page grew a section".

A hub is not an exception to the rule — it is the rule. A publishing hub is something you launch, so it gets an app tile ({viewer}/_App/{id} with content.plugin naming the hub) and stays off the content list like every other app.

The own-partition leg is NOT type-filtered — the NODE opts out instead

Your home partition contains no Spaces at all, so filtering that leg by type would empty the list of everything you ever authored. What keeps app chrome out of it is a property of the node: ExcludeFromContext: ["content"], which is:content honours at query time.

An install lands a workspace root at {viewer}/{packageId}AppleMaps — my workspace, ThinkInStreams — my workspace. That node is the landing page of an app whose tile is on the band directly above it. Localizer.BuildHomeRoot stamps the opt-out; Localizer.ReconcileHomeRoot heals one an install touches again; AppTileRefresh.HomeRootHeals reaches the roots no install will ever touch again, on the store render per viewer and mesh-wide under RefreshAppTiles. One viewer's home listed forty of them before that landed.

🚨 A root is a root because the viewer's own INSTALL MANIFEST says the package landed there — the recorded installedPath, else the standard target. Never because of how the node is named. A node you created yourself is untouched however closely it resembles the pattern.

A tile's name and icon are COPIES, so they converge

The Apps grid paints from query rows alone — MeshNode.Name and MeshNode.Icon, no per-tile hub activation and no content read. That is what makes it fast, and it means the tile's display identity is a machine-stamped copy of its package's, stored on the record.

A copy that no longer matches its source is exactly what a migration exists to fix, so AppTileRefresh converges both fields to the package's, with one explicit escape hatch each:

field converges to the viewer's own is kept when
Icon Localizer.HomeRootIcon(package.Icon) content.customIcon == true
Name package.Name content.customName == true

The escape hatch is always declared, never inferred. "It looks hand-edited" is not a signal a migration can act on.

Two things this must not do, both learned the hard way:

The accretion, and why the fallback strips

CourseAppTile reads its base back from tileBaseName and falls back to the record's current name only when that key is missing. That fallback is right exactly once — the first stamp. If the key is ever lost (a writer replaces the content object without round-tripping it), the next stamp reads an already-stamped name as its base and appends to it, and every stamp after that appends again. On memex one tile had reached:

AdvancedBusinessRules · 1/17 · 1/14 · 1/14 · 1/14 · 1/14 · 1/14 · 1/14 · 1/14 · 1/14

CourseAppTile.Unstamped therefore strips whole trailing · n/m groups in that fallback: a lost base must cost at most the tail it can no longer explain, never a longer name. It strips only whole groups, so no ordinary name is touched; a package legitimately named Fractions · 1/2 loses its tail only in the window before a store render, because AppTileRefresh.Renamed writes the base from the package's own name and is authoritative over the heuristic.

Where each rule lives

Rule Code
Root level of the content list is Spaces core MeshWeaver.Graph/UserActivityLayoutAreas.RootTypeFilter
A workspace root is not content Store/Installer/Source/Localizer.{BuildHomeRoot,ReconcileHomeRoot,WithoutContentContext}
…and the roots already out there heal Store/Installer/Source/AppTileRefresh.HomeRootHeals
A tile's name/icon converge to its package Store/Installer/Source/AppTileRefresh.{Renamed,Refreshed}
A learner's progress rides on the same name Edu/Module/Source/CourseAppTile
The mesh-wide migration Store/MaintenanceRefreshAppTiles
Reconnecting…
The server was updated. Reloading the page to pick up the latest version.