Skip to content

Modularize the export layer: node types declare their export composers — no compiled type gates #1576

Closed

Description

@rbuergi

Problem

The export subsystem is hard-coded per node type, in compiled code:

  • DeckExportMenuProvider / MarkdownExportMenuProvider are separate providers, each gating on a NodeType comparison (Deck / Markdown) — a new exportable type means a new compiled provider.
  • ExportPdf.csx / ExportHtml.csx branch if (isDeck) to pick the composition strategy (slide chapters vs single document).
  • ExportDocumentLayoutArea decides pixel-fidelity availability off the same compiled type check.

The suffix-aware DeckNodeType.Matches/SlideNodeType.Matches gates (follow-up to the Publish-pack extraction) let plugin-typed decks/slides through these compiled gates — but they are a bridge, not the architecture.

Direction (composition-first, same pattern as layout areas)

Exportability becomes a DECLARATION on the node type's hub configuration, contributed by the type's own configuration lambda (which lives in plugin content for plugin types):

  1. A compiled public surface config.WithExportComposer(descriptor) — format(s), menu label key, fidelity options, and the composition strategy (how this type's content becomes an export document: e.g. deck → one chapter per slide in deck order).
  2. ONE generic export menu provider that renders entries from the declarations on the node's hub config — DeckExportMenuProvider and MarkdownExportMenuProvider collapse into it and are deleted.
  3. The export templates dispatch through the declared composer instead of if (isDeck) — the deck branch moves into the Publish pack's declaration.
  4. The suffix gates from the bridge PR are then deleted.

Design constraint: the hub config freezes at hub build, so declaration changes ride the existing generation-bump → hub-recycle mechanism (same edge as the WS7 menu/composition design — solve once, use for both).

Part of the composition-first program (WS7): a deployment = base runtime + plugin composition; the export if-chains are one of the last compiled per-type switches.

🤖 Generated with Claude Code

Activity

  1. rbuergi commented on Aug 21, 2026

    @rbuergi
    ContributorAuthor

    Verdict: PARTLY DONE on main → finished by the two PRs below

    Verified against origin/main (not PR titles). Phase 1 (#1594) landed the declaration and the one generic menu provider. Three compiled type gates survived it, and the acceptance list's item 4 was still open:

    item on main today
    1. config.WithExportComposer(descriptor) shipped as WithExport(ExportDeclaration) — src/MeshWeaver.Graph/ExportDeclaration.cs ✅
    2. ONE generic menu provider; per-type providers deleted ExportMenuProvider; MarkdownExportMenuProvider/DeckExportMenuProvider gone ✅
    3. templates dispatch through the declared composer ❌ ExportPdf.csx:74 / ExportHtml.csx:67 still DeckNodeType.Matches(rootNode.NodeType) ✗
    4. the bridge's suffix gates deleted ❌ ExportDeclaration.Resolve still carried the DeckNodeType.Matches fallback; Publish/Deck declared nothing, so the fallback was load-bearing ✗

    Note the export engine LEFT this repo in 0b3b4266f ("Fourteen modules leave the platform") — MeshWeaver.Markdown.Export now lives in MeshWeaver.Plugins, so items 3 and 4 are a two-repo change.

    PRs

    • Systemorph/MeshWeaver.Plugins#553 — items 3 + the declaration. Publish/Deck DECLARES .WithExport(ExportDeclaration.SlideDeck); ExportMenuProvider and ExportDocumentLayoutArea read Configuration.Get<ExportDeclaration>(); the templates dispatch on a composition input. Green.
    • refactor(export): delete the export layer's last compiled type gate (#1576 item 4) #1973 — item 4: ExportDeclaration.Resolve deleted (no callers left), plus the doc and a contract test.

    How the declaration reaches a template

    The design note said "put the composition on the export request, stamped by ExportDocumentLayoutArea". It turned out simpler: ExportDocumentHandler runs ON the source node's own hub — every caller targets new Address(sourcePath) — so it reads the declaration off hub.Configuration, the same read the menu provider does, and stamps it into the script inputs. No contract change, no client change, and the layout area never has to hand a value forward.

    The gate's failure mode, reproduced

    A plugin type that composes like a deck but is not NAMED one fell through to the document branch and exported the root node's own body. A deck-shaped node has no body, so the export succeeded and produced an empty file. Control run of the new test against the pre-fix module code:

    activity status=Succeeded
      [Information] Rendered 414 bytes of email-safe HTML
    === TEST FAILED: Expected "<!doctype html>…<div style="max-width:840px;…"></div></body></html>"
        to contain "ALPHAWIDGET" …
    Failed: 1
    

    One gate deliberately NOT removed

    DeckSelection.ResolveDeckSelection keeps SlideNodeType.Matches for the DEFAULT slide query. That is not an export composer — it is which children a deck with no manifest treats as slides, and it is already overridable as data (DeckContent.Query, which turns the filter off entirely). Leaving it does not let a name gate back into the export path.

    Merge order: Plugins#553 first, then #1973 — the fallback is what keeps a deck exportable on an install whose Publish pack has not yet synced its declaration.

  2. added a commit that references this issue on Aug 21, 2026
  3. rbuergi commented on Aug 25, 2026

    @rbuergi
    ContributorAuthor

    Fixed on origin/main — verified rather than assumed.

    ExportDeclaration (src/MeshWeaver.Graph/ExportDeclaration.cs:41) is the declaration, set via config.WithExport(...). Commit e2cba48723 collapsed DeckExportMenuProvider + MarkdownExportMenuProvider into a single ExportMenuProvider (now MeshWeaver.Plugins:src/MeshWeaver.Markdown.Export/Layout/ExportMenuProvider.cs, registered in MarkdownExportExtensions.cs), and d50050cb13 deleted the last compiled type gate — item 4 — including the DeckNodeType.Matches transition fallback. isDeck has zero occurrences tree-wide; templates now dispatch on the composition stamped by ExportDocumentHandler.

    Crucially — extracting is not using, so the consumer is named and pinned: Publish/Deck declares WithExport(ExportDeclaration.SlideDeck), asserted by Plugins:Publish/Deck/Test/DeckTests.cs and DeclaredCompositionExportTest.

    The residual SlideNodeType.Matches/DeckNodeType.Matches callers (DeckSlidesCache.cs:204, MarkdownFileParser.cs:270) are deck ordering and the persistence parser — #1580's scope, not the export layer's.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions