Skip to content

feat(modules): bundle build + Store funnel + auto-update — #1664 Slices B+C - #1673

Merged
rbuergi merged 3 commits into
mainfrom
feat/1664-module-bundles
Aug 16, 2026
Merged

rbuergi merged 3 commits into
mainfrom
feat/1664-module-bundles

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Implements #1664 Slice B (module package shape + bundle build) and Slice C (funnel + landing + auto-update), on top of the merged Slice A landing seam (ModuleActivation / ModuleLandingService).

Shape decisions

One bundle, two lanes. A package's compiled module rides inside the same bundle that already carries its NodeType assemblies — meshweaver/modules/<file> beside meshweaver/assemblies/<nodePath>.dll, one meshweaver/manifest.json naming both (module: { assemblyName, assemblies[] } next to assemblies[]). Same routes, same index, same instance-key auth — the #13 transport decision is honored by extending /api/plugins/bundles, not adding a channel. One reader serves both lanes (BundleReader.Read / BundleReader.ReadModule, both manifest-driven); the module side is deliberately all-or-nothing (a NodeType with missing bytes compiles; a module missing part of its closure loads and faults at first use).

The declaration is two fields authors already know. PackageManifest.Module = the module's entry-assembly name, PackageManifest.MinMeshVersion = its declared platform floor (both authored on the plugin root's index.json content, read by NodeRepoPackageSource, carried onto the install record by the ordinary record stamp). A mixed package — content nodes + compiled module in one Store product, the SocialMedia shape — is exactly these fields on an ordinary node-repo package; card/price/funnel/pre-install are untouched.

🚨 The module gate is a minMeshVersion FLOOR — deliberately NOT MVID equality (maintainer's dependency-analysis correction). MVID equality is bake semantics — a NodeType assembly compiled in-process against exact refs — and applying it to ordinary modules would force rebundling every module on every CI build and forbid ex-post Store installs across platform versions ("install GRPC ex post through the Store"). Modules are plain assemblies binding by simple name; their contract is API compatibility, which the semver floor expresses. The ONE gate is the new ModulePlatformFloor.DeclineReason (running platform = MeshWeaver.Graph's informational version, SemVer-compared via NuGetVersionComparer), applied at the index, the manifest, placement, and boot; an absent floor is no constraint. The bundle keeps recording its built-against MVID as DIAGNOSTIC metadata — logged at landing, surfaced in the index/sidecar — never a refusal. NodeType-bake bundles keep the strict MVID gate (PrebuiltAssemblySeeder.DeclineReason) unchanged. Consequence: landed modules keep loading across ordinary platform updates (no re-land per image roll); the only boot skip is a platform rollback below a module's declared floor.

The registry serves what it runs. Module bytes are served from the registry's own modules/<name>/ tree (ModuleBundleSource) — the same philosophy as the NodeType lane ("the inputs ARE the storage") — and a landing the registry's own boot would skip (uninstalled, or a floor its platform no longer satisfies) is refused, so a registry can never fan out assemblies it could not load itself. The index stamps each bundle's module + minMeshVersion only when the bytes are actually servable.

