File formats — how a slide file is read
A .md file whose front matter declares a slide NodeType is read by this pack, not by the
platform. MeshWeaver.Publish (the pack's compiled half) contributes a SlideFileParser to the
shared file-format parser chain, and the chain tries every module-contributed parser before the
catch-all MarkdownFileParser — the same lane the AI engine's agent and skill parsers already use.
.md → [ contributed: AgentFileParser · SkillFileParser · SlideFileParser ] → MarkdownFileParser
(accepts everything)
This is phase 3 of the declarative import/export programme (Doc/Architecture/DeclarativeImportExport,
Systemorph/MeshWeaver#1580, MeshWeaver.Plugins#961). Before it, the platform's catch-all parser
carried one compiled if for slides — the last per-type branch left in the import path.
What the parser owns, and what it delegates
It owns exactly one thing: the content shape. The stage body, the Notes (spoken script) and
the Background (stage CSS) become a SlideContent instead of the generic MarkdownContent that
has nowhere to put the last two.
Everything else — the id and namespace derived from the path, the name, category, description, icon
resolution, state, order, the persisted version counter, the pre-rendered HTML, the BOM strip and
the defensive front-matter extractor — is delegated to MarkdownFileParser, never copied. Those
rules are why a markdown node gets its identity, they change, and a second copy of them would drift
in silence: a slide would start deriving a different id, or quietly lose a field, with nothing red
anywhere. SlideFileParserTest pins the delegation field by field.
The write direction is deliberately still core's. CanSerialize returns false, so
MarkdownFileParser keeps emitting slide files exactly as it does today. Claiming the write would
make this pack responsible for reproducing core's front-matter emission byte for byte, and anything
short of exact rewrites every slide file in every synced repo on the next sync-back — a diff nobody
asked for, from a change whose entire purpose is that nothing changes. The write direction has its
own, better answer to build, and it is core's: dispatch on the live content object through an
IFrontMatterRoundTrip interface rather than on a type name, which needs no bootstrapping because
the instance exists at write time.
🚨 The decision: import ordering is NOT guaranteed, and must never be relied on
The open question this change had to settle (DeclarativeImportExport → "What blocks the import
half", condition 1): a cold-boot import — a bake, a first git-sync — can parse a .md before the
NodeType that owns it exists. The two candidate answers were make a miss impossible (guarantee
types are registered before any content of them is parsed) or accept the loss and make it loud.
Decided: neither. The premise is removed instead.
A file-format parser answers from the FILE ALONE — its front matter and its path. It consults no NodeType registry, no compiled assembly and no hub state. Completing the typing is the read seam's job, not the parser's.
Why
Ordering cannot be guaranteed cheaply, because parsing is how nodes get created. The import that parses
Deck/Intro.mdis the same import that creates thePublish/SlideNodeType node fromSlide/index.json. "Types before content" therefore means a two-pass import with a compile barrier per package, running before storage can answer a single read — theProvisionPlanmachinery, hoisted into the boot path. That trades a narrow typing gap for serialising every partition's compile at startup.It is not needed, because the parse-time shape does not have to be final. It only has to be lossless and non-contradicting.
IMeshContentTypeRegistry.TryRecoverForNodeTypere-materializes content into the NodeType's own CLR type on first activation, and itsDiscriminatorAdmitsguard matches on the short type name — so the compiledSlideContentthis parser emits is admitted into the in-meshSlideContentcompiled fromSlide/Source, whether or not that type existed when the file was read. The seam already exists and already runs.A parser that consulted the registry would be nondeterministic. The same file would parse to a different content shape depending on whether the owning type had compiled yet in this process. That is the "same input, different results" failure the discriminator-ambiguity fix (Systemorph/MeshWeaver#1299) already refused once, and it is strictly worse than the branch it would replace.
The move itself closes the gap the question was about. This is the part worth keeping. A compiled branch in core hosting is present on every host whether or not slides are installed, and absent from none — the branch and the node type it serves have independent lifetimes, which is what made "was the type there when the file was parsed?" a real question with a silent wrong answer. A contributed parser ships in the same package as the node type it serves, so the two now have the same lifetime: a host that can hold slide nodes has this parser, and a host without the Publish module has no slide nodes for it to mis-parse. The ordering hazard was a symptom of the branch living in the wrong repository.
What this forbids
- Resolving a NodeType's content type at parse time (the
TryResolveByNodeType-in-the-parser route the design doc floated) — see 3. - A content-shape rule in place of a type rule ("front matter has
NotesorBackground⇒ slide"). It needs no registry and no ordering, and it silently empties the 3 real slides in 235 that carry neither key.ASlideWithNeitherNotesNorBackground_StillBecomesSlideContentpins that those files stay slides. - Switching the generic fallback to untyped content to "preserve extras". That changes the stored
shape of every markdown node on every mesh (621
Markdown+ 506Edu/Lesson+ 396Edu/Exercise+ …) to solve a problem the contributed parser no longer has.
What is still true, and where the residual loss is
A .md declaring a NodeType whose package is not installed still reaches the catch-all and
imports as a plain Markdown node. That is now the honest answer rather than a gap: the node type is
not installed either, so there is nothing that could render the richer shape. It stops being silent
the moment the package is installed, because the file is re-read by its own parser on the next sync.
Cross-repo sequence
The parser landed here first, deliberately, and is a no-op until core's branch is deleted:
| # | Change | Repo | State |
|---|---|---|---|
| 1 | SlideFileParser contributed by MeshWeaver.Publish |
this repo | done (#961) |
| 2 | Delete MarkdownFileParser.IsSlideNodeType's parse branch (:215, :270) |
core | follows, after this module ships |
| 3 | IFrontMatterRoundTrip for the serialize half; delete the :295 read |
core | independent, no design blocker |
| 4 | SlideContent / SlideNodeType leave the platform |
core | blocked, and not by the parser |
While core still carries its branch, both parsers build the same object, so either order is safe and
there is never a window in which a slide is unparseable. TheStageBody_IsTheCatchAllsOwnBody is
what keeps that true across the deletion: core answers a slide file with SlideContent today and
with MarkdownContent afterwards, and MarkdownContent.Parse assigns its input verbatim, so the
body this parser reads is identical either way.
🚨 Row 4 corrects the design doc. It records that after phase 3 "the parser is the last one",
which is what would let SlideContent/SlideNodeType leave core. Measured on core main
(2026-08-31), it is not: SlideNodeType.Matches is still read by DeckSlidesCache (the export
templates' slide resolver), and the SlideContent record is still needed by BundleWriter,
FluentIcons and its own typeRegistry.WithType registration — which is also what gives the
record its short-name $type and therefore makes the read-seam recovery above work. Those readers
have to go first; deleting the parser branch does not unblock the record.