Mount-Relative Source Queries

A NodeType names the Code nodes it compiles with a list of queries (NodeTypeDefinition.Sources / .Tests, expanded by CodeQueryResolver). Two spellings behave very differently, and the difference only shows up once the content is served from somewhere other than where it was authored:

Entry Resolves to Survives a mount prefix?
namespace:Source scope:subtree (the default) {$self}/Sourcerebased on the owning type's path Yes$self already carries the prefix
shared=@Other/Type/Source path:Other/Type/Source + namespace:Other/Type/Source scope:subtree Not on its own — the value is absolute

A cross-type source reference is authored MOUNT-RELATIVE, exactly like an @@ include. The resolver therefore emits the mount-anchored spelling alongside the authored one, and the node is found from whichever mount it is served.

The defect this closes

samples/Graph/Data/Northwind/Product.json declares:

"sources": [
  "namespace:Source scope:subtree",
  "shared=@Northwind/AnalyticsCatalog/Source/Supplier",
  "shared=@Northwind/AnalyticsCatalog/Source/Category"
]

Correct at a root mount — in the Monolith the type is Northwind/Product. In a statically-imported partition the same nodes are served under a prefix, so the type is MeshWeaver/samples/Graph/Data/Northwind/Product and the sibling entity sources are at MeshWeaver/samples/Graph/Data/Northwind/AnalyticsCatalog/Source/…. Resolved verbatim, the two shared= entries matched nothing.

🚨 And nothing said so. The type's own namespace:Source scope:subtree is rebased and did match its two files, so the merged source set was non-empty — which means neither of the two mechanisms that exist to catch a short set could fire:

Roslyn was handed a set short of the sibling entity sources and reported exactly what it saw:

Compilation failed for 'MeshWeaver/samples/Graph/Data/Northwind/Product':
CS0246 Error (line 45): The type or namespace name 'Supplier' could not be found …
CS0246 Error (line 49): The type or namespace name 'Category' could not be found …
--- Source discovery ---
Matched Code nodes (2)

A completely genuine-looking verdict about code that is fine, on a mesh whose content is entirely present. Measured on memex-cloud 2026-09-19/20 (issue #4813): every pod that attempted the compile failed the same way, and it does not self-heal — there is nothing to wait for.

The rule, and why it is one rule

CodeQueryResolver.AnchorToMount rewrites the path: / namespace: value of an expanded query by calling NodeCompileShaping.AnchorIncludePath — the function that already closed the identical failure for @@ include paths, where an unresolved include is left verbatim in the source so Roslyn parses the @@ line and reports on path segments as though they were symbols.

Calling that function rather than writing a second copy of the rule is deliberate: a source query and an @@ include that name the same node must not disagree about where it lives. The anchor is the deepest occurrence of the value's first segment in the owning type's path — the most local reading — so Northwind/AnalyticsCatalog/Source/Supplier read from MeshWeaver/samples/Graph/Data/Northwind/Product anchors to MeshWeaver/samples/Graph/Data/Northwind/AnalyticsCatalog/Source/Supplier.

Because Expand is the single funnel every consumer goes through — the runtime compile (NodeSources.GetSources), the tree bake (NodeSet.ResolveSources), the Configuration side menu, and SourceCoverage — producer and consumer cannot fork on which files count. That invariant is what NodeTypeSourceFingerprint depends on, so the fix had to land there and nowhere else.

The authored spelling is kept, never replaced — and that is a UNION, not a fallback

Both queries are emitted, because which one resolves is a property of the mount, not of the entry, and nothing offline can know which.

🚨 It is a union, and the difference is worth stating. Every consumer treats the query list as a union — workspace.GetQuery unions its queries, NodeSet.ResolveSources adds each query's matches — so there is no precedence step that makes the anchored spelling win. ResolveCodeIncludes genuinely can try anchored first and fall back, but only because it reads one node per include; a set-valued query cannot express the same thing by adding a list member.

What the union costs, stated rather than assumed:

What is deliberately NOT anchored

Two guards, and both are load-bearing:

  1. A value already rooted at this mount is never touched — its first path segment equals the owning type's. This is what stops a double prefix when the mount root repeats further down: for selfPath = Space/Source/Nested/Space/Type, the already-rebased own-source value Space/Source/Nested/Space/Type/Source would otherwise anchor on the second Space and come out as Space/Source/Nested/Space/Source/Nested/Space/Type/Source. The correct query would still be emitted, but a query naming nothing is not free: it is one more leg in the compile's source probe, and a leg that errors makes the whole snapshot unestablished and refuses the compile.
  2. A genuinely cross-partition reference stays verbatimshared=@Store/Core/Source read from rbuergi/OperationRequest has no Store segment to anchor to. Rewriting it would turn a working reference into a missing one.

Between them, a root-mounted type keeps exactly the queries it had — the Monolith and every unprefixed deployment are unchanged, which is the control on the other side of the rule.

A group whose entries anchor keeps a usable CodeQueryGroup.BaseNamespace: a root and its anchored form are the same root resolved two ways, so the source listing still shows files relative to it rather than falling back to full paths. 🚨 That fold is the actual anchoring relationship (candidate == AnchorIncludePath(authored, selfPath)), never "one root is a suffix of the other" — a suffix test folds unrelated roots, so a group holding @A/B and @B would report a single base and relativise a genuinely mixed group against it.

🚨 Four reasons a NodeType reports CS0246, and they need different answers

A CS0246 on an in-mesh type is not one defect. Reading the source-discovery block under the diagnostics is what tells them apart, and the wrong reading sends the reader to module surfaces that never carried the symbol.

What the discovery block says What it is The answer
Matched Code nodes (0) The source set is gone — a retired type whose sources were pruned, or an orphan the sync never removed Finish the retirement: Retiring a NodeType. No recompile will change it
Matched nodes, and a declared entry reports MISSING SOURCES One declared query answered and matched nothing — the SourceCoverage signal Restore the source nodes, or correct the query
Matched nodes, entries all satisfied, and the mesh is served from a prefix This page — a mount-relative reference resolved against the wrong root Nothing to do; the resolver now asks for both spellings
Matched nodes, and the same failure across many types in one burst that goes green minutes later A compile that read a source set mid-edit. Both re-arms are change-driven and already in place: NodeTypeCompileParkRegistry.ShouldRetryForSourceChange (a source arrival after the park) and SourcesMovedSince (the consumed set had already moved when the verdict was written) Nothing. The green recompile is the mechanism working — do not add a retry or a delay

🚨 A fifth shape is not a CS0246 at all: a held type (NodeTypeDefinition.PendingRetirement) whose shared library has moved on. Its own Source/Test nodes are kept while instances remain, so a rename in the shared partition breaks the held type's tests and parks the type although its assembly is still serving. The verdict is real, and the fix belongs to the migration, not to the compiler.

Verifying

The sweep is search 'nodeType:NodeType content.compilationStatus:Error partitions:all' limit:200 and its answer means nothing without a denominator — it runs as you and is RLS-filtered, so a type parked in a partition you hold no grant on is silently not counted. State "0 of N over M readable partitions", taking N and M from the same instrument. Full rules, and why a Not found from outside that denominator closes nothing: Search Coverage and Refusal.

Offline, the rule is a pure function over strings and is pinned by MountRelativeSourceQueryTest — including a case on each side of the change (mirrored mount rewrites, root mount does not) and a negative control that a genuinely absent sibling is still reported as a missing source.

See also