Skip to content

feat(modules): Markdown.Export ships via modules/ — the first step-2 flip (#1644) - #1666

Merged
rbuergi merged 1 commit into
mainfrom
feat/modules-flip-markdown-export
Aug 16, 2026
Merged

rbuergi merged 1 commit into
mainfrom
feat/modules-flip-markdown-export

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

First step-2 flip of #1644 (after step-1 in #1653): MeshWeaver.Markdown.Export no longer rides any ships-the-bits ProjectReference — the modules/ folder is the only way its bits ship. This flip sets the pattern for the remaining 11 modules.

What flipped

  • Memex.Portal.Shared drops the MeshWeaver.Markdown.Export ProjectReference (no other host referenced it). Only the wire contract stays in the app closure, via MeshWeaver.Blazor → MeshWeaver.Markdown.Export.Contract. Activation is unchanged: Modules:Assemblies → MarkdownExportProviderAttribute → AddMarkdownExport(); appsettings entries stay exactly as-is (MeshWeaver.Markdown.Export.dll — the loader probes modules/ first per feat(modules): the modules/ publish layout, step 1 — probing, compile surface, fingerprint (#1644) #1653).
  • memex/MeshModulesPublish.targets gains the closure lane (@(MeshModuleClosure)), with Markdown.Export as its first entry; the other 11 modules stay on the thin step-1 lane. The closure lane:
    • full Publish of the module into modules/<Name>/ (explicit Restore first — post-flip the module is outside the host graph; Build runs with the module's ordinary global properties so a concurrent solution-level build merges into the same scheduled instance instead of racing one obj/, then Publish with NoBuild=true);
    • prune 0: drops runtimes/<rid>/ trees — Assembly.LoadFrom never consults the module's deps.json, so a RID tree under a module folder is unreachable by construction;
    • prune a: same-identity files the app publish already carries at the same relative path;
    • prune b: shared-framework-provided assemblies, globbed from the machine's Microsoft.AspNetCore.App.Ref/Microsoft.NETCore.App.Ref targeting packs (self-maintaining — no hand list to drift), with a hard error if the packs cannot be found (a silent skip would ship framework duplicates);
    • culture satellites suppressed at the source (SatelliteResourceLanguages=en) instead of deleted afterwards;
    • a fail-if-referenced guard (publish lane): the module's own dll in the app publish root is an error (design §1's same-identity double-load trap);
    • the closure lane also runs after plain Build of a host (into bin/…/modules/): a flipped DLL exists nowhere else, so without this dotnet run would die in Assembly.LoadFrom at startup.

Measured private deps (the closure, empirically)

Standalone module publish (160 files) diffed against the post-flip app closure (-p:PublishMeshModules=false publish of the Monolith): 22 leftovers — 4 module-own files, 18 shared-framework assemblies, zero true private deps.

  • Kept in modules/MeshWeaver.Markdown.Export/ (each hand-verified): MeshWeaver.Markdown.Export.dll, .pdb, .xml, .deps.json — the module's own outputs only.
  • Pruned as shared-framework (all present in the Microsoft.AspNetCore.App.Ref targeting pack): 17 × Microsoft.Extensions.* (Caching.Abstractions, Caching.Memory, Configuration, Configuration.Abstractions, Configuration.Binder, DependencyInjection, DependencyInjection.Abstractions, Diagnostics, Diagnostics.Abstractions, FileProviders.Abstractions, Hosting.Abstractions, Http, Logging, Logging.Abstractions, Options, Options.ConfigurationExtensions, Primitives) + System.Diagnostics.EventLog.dll.
  • The engine's package closure (Markdig, DocumentFormat.OpenXml(+Framework), HtmlAgilityPack, UglyToad.PdfPig.*) still rides the app via other live references (ContentCollections, Blazor, DataSetReader.Excel); if one of those ever leaves, the closure publish+prune starts keeping the dep automatically.

Publish assertions (real dotnet publish memex/Memex.Portal.Monolith -c Release)

LayoutMeshModuleClosures: 1 closure module(s) → …/app-pub-flip/modules/; pruned 124 app-closure + 18 shared-framework duplicate(s); kept: MeshWeaver.Markdown.Export.deps.json;MeshWeaver.Markdown.Export.dll;MeshWeaver.Markdown.Export.pdb;MeshWeaver.Markdown.Export.xml
PublishMeshModules: 11 thin + 1 closure module(s) laid out; pruned 33 app-closure duplicate(s) from the thin lane; modules/ keeps 4 file(s).
  • modules/MeshWeaver.Markdown.Export/MeshWeaver.Markdown.Export.dll exists ✔
  • app root does NOT contain MeshWeaver.Markdown.Export.dll ✔ (the Contract dll rightly remains)
  • the module folder holds exactly the 4 files above — no satellite dirs, no runtimes/ ✔
  • same layout verified in bin/Release/net10.0/modules/ after a plain build (the dotnet run path) ✔

Tests

  • Export test projects reference the engine directly now (MeshWeaver.Hosting.Monolith.Test, MeshWeaver.Security.Test, Memex.Portal.Shared.Test — they previously rode Portal.Shared's transitive reference); test projects keep shipping the DLL in their own bins.
  • The InstallAssemblies-folds-the-attribute pin the flip relies on already exists twice: ExportBootPackTest.InstallingTheAssembly_AppliesAddMarkdownExport (real built DLL via Assembly.Location) and ModuleLaneSwitchTest.MarkdownExportModule_FoldsTheEngineRegistrations — no third copy added.
  • The modules-folder loading path is pinned by InstalledModulesTest.ResolveModulePath_PrefersTheModulesFolder_AndFallsBackToBaseDirectory.

Gate parity (design §5)

Companion PR in MeshWeaver.Plugins (Systemorph/MeshWeaver.Plugins#498): fetch-refs.py hoists modules/*/*.dll from the extracted platform image /app tree to the flat ref-set top level, so the compile gate keeps seeing a flipped module exactly as in-mesh compilation does (TPA + installed modules). Merge order: this PR first, Plugins after (the sweep is a no-op until an image carries a populated modules/ folder).

Verification

  • Memex.Portal.Shared, Memex.Portal.Monolith, Memex.Portal.Distributed: -c Release -warnaserror clean (0 warnings / 0 errors each).
  • MeshWeaver.Hosting.Monolith.Test export classes (DocumentExportAreaAccessTest, DocumentExportLayoutAreaTest, EmailDocumentExportTest): Passed! Failed: 0, Passed: 9 (fresh .trx) — they compose .AddMarkdownExport() via their now-direct project reference, proving the module code itself.
  • MeshWeaver.Graph.Test full suite: Passed! Failed: 0, Passed: 1183 (includes the ResolveModulePath modules-folder probing pin).
  • MeshWeaver.Markdown.Export.Test full suite: 137/137 (includes ExportBootPackTest — InstallAssemblies on the real built DLL folds AddMarkdownExport()); Memex.Portal.Shared.Test ModuleLaneSwitchTest: 2/2.
  • Plugins repo: validate-repos.py ✔ (161 nodes / 20 repos), compile-check.py ✔ (34 clean · 0 known-debt · 0 NEW breaks · 22s) — the sweep change is inert for both until the pinned image carries a populated modules/.

No What's New entry: deployment machinery, no user-visible change.

🤖 Generated with Claude Code

…flip (#1644)

Memex.Portal.Shared drops the ships-the-bits ProjectReference; the module's
bits now ship ONLY through the modules/<Name>/ closure layout. The targets
file grows the closure lane: full per-module Publish (explicit Restore —
the module left the host graph; Build with ordinary globals so a concurrent
solution build merges into the same scheduled instance instead of racing one
obj/; Publish NoBuild), then prunes RID trees (LoadFrom never reads the
module's deps.json), same-relative-path app duplicates, and shared-framework
assemblies globbed from the targeting packs — measured survivors: exactly
the module's own dll/pdb/xml/deps.json, zero private deps (its package
closure still rides the app via other references). The closure lane also
runs after plain Build so dotnet run keeps working; the publish lane fails
hard if the module's dll reappears in the app root (the same-identity
double-load guard). Export test projects reference the engine directly now;
Modules:Assemblies entries are unchanged — the loader probes modules/ first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 16, 2026 09:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR advances the #1644 “modules/ publish layout” migration by performing the first step‑2 flip: MeshWeaver.Markdown.Export no longer ships via any host ProjectReference and instead ships exclusively through the modules/ publish layout, while keeping activation via Modules:Assemblies.

Changes:

  • Removed MeshWeaver.Markdown.Export from Memex.Portal.Shared’s ships-the-bits references and updated tests to reference the export engine directly.
  • Extended memex/MeshModulesPublish.targets with a new closure lane (@(MeshModuleClosure)) and added a post-Build hook to lay out flipped modules into bin/.../modules/ for dotnet run.
  • Updated architecture documentation to describe the first flipped module and the closure lane behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
test/MeshWeaver.Security.Test/MeshWeaver.Security.Test.csproj Adds a direct reference to the export engine so export-related security tests remain pinned after the host reference is removed.
test/MeshWeaver.Hosting.Monolith.Test/MeshWeaver.Hosting.Monolith.Test.csproj Adds a direct reference to the export engine so monolith export tests don’t rely on host transitive closure.
test/Memex.Portal.Shared.Test/Memex.Portal.Shared.Test.csproj Adds a direct reference to the export engine to anchor module lane switching behavior in tests.
src/MeshWeaver.Documentation/Data/Architecture/Modules.md Documents the first step‑2 flipped module and how the closure lane supports dotnet run.
memex/MeshModulesPublish.targets Implements the closure lane publish+prune pipeline and the after-build layout needed when a flipped module no longer exists in the host output by default.
memex/Memex.Portal.Shared/Memex.Portal.Shared.csproj Drops the ships-the-bits ProjectReference to MeshWeaver.Markdown.Export, leaving only the contract in the app closure.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

918 tests  ±0   917 ✅ ±0   10m 59s ⏱️ -4s
 10 suites ±0     1 💤 ±0 
 10 files   ±0     0 ❌ ±0 

Results for commit 8923b59. ± Comparison against base commit 20b793b.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   11 files  ±0     11 suites  ±0   4m 51s ⏱️ - 1m 18s
2 196 tests ±0  1 997 ✅  - 8  199 💤 +8  0 ❌ ±0 
2 556 runs  ±0  2 357 ✅  - 8  199 💤 +8  0 ❌ ±0 

Results for commit 8923b59. ± Comparison against base commit 20b793b.

This pull request skips 8 tests.
MeshWeaver.Hosting.Cosmos.Test.CosmosChangeFeedTests ‑ CosmosChangeFeedProcessor_StartsAndStops_Successfully
MeshWeaver.Hosting.Cosmos.Test.CosmosChangeFeedTests ‑ CosmosStorageAdapter_WithChangeFeedProcessor_CanBeAttached
MeshWeaver.Hosting.Cosmos.Test.CosmosChangeFeedTests ‑ CreateLeaseContainerAsync_CreatesContainer_WhenNotExists
MeshWeaver.Hosting.Cosmos.Test.EmulatorSmokeTests ‑ Exists_TracksWriteAndDelete
MeshWeaver.Hosting.Cosmos.Test.EmulatorSmokeTests ‑ ListChildPaths_ReturnsChildren
MeshWeaver.Hosting.Cosmos.Test.EmulatorSmokeTests ‑ QueryNodes_FiltersByNodeType
MeshWeaver.Hosting.Cosmos.Test.EmulatorSmokeTests ‑ Write_IsUpsert_NotDuplicate
MeshWeaver.Hosting.Cosmos.Test.EmulatorSmokeTests ‑ Write_Then_Read_RoundTripsNode

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 542 tests  ±0   1 536 ✅ ±0   7m 21s ⏱️ +7s
   11 suites ±0       6 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 8923b59. ± Comparison against base commit 20b793b.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 367 tests  ±0   1 366 ✅ ±0   5m 56s ⏱️ -3s
   11 suites ±0       1 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 8923b59. ± Comparison against base commit 20b793b.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

2 178 tests  ±0   2 075 ✅ ±0   7m 23s ⏱️ +2s
   11 suites ±0     103 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 8923b59. ± Comparison against base commit 20b793b.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

2 384 tests  ±0   2 380 ✅ ±0   7m 14s ⏱️ -30s
   11 suites ±0       4 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 8923b59. ± Comparison against base commit 20b793b.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

    65 files  ±0      65 suites  ±0   43m 47s ⏱️ - 1m 44s
10 585 tests ±0  10 271 ✅  - 8  314 💤 +8  0 ❌ ±0 
10 945 runs  ±0  10 631 ✅  - 8  314 💤 +8  0 ❌ ±0 

Results for commit 8923b59. ± Comparison against base commit 20b793b.

This pull request skips 8 tests.
MeshWeaver.Hosting.Cosmos.Test.CosmosChangeFeedTests ‑ CosmosChangeFeedProcessor_StartsAndStops_Successfully
MeshWeaver.Hosting.Cosmos.Test.CosmosChangeFeedTests ‑ CosmosStorageAdapter_WithChangeFeedProcessor_CanBeAttached
MeshWeaver.Hosting.Cosmos.Test.CosmosChangeFeedTests ‑ CreateLeaseContainerAsync_CreatesContainer_WhenNotExists
MeshWeaver.Hosting.Cosmos.Test.EmulatorSmokeTests ‑ Exists_TracksWriteAndDelete
MeshWeaver.Hosting.Cosmos.Test.EmulatorSmokeTests ‑ ListChildPaths_ReturnsChildren
MeshWeaver.Hosting.Cosmos.Test.EmulatorSmokeTests ‑ QueryNodes_FiltersByNodeType
MeshWeaver.Hosting.Cosmos.Test.EmulatorSmokeTests ‑ Write_IsUpsert_NotDuplicate
MeshWeaver.Hosting.Cosmos.Test.EmulatorSmokeTests ‑ Write_Then_Read_RoundTripsNode

@rbuergi
rbuergi merged commit 7823206 into main Aug 16, 2026
23 checks passed
@rbuergi
rbuergi deleted the feat/modules-flip-markdown-export branch August 19, 2026 10:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants