Docs Follow The Functionality
A documentation page belongs in the module it documents. MCP docs with the MCP module, AI docs with the AI engine, GUI docs with the GUI. Core's own mechanisms β the compiler, the mesh, node-type compilation, module versioning β keep their docs in core, because core is the functionality they document.
This is not a new rule. It is the rule already written down for skills, applied to the other kind of prose the mesh serves:
π¨ Skills are DECENTRAL β they live in the module they are about β¦ skills are injected by the partition of the NodeType and of the data element in view β placement IS the targeting.
Documentation has the same shape. A doc page is a Markdown node in a partition, found by
search namespace:Doc scope:descendants <topic> and by the vector index. Put it in the module's
partition and it is found by anyone working in that module, for the same reason a skill is.
Measured against Systemorph/MeshWeaver@f8e208567 and Systemorph/MeshWeaver.Plugins@7e0750c2
(2026-09-01). Re-measure before acting; the numbers move daily.
The starting position, corrected
src/MeshWeaver.Documentation/Data/ holds 996 files, not 985 β the tree grew by 11 while the
survey ran, which is itself the point: this is a live stream, not an archive.
| Folder | Files | .md |
.json |
.cs |
Executable blocks | Inbound /Doc/ links |
|---|---|---|---|---|---|---|
WhatsNew |
709 | 709 | 0 | 7 | ||
Architecture |
170 | 163 | 3 | 4 | 17 (in 7 pages) | 838 |
DataMesh |
53 | 32 | 14 | 7 | 27 (in 17 pages) | 77 |
GUI |
42 | 42 | 84 (in 26 pages) | 113 | ||
AI |
13 | 13 | 1 (in 1 page) | 63 | ||
| root pages | 6 | 6 | 3 | 2 | ||
PreReleaseNotes / ReleaseNotes |
3 | 3 | 0 | 0 |
The executable-block and inbound-link columns are the cost columns, and they do not track each other. They are what makes one folder nearly free to move and another prohibitive.
The repo has 91 .csproj (36 src/, 44 test/, 5 tools/, 3 memex/, 2 samples/, 1
deploy/) β the "79 under src/" in the original framing was the src/+test/ total, and only
src/ counts as the platform surface.
π¨ First: Data/ is the SOURCE, not a second copy
This had to be settled before anything else, because if the mesh held an independently-authored copy then drift would outrank placement. It does not.
The tree is compiled into MeshWeaver.Documentation.dll as embedded resources and reaches a mesh one
of two ways β the in-memory EmbeddedResourceStorageAdapter partition, or, when Doc is
DB-synced, DocumentationStaticRepoSource materialising the same embedded resources into Postgres
on boot (see Static Repo Import). Both read the same bytes. And the partition
is authored read-only: Doc/_Policy sets Create/Update/Delete = false, so a live edit is not
merely discouraged, it is refused.
Measured against memex.meshweaver.cloud (running 0a1eabdc0, 12 commits behind main):
| Disk | Mesh | On mesh but not on disk | |
|---|---|---|---|
Doc/WhatsNew |
709 | 702 | 0 |
Doc/Architecture (top level) |
161 | 159 | 0 |
Every difference is disk-ahead β the 7 WhatsNew entries and 2 Architecture pages dated after the deployed image was built. Zero mesh-only nodes in either subtree. The mesh copy is derived and lagging, never divergent.
π¨ The finding that matters is the lag, not the copy. A doc fix reaches a mesh only through a
core image roll, because the bytes ride inside MeshWeaver.Documentation.dll and that dll ships
in the portal image. That is a strictly slower channel than the one every module already uses, and it
is an argument for module-shipped docs rather than against them.
The destination map
AI (13 files) β the AI package. The clearest move in the tree.
Every symbol these pages name resolves in MeshWeaver.AI and its provider modules, and not one of
them resolves in core: ChatCommands 11-0, ProviderConfiguration 11-0, ModelTiers 2-0,
AgentDesign 2-0, ModelProviderSetup 12-4, ModelProviderSettings 14-6. TeamsBot belongs with
Teams, ExecutiveAssistant with Mail, McpAuthentication with Mcp.
The AI engine left the platform repo with its built-in agent and skill roster. Its documentation did not travel with it. That is the same half-finished-move shape the carving survey names β and the cost of finishing it here is the lowest in the tree: 1 executable block across all 13 pages, so almost nothing to re-gate.
WhatsNew (709 files) β stays, and the mechanism is already decentral.
This one looked like the big prize and is not. Doc/WhatsNew is deliberately one fleet-wide feed,
and a satellite repo already files into it without a cross-repo PR: drop
WhatsNew/<date>-<slug>.md with nodeType: WhatsNew in its own tree. The portal's What's New tab
runs two queries β path:Doc/WhatsNew scope:children for the platform's own entries and a
node-type query for everyone else's β and de-duplicates by path. WhatsNew/ is a reserved
non-package directory in eight scripts across both repos.
Measured: of the 709 entries, 60 (8.5 %) name a package that lives outside core and 105 (14.8 %) mention plugins at all. Those are history. New ones already go to the right place.
π¨ What the survey found instead is that the decentral lane is UNGATED. Core's 709 entries are
pinned by WhatsNewEntryIntegrityTest (ISO-dated filename; Name, Category β {Feature, Fix},
Description, Order = -yyyyMMdd). Nothing runs that check over a satellite's WhatsNew/. Three of
the four entries in the plugins repo today carry title:/category:/date: and no Description
and no Order β the exact malformed shape the core test was written to catch, in the feed, right
now, invisible. Rendering degrades quietly: no Order means the day sorts alphabetically by title.
The fix is not a move. It is to run the same pin on the satellite lane β the ratchet-in-every-repo pattern of the CI policy β so a note authored anywhere is judged everywhere.
GUI (42 files) β stays in core.
This is the counter-result the axis produces, and it is worth stating plainly because "the Blazor
renderers live in plugins" makes the opposite look obvious. It is wrong. A GUI doc documents the
control, and controls are declared in MeshWeaver.Layout, which is core. Of 42 pages, 30 name
zero plugins-resident symbols; the remainder are dominated by core symbols
(Data Binding: 19 core to 3). The renderer is an implementation of the
control contract, not its subject.
The cost side agrees, hard: GUI carries 84 of the tree's 132 executable blocks, in 26 of its 42
pages. Those blocks are compiled and executed against a real kernel by DocExecutableBlocksTest
and DocumentationCodeBlockCompilationTest, and browser-rendered by DocExamplesRenderTest. A
GUI page is not prose; it is gated content. Moving it means moving its gate.
Two genuine exceptions: React/* (7 pages) belongs with the React client that already moved, and
BlazorCustomControls with MeshWeaver.Blazor.Radzen.
DataMesh (53 files) β stays in core, with two departures.
Symbol residence is overwhelmingly MeshWeaver.Graph, MeshWeaver.Mesh.Contract and
MeshWeaver.Data β nodes, node types, the unified path, query syntax, CRUD. This is the platform's
own data model. 27 executable blocks in 17 pages.
The departures: CollaborativeEditing (plus its 6 _Comment fixtures) documents
MeshWeaver.Markdown.Collaboration, delivered as the Essentials package; and SocialMedia/ β a
page plus two NodeTypes with 5 Source/*.cs β is a worked example whose subject has a whole
satellite repo. Note carefully what SocialMedia teaches: how to author a NodeType. That is a core
mechanism wearing a domain costume, so the honest split is to keep the lesson and re-cast the example,
not to relocate the page.
Architecture (170 files) β splits, and does not split by folder.
The largest and least homogeneous group. Classified by resolving each page's backticked symbols against both repos' declaration index, then hand-checking:
Move β the subject lives in a plugins module:
| Page | Destination | Signal (plugins-only / core-only symbols) |
|---|---|---|
ModelProviders, AgenticAI, AgentFrameworkStores, ThreadOperations, ThreadExecutionStreaming |
AI |
8-0, 2-0, 9-1, 16-7, 4-3 |
PixelFaithfulExport |
Export |
15-4 |
LogWatchTriage |
Observability |
9-6 |
CentralizedSpeech, OnDeviceVoice |
Voice |
4-2, weak |
Notifications, EmailIngestionAndNotifications |
Notifications, Mail |
1-4, 8-9 |
InstanceSync, CrossInstanceMirror |
InstanceSync |
3-3, weak |
DeploymentAKS, ReleaseGates, Instances, InstanceIdentityAndSetup |
Hosting |
2-4, 1-8, weak |
LinkPreviews |
OgCard |
weak β see the caveat below |
Stay β core is the functionality: NodeTypeCompilation (43 core symbols to 4),
Plugin Packaging (23-2), Plugin Registry (19-1),
Plugin Build Contract (7-0), Module Versioning (2-0),
Module Build Architecture (1-0), and the whole reactive/storage/security
spine. The plugin system is core β MeshWeaver.PluginCatalog is 21 245 lines of it β so the pages
that document it stay, even though every plugin they describe lives elsewhere. Documenting a
consumer is not owning it.
π¨ The symbol signal is a lead, never a verdict. It reads backticked identifiers, so a
narrative page with few code names (OnDeviceVoice, LinkPreviews, MenuAsData) scores near zero
in both columns and must be judged by subject. It also over-counts MeshWeaver.AI, which declares
enough types to appear in pages that have nothing to do with it. And it is blind in exactly the way
the carving survey records: "a reference may be dead weight, and a
live consumer may have no reference at all."
The mechanism: MeshDocs/, and why it is easier than skills were
MeshWeaver.Plugins#1060 establishes the pattern for skills: a module embeds
MeshSkills/{Package}/{id}.md, an IStaticNodeProvider reads the resource names of the
assemblies the mesh installed, and the node lands at {Package}/Skill/{id}. One csproj line, no
reference to the AI engine, and β the whole point β the skill travels inside the artifact it belongs
to, so it cannot be delivered separately and go missing.
The same shape carries documentation, and every ingredient is already core:
| Ingredient | Home |
|---|---|
InstalledModuleAssembly |
MeshWeaver.Mesh.Contract |
IStaticNodeProvider |
MeshWeaver.Mesh.Services |
MarkdownFileParser / FileFormatParserRegistry |
MeshWeaver.Hosting.Persistence.Parsers |
the Markdown node type |
core |
So a ModuleDocProvider reading MeshDocs/{Package}/β¦ into {Package}/Doc/β¦ belongs in
MeshWeaver.Hosting and needs no AI dependency at all β strictly simpler than the skill case,
which had to live in MeshWeaver.AI because SkillMarkdown and SkillNodeType do.
Four things are genuinely different and must be designed, not copied:
- Depth. A skill id is one segment;
ModuleSkillProviderrejects anything else outright (parts.Length != 2). Docs nest three deep. The split becomes a path walk β and every folder level needs a backing node file, the wayArchitecture.mdbacksArchitecture/, or the folder is nothing at all in the mesh. - Page assets. A doc page's inline embeds resolve through a per-node embedded content collection
that
AddDocumentationwires by testing for theDoc/prefix, plus the public/static/DocContentbuild-asset mount. Generalising that per module is the fiddliest part β but only 6 pages and 13 embeds use it today, so it can follow rather than block. - One authored home.
ModuleSkillProviderrecords the constraint: a statically served path cannot also hold a durable row. A module-shipped doc must therefore not also exist as a node file in that package's repo folder. For the 31 of 53 plugins packages that are assembly-backed, that is a rule authors must be told; for the 22 pure node packages,MeshDocs/does not apply at all and the page is simply a node file. - The gates must travel. This is the one that makes a cheap move expensive, below.
What is load-bearing where it is
The counter-list, and the half that stops a good idea becoming a bad migration.
1. The doc tree is gated content, and the gate is core's. doc-gate ("Doc content compiles and
runs") is a required job: it stages Data/ as a Doc/ package root, bakes it with mw-plugin-test,
then installs, compiles, renders and runs its Tests areas. .github/doc-gate.allow is empty β
zero waivers. On top of that sit DocumentationCodeBlockCompilationTest and
DocExecutableBlocksTest (which carries a coverage ratchet), DocExamplesRenderTest,
ReactDocViewsTest β the last with a hardcoded table of ~20 Doc/β¦ paths and their expected
rendered text β and WhatsNewEntryIntegrityTest. A page that moves and leaves its gate behind is
not "moved", it is unpinned, and nothing goes red to say so.
2. stage-doc-gate.sh is shared with CD on purpose. The PR gate and main-cd's publish-bake
stage the same tree through one script, "so the bake publishes exactly what the gate judged." Any
split of the tree splits that guarantee and must re-establish it on both sides
(see CI Content Bake).
3. Eleven .cs and seventeen .json under Data/ are invisible to dotnet build β the csproj
removes them from compilation deliberately. doc-gate is the only thing that type-checks them, and
MeshWeaver.Layout.Test splices two of them into a test assembly by hand.
4. Ten test projects and one production project reference MeshWeaver.Documentation. The single
non-test consumer in core is memex/Memex.Portal.Shared, which is also the sole caller of
AddDocumentation() and the sole registrar of DocumentationStaticRepoSource. The rest is fixtures:
MeshWeaver.Content.Test (7 wiring tests), Hosting.Orleans.TestBase β so every Orleans test
transitively loads the Doc partition β MarkdownPipelineBenchmarkTest (whose corpus is
Data/Architecture), and PythonDemo.Test, which copies one specific page. A doc that is also a
fixture cannot simply follow its module.
5. Links. 1 527 internal links, 1 094 of them absolute /Doc/<Section>/β¦; 226 of the 996 files
cross-link each other by node path. Outside the tree, 315 core files (670 occurrences) name a
Doc/β¦ node path and 48 files name the filesystem path β including all 17 core skill files and
~25 links in AGENTS.md. The two are decoupled: preserving the Doc partition prefix costs 48
edits; changing it costs 363. Where a page must change path, Moved-Node Redirects
is the mitigation that already exists.
6. π¨ The dependency runs the WRONG WAY today, and a naive move would deepen it. The link and
embed integrity gates for core's documentation live in the plugins repo
(src/MeshWeaver.AI.Test/DocumentationLinkIntegrityTest.cs and DocumentationEmbedIntegrityTest.cs,
both in namespace MeshWeaver.Documentation.Test, both reading
MeshWeaver.Documentation.Data. resources). So does DocumentationBackfill. Three production
plugins projects β Memex.Portal.Gui, Memex.LocalMesh, Memex.Database.Migration β reference
core's MeshWeaver.Documentation.csproj across the repo boundary through $(MeshWeaverRoot).
Against the standing rule that each repo should only use itself, this is the real defect the survey found. Moving AI docs to the AI package removes a cross-repo dependency; moving core's docs out of core would only reverse one β and would put a required core gate behind a sibling checkout, which disqualifies it outright.
Beyond docs: the content still in core
The doc tree is not the largest content in the platform repo.
samples/ β 988 files, 18.9 % of the tree, and CD already refuses to ship it. samples/Graph is
a pure node-content tree (953 files; 112 .cs, 556 .json) whose csproj sets ExcludeFromBuild and
which nothing references by project name β it is not even in MeshWeaver.slnx. main-cd.yml
excludes it from the published bake with the measurement recorded: "7 packages / 24 assemblies whose
bundles NO deployment can adopt." Its README already names its consumer as living in the other repo.
It is anchored to core by 13 test projects and one PR gate stage, nothing else.
π¨ And it contains the same split this page is about. samples/Graph/Data/Northwind is 276
files of node-native Northwind content in core β while the plugins repo's Northwind package is a
three-file shell (index.json, Guide.md, manifest.lock) pointing at
MeshWeaver.Northwind.Application. One product, two repos, two different artifacts, no gate that can
see both. FutuRe (138 files, reinsurance) and PensionFund (72) have satellites of their own.
The concrete cost of that content being able to break a framework build is already on record: a
€ entity inside an XML doc comment in
samples/Graph/Data/FutuRe/Currency/Source/Currency.cs β malformed XML (CS1570), warnings-as-errors,
compiled by two independent lanes, and not covered by samples-gate.allow.
clients/voice-gateway β 35 files with no CI job at all, excluded from CD change detection, and
the workflow says so in a comment. A Home Assistant / Ollama voice satellite sitting in the framework
repo, unbuilt and untested.
The plan, ranked by value over cost
| # | Move | Files | Removes a dependency? | Cost |
|---|---|---|---|---|
| 1 | Gate the satellite WhatsNew lane |
0 moved | no β closes a hole | one test, ratcheted per repo. Not a move at all, and it fixes a live defect |
| 2 | Doc/AI β the AI package |
13 | yes β the docs join the engine | 1 executable block; 63 inbound links need redirects |
| 3 | Build ModuleDocProvider in MeshWeaver.Hosting |
β | enables 2, 4 | depth, page assets, and porting the doc gate to modules |
| 4 | The ~20 module-owned Architecture pages |
~20 | yes | per-page; 838 inbound links, so redirects are mandatory |
| 5 | samples/ β plugins, Northwind first |
953 | yes β closes the two-repo split | 13 test projects; one PR gate stage must move with it |
| 6 | clients/voice-gateway |
35 | yes | none measurable β nothing builds it |
Stays in core, and the reason is the rule, not inertia: Architecture's reactive, storage,
security, node-type, plugin-system and release pages; all of DataMesh bar two; all of GUI bar
React/*; every WhatsNew entry already written. Core is the functionality those pages document.
Two defects this survey found
A gate that judges one repo's content from another repo. DocumentationLinkIntegrityTest and
DocumentationEmbedIntegrityTest pin core's doc tree from inside MeshWeaver.Plugins. Core's own CI
cannot run them, so an ordinary doc edit here can only be judged by a sibling repo's suite β the
exact shape recorded in Carving Projects Out Of Core as the thing not
to add instances of. It was a reasonable consequence of the AI engine's move and it is now load-bearing
in the wrong place.
A decentral lane with no ratchet. The satellite WhatsNew contract shipped, works, and is
unpinned; three of four entries authored through it are malformed against the very rules the core
test enforces, and the feed renders them in the wrong order rather than failing. A gate copied but
never armed is worse than no gate, because the wall of ticks now includes it.
Related
- Carving Projects Out Of Core β the SOURCE-move / MODULE-move distinction, and what pins each remaining project
- Authoring Documentation β the link rules, frontmatter and executable blocks any destination must keep honouring
- Static Repo Import β how the embedded tree becomes partition rows
- Moved-Node Redirects β keeping 1 094 absolute links alive across a move
- Plugins Β· Module Build Architecture