Graph / Compiler Layering
NodeType compilation spans four assemblies. Two of the boundaries look like they could be collapsed and neither can. This page records why, because both were re-derived wrongly at least once.
MeshWeaver.Graph the graph model: NodeTypes, layout areas, hub composition (AddGraph)
|
v
MeshWeaver.Compiler.Pipeline the MESH-ACTOR half: the compile actor, the assembly cache,
| prebuilt adoption, the park registry, bake status, the LSP service
v
MeshWeaver.Compiler the pure TOOLCHAIN: skeleton generation, source-query resolution,
| reference set, generators, Roslyn emit, framework identity
v
MeshWeaver.Graph.Contract the vocabulary the model and the pipeline share
MeshWeaver.Compiler and MeshWeaver.Graph.Contract do not reference MeshWeaver.Graph.
Why the model and the pipeline cannot simply be two projects
They were mutually recursive. Before the split an 8-file, 11,059-line strongly-connected component straddled the seam:
| pipeline side | model side |
|---|---|
MeshNodeCompilationService, NodeTypeCompilationHelpers, NodeTypeBakeStatus, NodeTypeContractHandler, PrebuiltAssemblySeeder |
MeshDataSource, MeshNodeExtensions, MeshNodeTypeSource |
MeshDataSource wires the compile handlers onto every per-node hub
(.WithHandler<DispatchCompileTrigger>(NodeTypeCompilationHelpers.HandleDispatchCompile)), and
NodeTypeCompilationHelpers called back into MeshDataSourceExtensions.TryCreateReleaseNode. In a
strongly-connected component no assignment of the files to two projects is acyclic β a cycle
that a namespace tolerates cannot be expressed across an assembly boundary at all.
Taking the transitive closure in either direction confirms it. A valid "compiler" set must be closed under predecessors one way and successors the other:
| direction | minimal compiler set | what it drags in |
|---|---|---|
Compiler -> Graph |
69 files / 35,011 lines | CreateLayoutArea, MarkdownLayoutAreas, SettingsLayoutArea, SpaceNodeType |
Graph -> Compiler |
74 files / 32,520 lines | SlideNodeType, DeckNodeType, MeshNodeCardControl, NodeIconPickerDialog |
Neither is a compiler. MeshWeaver.Graph.Contract is the structural answer: the types both
halves speak are owned by neither, so neither can close a cycle through the other β and, unlike a
closure that merely happens to be acyclic today, that property survives the next commit.
It is a contract, and it has to stay one: the NodeType declaration and its build/release state
(NodeTypeDefinition, BuildState, NodeTypeRelease, ReleaseArtifact, ServedBuildIdentity),
the node-type name literals the pipeline reads and writes (GraphNodeTypeNames), the synced-query
helpers, and ICompileFailureNotifier. No NodeType implementations and no layout areas β a
contract carrying SlideNodeType is not a contract.
π¨ Why the pipeline is NOT inside MeshWeaver.Compiler
This is the boundary that looks most collapsible and is the most costly to collapse.
MeshWeaver.Compiler is a full-MVID toolchain root β see
FrameworkBuildIdentity.ToolchainRoots. For an assembly in
that closure the whole implementation MVID feeds the framework build identity, not its public
surface. So a body-only change to any file in it β a log line, a renamed local β changes the
identity, and every NodeType on every mesh re-compiles. That is deliberate: the toolchain shapes
what gets compiled, so a body change there really can change the output with no API change.
The consequence is a size rule. Issue #1707 factored the toolchain out of MeshWeaver.Graph
precisely so the full-MVID rule pins a small, low-churn assembly.
The compile pipeline is the opposite: it is the highest-churn code in the platform. Folding it into
MeshWeaver.Compiler tripled that assembly (5,542 β 18,891 lines) and turned every pipeline commit
into a fleet-wide re-bake. It also measurably widened the closure β
FrameworkBuildIdentityTest.FullMvidClosure_IsExactly_TheKnownSet went red, with
MeshWeaver.Graph.Contract and MeshWeaver.Kernel.Hub newly inside it.
So the pipeline sits above the toolchain, in MeshWeaver.Compiler.Pipeline, and is
surface-hashed exactly as it was while it lived in MeshWeaver.Graph. The full-MVID closure is
unchanged by the split.
The rule to carry forward: adding a ProjectReference to MeshWeaver.Compiler β or moving code
into it β is a fleet-wide re-bake decision, not a refactor. FullMvidClosure_IsExactly_TheKnownSet
is the gate; when it goes red, the question is whether the toolchain genuinely needs what was just
added, never how to update the expected set.
Both new assemblies are on the content surface
In-mesh plugin source binds these types β Store/Publishing/Source/Provisioning.cs uses
NodeTypeDefinition β and in-mesh source may only reference
FrameworkBuildIdentity.ContentSurfaceAssemblies. MeshWeaver.Compiler.Pipeline and
MeshWeaver.Graph.Contract are both on that list (surface-hashed), which is what keeps that source
compiling. CanonicalList_MatchesTheTesterClosure computes the list from the csproj graph and fails
naming the drift, so it cannot silently fall out of date.
Moving a type between these assemblies
Namespaces did not change in the split β MeshWeaver.Graph and
MeshWeaver.Graph.Configuration still name types in all four assemblies. That is intentional twice
over: no in-mesh source or sibling repo sees an API change, and a type forwarder cannot rename.
Any public type that changes assembly needs [assembly: TypeForwardedTo(typeof(β¦))] left behind in
its original one. A module published earlier binds MeshWeaver.Graph!<name> by assembly-qualified
name; without the forwarder it dies with TypeLoadException on the first portal that adopts the new
platform (#2370 took down a production /mcp surface exactly this way). The split left 42
forwarders in src/MeshWeaver.Graph/TypeForwarders.cs. Use a forwarder, never a shim β a
forwarder keeps one type identity, so is/as, serialization and reference equality survive the
boundary; a re-declared compatibility type mints a second identity.
scripts/check-type-forwards.py --base <merge-base> is the gate, and it runs locally.
Where the tests live
Each suite sits beside its subject: MeshWeaver.Compiler.Pipeline.Test (the pipeline and the
toolchain) and MeshWeaver.Graph.Test (the model, the layout areas, hub composition). Assignment is
by subject, not by whether a test spins up a mesh β splitting unit from integration would have
put FrameworkBuildIdentityTest further from FrameworkBuildIdentity, not closer.