Package Mark Inheritance

A node with no icon of its own resolves its partition root's mark before falling through to the generic glyph for its type. A lesson under AgenticEngineering, a game under Chess, a doc under a store package β€” each one wore the same document chrome. Each one now wears its package's mark on its page.

The package roots already carry those marks, and the store's were deliberately aligned to one visual language β€” a bold 24Γ—24 plate with white detail β€” precisely so they read at 16 px (MeshWeaver.Plugins #588).

🚨 The BROWSER TAB is the surface this was reported from, and it is the surface this change does not finish. The resolution is in place and ResolveIconLink accepts a root, but both things that render a tab icon live in MeshWeaver.Plugins. See What this change does NOT cover below.

The chain

MeshNodeImageHelper.ResolveNodeIcon (src/MeshWeaver.Graph/), with the new step in bold:

  1. the node's own Icon β€” a content: reference resolved through the access-controlled content route, a URL, inline <svg>, an emoji
  2. a shipped glyph of that name, when the icon is a Fluent icon NAME this assembly ships
  3. the partition root's own mark β€” the same two resolutions applied to the root's Icon
  4. the NodeType default (/static/NodeTypeIcons/document.svg, code.svg, chat.svg, …)
  5. the neutral box

Steps 1–2 and 4–5 are untouched, and that ordering carries the whole policy:

Only the root's OWN mark is inherited

Step 3 reads the root's Icon β€” it does not run the root's own chain. Falling through to the root's NodeType default would dress every document in an unmarked Space as an organization, which is strictly worse than the document glyph it replaced. A root with no mark contributes nothing; the child keeps its own type.

"Partition root" is the FIRST segment, and it never inherits

PartitionRootPath returns the first path segment: Doc/Architecture/LinkPreviews inherits from Doc, not from Doc/Architecture. It returns null for a single-segment path β€” so a partition root has no ancestor to inherit from and cannot resolve itself. That is structural, not a guard bolted on afterwards.

The supplied root is also verified, not trusted: it is used only when its path really is the node's first segment. A caller that hands over the wrong node β€” the parent instead of the root, a stale frame from another page β€” gets no inheritance rather than an unrelated package's mark on someone else's page. That is the failure mode a screenshot could not catch.

The design decision: the resolver stays pure

ResolveNodeIcon is static, synchronous, and takes one MeshNode. The partition root is a different node, and reading one is an IObservable β€” a shape this signature cannot express and its callers are not built for. Two options:

make the resolver reactive pass the already-resolved root in
resolver returns IObservable<string?>; every caller becomes reactive unchanged: pure, total, unit-testable with no mesh
call sites all of them change, including Blazor render paths that cannot await only the ones that opt in
a caller with no root must still open a stream to get an answer calls the one-argument overload, exactly as today

The second was taken. An overload β€” ResolveNodeIcon(node, partitionRoot) β€” leaves the pure resolver pure and leaves every existing call site compiling and behaving identically. Pushing IObservable into a rendering helper would have made the resolution reactive in dozens of places that have a node in hand and nothing to await on.

🚨 An overload, not a fourth optional parameter. MeshNodeLayoutAreas.BuildHeader is a module-facing contract, and adding a parameter β€” default or not β€” replaces the signature every already-compiled module was built against. That is the same binary-contract argument PageIcon makes for using init properties instead of primary constructor parameters, and scripts/check-record-signatures.py is the gate that states it.

The reactive half

MeshNodeExtensions.ObservePartitionRoot(workspace, nodePath) is the seam that fetches what the resolver cannot fetch for itself:

host.Workspace.GetMeshNodeStream().CombineLatest(
        host.Hub.GetEffectivePermissions(hubPath),
        host.Workspace.ObservePartitionRoot(host.Hub.Address.Path),
        (node, permissions, partitionRoot) => …)

Four properties are load-bearing:

🚨 One fault is a STATE, not an error. Access to a node does not imply access to its partition root: an AccessAssignment can share a single node out of a partition the viewer is not a member of. A denial there means "this viewer inherits nothing", and it is classified by AreaErrorClassifier.IsExpectedUserActionFailure β€” the same predicate MeshNodeThumbnailControl.ShouldSurfaceStreamError already uses for the same reason. Nothing else is caught. A genuine infrastructure fault propagates to the page, because a decoration quietly swallowing infrastructure faults is how a broken mesh renders as a working one.

Inheritance is opt-in per SURFACE

Every call site kept compiling; the ones that pass a root were chosen, not swept.

A page identifies ONE node, so the package mark is pure gain there. The node-page header (MeshNodeLayoutAreas.Overview, ContentData) and the markdown page header (MarkdownOverviewLayoutArea.Overview) pass the root.

A LIST of siblings is not one node. In a mixed child listing the NodeType glyph is what tells a doc from a code node from a thread; flattening a rail to the same package mark repeated twelve times would take that away and give nothing back. So the nav rail's child links, cards, pickers and search results keep resolving through the one-argument overload.

That split is a policy, and it is the reason the mechanism is an overload rather than a global change to the chain: a surface that wants inheritance asks for it.

What this change does NOT cover

An inherited mark that is an official third-party mark still yields the portal's own mark in the tab: a favicon claims the tab is the site's, and that claim does not become truer by being inherited.

Verifying it

The pure chain, with no mesh, is pinned by PackageRootIconInheritanceTest; the reactive seam is pinned against a real mesh by PartitionRootStreamTest (both in test/MeshWeaver.Graph.Test). In a running portal, open a page under a marked package and read the header tile β€” a doc under Chess shows the board, not the document glyph, and Chess itself is unchanged.

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