The build lane is a plain tool invocation. meshweaver-plugin-build module-pack <outputDir> --plugin <id> --package-version <v> --min-mesh-version <floor> packs a module bundle recording the floor (the consumer's gate) and, when the restored MeshWeaver.Graph.dll is at hand, the built-against MVID (diagnostics — optional, warned when absent). Because the gate is the floor, ONE bundle serves every compatible platform build. The closure is an explicit statement (<name>.dll + --with), never a folder scrape, mirroring the modules// rule that for most modules the DLL alone is the closure. Invocable from any node repo's CI (the tool is PackAsTool; the platform's plugin-publish.yml already builds/runs it from a checkout).

The funnel (Slice C)

  • Install/update: the module branch lives in the ONE install orchestrator (CatalogLayoutAreas.InstallOrUpdate), riding a Bundles handle on RegistryPackageSource — so the catalog click, the content auto-update apply, and the boot default install all land a declared module identically: bundle-fetch → floor gate (ModulePlatformFloor.DeclineReason, checked at the index, at the manifest, and a third time at placement) → ModuleLandingService.LandModule (restart-as-activation; the existing PendingRestart sidecar flag is the signal). Nothing here can fail an install — every refusal is a logged zero. Followed the code on the brief's "PackageInstaller binary branch": PackageInstaller never sees a source or a registry credential, so the branch belongs one caller up, in the orchestrator that has both — reported as a deliberate deviation.
  • Auto-update: RegistryUpdateReconciler gains a module pass after its content pass: for installed module-declaring packages it consults the registry's bundle index and applies one pure decision (ModuleUpdateDecision.Decide) — lands newer when the bundle's floor is satisfied here, skips same-version without a download, skips a bundle whose floor exceeds the running platform silently-with-log (it becomes installable after the platform updates, and the same pass lands it then). Never rolls back unattended. The landed version + floor are recorded on the activation entry (ModuleActivationEntry.Version/.MinMeshVersion) so "already landed" costs zero bytes.

Policy-surface wiring (no new knob)

The unattended gate is the existing Admin/UpdatePolicy surface, bridged via IModuleUpdatePolicy (declared in MeshWeaver.PluginCatalog, implemented in Memex.Portal.Shared.SelfUpdate.PlatformModuleUpdatePolicy, registered by AddSelfUpdate beside the node type):

  • Continuous — the platform default, and what an absent node/absent provider reads as — lands unattended (DEFAULT = AUTO-UPDATE, per the maintainer directive).
  • Stable / None decline the upgrade — and only the upgrade: a first landing (completing an install the operator's own surfaces sanctioned) is policy-exempt, because gating it would ship a package whose binary half never arrives. (Under the floor gate there is no "re-land after an image roll" case at all — landed modules keep loading across platform builds.) An unreadable policy declines (fail toward not acting).

The plugin lane's existing AutoUpdate/AutoUpdateByDefault record flag keeps governing the content reconcile exactly as before; a content auto-update that applies carries its module with it through the orchestrator.

Deferred (follow-ups, not this repo/PR)

  • MeshWeaver.Plugins repo: the PluginContent record in Store/Plugin/Source gains the matching module field beside its existing minMeshVersion (authoring-side mirror of content.module); until then the field is plain JSON on the root and flows fine.
  • MeshWeaver.SocialMedia repo: its CI invokes module-pack and declares content.module: MeshWeaver.Social on its root — the satellite-CI wiring the maintainer scoped out of this PR.
  • Registry acquisition of bundles it does not run: today the registry serves module bytes from its own modules/ tree (image-shipped or store-landed). Once first-party modules fully relocate out of the image (Slice D), the registry acquires them the same way consumers do — by installing the package — or via a CI upload lane if one becomes necessary; that decision rides Slice D.
  • IVT consumers are the ONE build-time-coupled module class. A module using InternalsVisibleTo internals binds to build identity, not public API — a semver floor cannot vouch for it. Currently that is exactly MeshWeaver.Approvals (→ MeshWeaver.Graph internals); such modules stay image-shipped until de-internalized, and only then join the bundle lane.

Verification

  • MeshWeaver.Plugin.Packaging, MeshWeaver.Plugin.Build, MeshWeaver.PluginCatalog, Memex.Portal.Shared + all touched test projects: -c Release -warnaserror, 0 errors, one project per invocation.
  • Unit pins: bundle round-trip incl. the floor field, the mixed (both-lanes) bundle and the all-or-nothing closure (BundleReaderTest); the pure floor gate itself incl. SemVer-not-string ordering and never-land-on-unknown-running-version (ModulePlatformFloorTest); floor refusal at fetch (ModuleFunnelTest.ABundleWhoseFloorExceedsThisPlatform_IsRefused_NothingReachesDisk) AND at land (ModuleLandingServiceTest); the ex-post pin the correction demands — a bundle from another platform build with a satisfied floor LANDS, MVID recorded as diagnostics (ModuleFunnelTest.ABundleFromAnotherPlatformBuild_WithItsFloorSatisfied_Lands, ModuleLandingServiceTest.LandModule_ForeignBuiltAgainstMvid_Lands_TheMvidIsDiagnosticOnly, ModuleUpdateDecisionTest.ABundleBuiltAgainstAnOlderPlatform_Lands_WhenItsFloorIsSatisfied, ModuleBundleSourceTest.ALandingBuiltByAnotherPlatformBuild_IsStillServed_TheMvidIsDiagnostic); reconciler lands-newer / skips-same / skips-below-floor / skips-older / policy-gates-upgrades-only / never-reinstalls-uninstalled — all pure (ModuleUpdateDecisionTest); boot union floor-skip + no-floor-loads (ModuleActivationBootTest); installer routing — declaration + floor flow listing → record, module payload routes to landing not node-parse (ModuleFunnelTest); registry serve rules (ModuleBundleSourceTest).
  • Suites green (Release): MeshWeaver.PluginCatalog.Test 347/347 · MeshWeaver.Graph.Test 1194/1194 · Memex.Portal.Shared.Test 366/366 · doc integrity (DocumentationLinkIntegrityTest + WhatsNewEntryIntegrityTest) green after the doc edits.
  • Reactive rules: all landing/reconcile IO composes IObservable through the sealed IoPools (ModuleLandingService's cap-1 pool, the HTTP pool in PluginBundleClient); no bare async in hub-reachable code.

Docs: Modules.md → "The bundle lane — modules as Store packages" + the auto-update paragraph naming the policy surface and default; PluginPackaging.md → "The module variant". What's New: Modules install and update from the Store (Feature).

Closes nothing on its own — #1664 remains open for Slice D.

🤖 Generated with Claude Code

…es B+C

Slice B — package shape + bundle build:
- Bundle format gains a MODULE variant inside the SAME bundle the NodeType
  lane already ships: meshweaver/modules/<file> beside meshweaver/assemblies/,
  one manifest naming both (module: { assemblyName, assemblies[] }). One
  reader handles both (BundleReader.Read / ReadModule, manifest-driven; the
  module side is all-or-nothing — a partial closure loads and faults at
  first use, so it never lands).
- PackageManifest.Module — the compiled-module declaration (entry-assembly
  name), authored as content.module on the plugin root, read by
  NodeRepoPackageSource, carried onto the install record by the ordinary
  record stamp.
- meshweaver-plugin-build module-pack: packs a built module's closure into a
  bundle keyed to the MVID of the MeshWeaver.Graph.dll in the build output
  (read, never guessed). Explicit closure (--with), never a folder scrape.
  Invocable from any node repo's CI (PackAsTool).

Slice C — funnel + landing:
- PluginBundleEndpoints serves the module section from the registry's own
  modules/<name>/ tree (ModuleBundleSource — refuses uninstalled and
  framework-stale landings); the index stamps each bundle's module only when
  the bytes are servable. Same instance-key auth, fail-closed.
- PluginBundleClient.AdoptModule: index → ModuleUpdateDecision → download →
  MVID gate (PrebuiltAssemblySeeder.DeclineReason, checked at index,
  manifest, and placement) → ModuleLandingService.LandModule
  (restart-as-activation via the existing PendingRestart signal). Never
  fails an install — every refusal is a logged zero.
- The install branch rides the ONE orchestrator
  (CatalogLayoutAreas.InstallOrUpdate) via a Bundles handle on
  RegistryPackageSource, so the catalog click, the content auto-update
  apply, and the boot default install land a declared module identically.

Auto-update (rides the EXISTING policy surface — no new knob):
- RegistryUpdateReconciler gains a module pass: newer-for-the-running-MVID
  lands, same-version skips without a download, foreign-MVID skips
  silently-with-log; never rolls back unattended. Landed version recorded on
  the activation entry (ModuleActivationEntry.Version).
- Gate = Admin/UpdatePolicy via IModuleUpdatePolicy (memex bridges it in
  PlatformModuleUpdatePolicy, registered by AddSelfUpdate). Continuous — the
  platform default, and what an absent policy reads as — lands unattended;
  Stable/None decline UPGRADES only (first landings complete an install, and
  the image-roll heal keeps an installed module working — both exempt).

Docs: Modules.md bundle-lane section + auto-update policy paragraph;
PluginPackaging.md module variant; What's New feature entry.

Tests: bundle round-trip incl. mixed bundle (BundleReaderTest), pure
decision pins (ModuleUpdateDecisionTest), serve rules
(ModuleBundleSourceTest), fetch/land MVID refusals + declaration flow
(ModuleFunnelTest). Suites green: PluginCatalog.Test 340, Graph.Test 1194,
Memex.Portal.Shared.Test 366, doc integrity.

Part of #1664 (Slice D remains).

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

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

Implements #1664 Slice B+C by extending the existing plugin bundle transport to optionally carry a compiled module payload, wiring module landing into the install + boot reconcile funnels, and adding an unattended update gate tied to the existing Admin/UpdatePolicy surface (Memex).

Changes:

  • Extend bundle format to support a “module lane” (meshweaver/modules/*) alongside the existing NodeType assembly lane, with manifest-driven reading/writing and mixed-bundle support.
  • Route module-declaring packages through install/update + boot reconcile to land module bytes into modules/<name>/ and record activation/version for “already landed” decisions.
  • Add a module-pack build tool command, policy wiring (IModuleUpdatePolicy), API endpoint support, and tests/docs to pin behavior.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/MeshWeaver.PluginCatalog.Test/ModuleUpdateDecisionTest.cs Adds pure decision tests covering module reconcile actions (land/skip/policy/rollback rules).
test/MeshWeaver.PluginCatalog.Test/ModuleFunnelTest.cs Adds end-to-end funnel tests for module landing and manifest/record propagation.
test/MeshWeaver.PluginCatalog.Test/ModuleBundleSourceTest.cs Adds tests for registry-side “servable module bytes” rules from modules/ tree.
test/MeshWeaver.Graph.Test/BundleReaderTest.cs Extends bundle reader tests to cover module bundles and mixed bundles.
src/MeshWeaver.PluginCatalog/RegistryUpdateReconciler.cs Adds a post-content module reconcile pass using a shared bundle client per registry.
src/MeshWeaver.PluginCatalog/RegistryPackageSource.cs Adds optional Bundles handle so install orchestrator can land modules from registry sources.
src/MeshWeaver.PluginCatalog/PluginBundleClient.cs Adds module adoption/landing flow (AdoptModule + LandFromBundle) keyed off bundle index + MVID gate + policy.
src/MeshWeaver.PluginCatalog/Package.cs Adds PackageManifest.Module declaration field to route packages through the module funnel.
src/MeshWeaver.PluginCatalog/NodeRepoPackageSource.cs Reads content.module from root node JSON into the package manifest.
src/MeshWeaver.PluginCatalog/ModuleUpdateDecision.cs Introduces the pure module auto-update decision function + verdict/action types.
src/MeshWeaver.PluginCatalog/ModuleLandingService.cs Extends landing to record version; adds pooled activation read helper and exposes base directory.
src/MeshWeaver.PluginCatalog/ModuleBundleSource.cs Adds registry-side module closure collection rules from modules/<name>/ gated by activation + framework MVID.
src/MeshWeaver.PluginCatalog/ModuleActivation.cs Adds ModuleActivationEntry.Version for “already landed” checks.
src/MeshWeaver.PluginCatalog/InstanceAutoRegistrationService.cs Shares a single bundle client between default-install, registry source, and adopt paths.
src/MeshWeaver.PluginCatalog/CatalogLayoutAreas.cs Wires module landing into the main install/update orchestrator for registry sources.
src/MeshWeaver.Plugin.Packaging/NuGetPackageWriter.cs Adds distinct meshweaver/modules/ folder support and helper for module entry paths.
src/MeshWeaver.Plugin.Packaging/BundleReader.cs Adds manifest support for module section and a ReadModule extractor with all-or-nothing closure semantics.
src/MeshWeaver.Plugin.Build/Program.cs Adds CLI dispatch for module-pack.
src/MeshWeaver.Plugin.Build/ModulePackCommand.cs Implements module-pack to produce module bundles keyed to the framework MVID read from build output.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-16-modules-install-and-update-from-the-store.md Adds What’s New entry documenting module install + update behavior.
src/MeshWeaver.Documentation/Data/Architecture/PluginPackaging.md Documents the bundle/module variant layout and CI packaging invocation.
src/MeshWeaver.Documentation/Data/Architecture/Modules.md Documents Store module delivery via bundles and the module reconcile/policy behavior.
memex/Memex.Portal.Shared/SelfUpdate/SelfUpdateConfiguration.cs Registers the Memex policy provider implementation for module unattended landing.
memex/Memex.Portal.Shared/SelfUpdate/PlatformModuleUpdatePolicy.cs Implements IModuleUpdatePolicy over Admin/UpdatePolicy with fail-closed behavior on read errors.
memex/Memex.Portal.Shared/Api/PluginBundleEndpoints.cs Extends bundle index + bundle assembly to stamp/serve module payloads when servable on the registry instance.
Suppressed comments (1)

test/MeshWeaver.PluginCatalog.Test/ModuleFunnelTest.cs:196

  • Add Dispose() to ensure landingRoot is deleted even when a test fails before reaching the explicit cleanup statements.
        Directory.Delete(landingRoot, recursive: true);
    }
}

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

Comment thread src/MeshWeaver.PluginCatalog/ModuleUpdateDecision.cs Outdated
Comment thread src/MeshWeaver.Plugin.Build/ModulePackCommand.cs
Comment thread test/MeshWeaver.PluginCatalog.Test/ModuleFunnelTest.cs
@github-actions

github-actions Bot commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 601 tests   1 595 ✅  7m 30s ⏱️
   11 suites      6 💤
   11 files        0 ❌

Results for commit 2b0f05e.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

919 tests   918 ✅  11m 18s ⏱️
 10 suites    1 💤
 10 files      0 ❌

Results for commit 2b0f05e.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 371 tests   1 370 ✅  5m 51s ⏱️
   11 suites      1 💤
   11 files        0 ❌

Results for commit 2b0f05e.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   11 files     11 suites   6m 4s ⏱️
2 196 tests 2 005 ✅ 191 💤 0 ❌
2 561 runs  2 370 ✅ 191 💤 0 ❌

Results for commit 2b0f05e.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

2 438 tests   2 434 ✅  8m 31s ⏱️
   11 suites      4 💤
   11 files        0 ❌

Results for commit 2b0f05e.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

2 148 tests   2 045 ✅  6m 50s ⏱️
   11 suites    103 💤
   11 files        0 ❌

Results for commit 2b0f05e.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

    65 files      65 suites   46m 6s ⏱️
10 673 tests 10 367 ✅ 306 💤 0 ❌
11 038 runs  10 732 ✅ 306 💤 0 ❌

Results for commit 2b0f05e.

♻️ This comment has been updated with latest results.

@rbuergi

rbuergi commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

CI red root-caused before rerun: shard 0's OrleansGrainTeardownStragglerTest timed out in its SETUP wait (activation leaving the silo catalog, 30 s bound) — the pinned contract was never reached. The diff is structurally unreachable from that test host (no project it references references PluginCatalog/Plugin.Packaging/Plugin.Build/Portal.Shared; fixture composes AddGraph+AddAI+RLS only), and the last four failed runs in 24 h — main included — each failed on a different single integration test. Full evidence: #1384 (comment). Rerunning the failed jobs.

🤖 Generated with Claude Code

…D equality

Maintainer design correction from the dependency analysis: MVID equality is
BAKE semantics (NodeType assemblies compiled against exact refs) wrongly
applied to ordinary modules. Modules are plain .NET assemblies binding by
SIMPLE NAME; their contract is API compatibility. MVID-equality would force
rebundling every module on every CI build and forbid ex-post Store installs
across platform versions ('install GRPC ex post through the Store').

- NEW ModulePlatformFloor: the ONE module platform gate — running platform
  (MeshWeaver.Graph's informational version, +sha stripped) must satisfy the
  module's declared minMeshVersion, SemVer-compared (NuGetVersionComparer);
  absent floor = no constraint; declared floor + unknown running version
  declines (never land on faith). Applied at the index, the manifest,
  placement (ModuleLandingService — the MVID refusal moved here), the serve
  side (ModuleBundleSource), and boot (ModuleActivationBoot; wired in
  ConfigureMemexMesh). The built-against MVID stays RECORDED everywhere
  (bundle manifest, activation entry, landing log, index) as DIAGNOSTIC
  metadata — never a refusal. NodeType-bake bundles keep the strict MVID
  gate (PrebuiltAssemblySeeder.DeclineReason) unchanged.
- PackageManifest.MinMeshVersion (authored content.minMeshVersion, peeked by
  NodeRepoPackageSource, stamped onto the record) flows to the bundle index
  entry, the manifest's module section (BundleReader.ModuleRef), and the
  activation entry — so a consumer skips an uninstallable bundle without a
  download and boot re-checks the floor after a platform rollback.
- ModuleUpdateDecision: SkipForeignFramework → SkipPlatformBelowFloor;
  up-to-date keys on version alone (landed modules keep loading across
  platform builds — the image-roll re-land case no longer exists, so the
  policy exemption is first-landing only); MVID is no longer an input.
- module-pack: --min-mesh-version records the floor; --graph-dll becomes
  optional (MVID recorded as diagnostics when found, warned when not).
- Tests: ModulePlatformFloorTest (pure gate pins incl. SemVer-not-string and
  the unknown-running-version refusal); the foreign-MVID pins rewritten as
  floor pins; NEW ex-post pins — a bundle from another platform build with a
  satisfied floor LANDS/SERVES (client, landing, decision, serve rules);
  boot-union floor-skip + no-floor-loads. Docs (Modules.md,
  PluginPackaging.md, What's New) updated to the floor semantics.

Suites green (Release): PluginCatalog.Test 347, Graph.Test 1194,
Memex.Portal.Shared.Test 366, doc integrity.

Note: the ONE build-time-coupled module class is IVT consumers (currently
MeshWeaver.Approvals → Graph internals) — those stay image-shipped until
de-internalized.

Part of #1664.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rbuergi

rbuergi commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

Second commit applies the maintainer's design correction: the module-bundle gate is now a minMeshVersion floor (API compatibility — modules bind by simple name), never MVID equality; the built-against MVID is kept everywhere as diagnostic metadata only, and the NodeType-bake lane keeps its strict MVID gate unchanged. One bundle now serves every compatible platform build, ex-post Store installs across platform versions work, and landed modules keep loading across image rolls (the only boot skip is a rollback below a module's declared floor — so the policy exemption narrows to first landings). New ModulePlatformFloor is the one gate, applied at index/manifest/placement/serve/boot; pins rewritten accordingly (incl. the ex-post landing pins). PR body updated; suites re-run green (PluginCatalog 347, Graph 1194, Portal.Shared 366, doc integrity).

🤖 Generated with Claude Code

…lidation, test temp-dir cleanup

Copilot review on #1673, all three legitimate:

1) ModuleUpdateDecision: up-to-date used STRING equality while the downgrade
   check used NuGetVersionComparer — a landed '1.2.0' against a served '1.2'
   (manifests legitimately carry two-part versions) misread as an update and
   would re-land on every reconcile, forever. Both checks now go through the
   ONE comparer; pinned ('1.2' == '1.2.0' ⇒ up-to-date, both directions).

2) module-pack: moduleName/plugin/packageVersion flow into file paths (the
   entry-DLL probe, the closure entries, the output bundle name) — validate
   to a safe identifier/semver shape BEFORE composing any path, with a clear
   exit-2 error naming the offending value; '../evil' can no longer probe
   outside the module folder or write a bundle somewhere surprising.
   --min-mesh-version validated to the same version shape. Pinned
   (ModulePackCommandTest: four injection-shaped rejections + a pack→read
   round-trip through BundleReader.ReadModule).

3) ModuleFunnelTest: the per-test landing tree leaked whenever an assertion
   failed (inline end-of-test deletes are skipped the moment an assert
   throws). Cleanup moved to the lifecycle hook — the base implements
   IAsyncLifetime and xUnit v3 calls DisposeAsync in preference to
   IDisposable.Dispose, so the DisposeAsync override (best-effort catch) is
   the hook that actually runs; inline deletes dropped.

Targeted classes green (Release): ModuleUpdateDecision+Floor+Funnel 24/24,
BundleReader+ModulePackCommand 17/17.

Part of #1664.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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