Skip to content

fix(registry): label each served assembly with its own module version (#6831) - #6838

Merged
rbuergi merged 5 commits into
mainfrom
fix/6831-served-build-module-version
Oct 16, 2026
Merged

rbuergi merged 5 commits into
mainfrom
fix/6831-served-build-module-version

Conversation

@rbuergi

@rbuergi rbuergi commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Closes #6831

Root cause

Where adoptedModuleVersion: 1.1.1 came from:

  • The registry's own-lane bundle serve (PluginBundleEndpoints.Assemble → BuildResult) builds the archive from each NodeType's current build (LastCompiledVersion).
  • It writes the package's advertised version (package.Version) as the manifest's only version.
  • PluginBundleClient.SeedAll stamped every adopted assembly with that manifest.Version (PrebuiltAssemblySeeder.Write → AdoptedModuleVersion = moduleVersion).

The registry's Personal/PersonalApp record still held the 1.0.1 build, because 1.1.1's only bake was cancelled. Every consumer therefore adopted the 1.0.1 bytes (same MVID) stamped 1.1.1.

The own-lane serve carries no per-assembly source fingerprint, so the result is AdoptedUnverified. That matches the record in the issue exactly.

This is a different root cause from #6832 (#6835), so it is a separate PR.

Fix

  • BundleReader.AssemblyRef.ModuleVersion (init) and Manifest.ModuleVersionPerAssembly (init) are new. Manifest.ModuleVersionOf(ref) resolves an assembly's version:
    • under the flag, the per-assembly value, and null means unknown;
    • without the flag, the bundle's Version, as before.
  • Payload.ModuleVersion carries the resolved value.
  • The registry (BuildResult) writes moduleVersion on every assembly record, plus moduleVersionPerAssembly: true. Where the value comes from:
    • Own-lane serve: read off the record that names the bytes, through the new pure rule PrebuiltAssemblySeeder.ModuleVersionOfBuild:
      • an adopted build → its AdoptedModuleVersion;
      • compiled here and current → CurrentModuleVersion;
      • compiled and dirty → null.
    • Release-artifact path: the same rule, but only when the artifact is the record's current build; otherwise null.
    • Sealed-lane serve: carried from the sealed archive.
  • PluginBundleClient and ShippedPrebuiltBundles stamp Payload.ModuleVersion. Bake-sealed bundles carry no flag, so they behave exactly as before.
  • Doc: Doc/Architecture/SealedPublicationReads, new section "The module version on each assembly (Fix buildProvenance AdoptedUnverified naming a module version whose build was never baked #6831)".

Test

test/Memex.Portal.Shared.Test/RegistryStatesTheServedBuildsModuleVersionTest.cs uses the real authenticated route, a real install record at 1.1.1, the real assembly store and the real BundleReader. It covers:

  • The issue's record: a type whose current build is an adoption at 1.0.1, served under the package's 1.1.1, must read 1.0.1.
  • Control: a type compiled here from the current source reads 1.1.1.
  • The pure rule: row by row.
  • The no-flag case: a bake bundle keeps the bundle version.

Negative control. Reverting only memex/Memex.Portal.Shared/Api/PluginBundleEndpoints.cs to origin/main makes the test fail:

failed Memex.Portal.Shared.Test.RegistryStatesTheServedBuildsModuleVersionTest.AnOlderBuildServedUnderANewerPackageVersion_IsLabelledWithItsOwnVersion
  Assert.Equal() Failure: Strings differ
  Expected: "1.0.1"
  Actual:   "1.1.1"

With the fix restored: total: 8 failed: 0.

Related suites are green locally (Release, -warnaserror builds, 0 Error(s)):

  • Memex.Portal.Shared.Test bundle/registry filters: 89/89
  • Hosting.Test Prebuilt/Bundle/Shipped: 88/88
  • Compiler.Pipeline.Test Bundle/Prebuilt/Adopt: 224/224

Rollout

Recycle post-roll: none. This changes the registry serve and the consumer's stamp, not what any per-node hub binds.

Pairs-with: none — adds init members to public records and one public static method; the only removal is a private method's parameter.
Mirror-sync: none — no i18n catalog change.

🤖 Generated with Claude Code

…#6831)

The registry's own-lane bundle serve built the archive from each NodeType's
current build (LastCompiledVersion) but wrote the package's ADVERTISED version
as the manifest's only version, and PluginBundleClient stamped every adopted
assembly with it. A record whose current build was an older adoption (Personal
1.0.1 bytes while the package was advertised at 1.1.1) was therefore adopted
downstream as adoptedModuleVersion 1.1.1, naming a version never baked.

Each assembly record now carries moduleVersion, read off the record that names
the bytes (PrebuiltAssemblySeeder.ModuleVersionOfBuild: adopted -> its
AdoptedModuleVersion; compiled and current -> CurrentModuleVersion; compiled
and dirty -> unknown), and the manifest declares moduleVersionPerAssembly so an
unstated version is unknown rather than the package's. Bake bundles carry no
flag and keep the bundle version. Consumers stamp Payload.ModuleVersion.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 15, 2026 13:55
@rbuergi
rbuergi enabled auto-merge October 15, 2026 13:55

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.

🟡 Changes recommended

Build-version inference can still label served assemblies with a version belonging to different bytes.

1 open finding
What changed in this PR

Addresses #6831 by carrying per-assembly module versions from registry bundles into consumer adoption records, rather than using the advertised package version.

Changes:

  • Adds per-assembly version metadata with legacy-bundle fallback.
  • Updates registry serving and both adoption consumers.
  • Adds regression tests and documentation.
File Description
test/​Memex.Portal.Shared.Test/​RegistryStatesTheServedBuildsModuleVersionTest.cs Tests version attribution and legacy fallback.
src/​MeshWeaver.PluginCatalog/​PluginBundleClient.cs Adopts resolved per-assembly versions.
src/​MeshWeaver.Plugin.Packaging/​BundleReader.cs Reads and resolves version metadata.
src/​MeshWeaver.Hosting/​ShippedPrebuiltBundles.cs Seeds each payload’s module version.
src/​MeshWeaver.Documentation/​Data/​Architecture/​SealedPublicationReads.md Documents version attribution.
src/​MeshWeaver.Compiler.Pipeline/​PrebuiltAssemblySeeder.cs Adds build-version inference.
memex/​Memex.Portal.Shared/​Api/​PluginBundleEndpoints.cs Emits per-assembly version metadata.

🧠 Review effort: Balanced


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

Comment thread src/MeshWeaver.Compiler.Pipeline/PrebuiltAssemblySeeder.cs Outdated
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

    26 files  ± 0      26 suites  ±0   1h 0m 44s ⏱️ -41s
12 812 tests +19  12 619 ✅ +19  193 💤 ±0  0 ❌ ±0 
12 828 runs  +19  12 635 ✅ +19  193 💤 ±0  0 ❌ ±0 

Results for commit 4803bf9. ± Comparison against base commit 22eb9e4.

This pull request removes 44 and adds 37 tests. Note that renamed tests count towards both.

   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---)
   --- End of inner exception stack trace ---, expected: True)
   --- End of inner exception stack trace ---, isDenial: True)
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
 ---> (Inner Exception #1) System.ArgumentException: Value does not fall within the expected range.<---
 ---> (Inner Exception #1) System.InvalidOperationException: boom<---
 ---> (Inner Exception #1) System.InvalidOperationException: source B is misconfigured<---
…
Memex.Portal.Shared.Test.InstanceIdRulesMatchTheRegistryTest ‑ TheSetupHostAgreesWithTheRegistry(candidate: "b0795228-f774-41f7-a9aa-8c7f6126213c")
Memex.Portal.Shared.Test.PublishStorageFaultIsNotAConflictTest ‑ AStorageFault_IsA500_NamingTheError(fault: System.AggregateException: One or more errors occurred. (Access to the path '/data/modules/activation.d/MeshWeaver.Markdown.Export.json' is denied.)
 ---> System.UnauthorizedAccessException: Access to the path '/data/modules/activation.d/MeshWeaver.Markdown.Export.json' is denied.
   --- End of inner exception stack trace ---)
Memex.Portal.Shared.Test.PublishStorageFaultIsNotAConflictTest ‑ AStorageFault_IsA500_NamingTheError(fault: System.InvalidOperationException: landing faulted
 ---> System.UnauthorizedAccessException: Access to the path '/data/modules/activation.d/MeshWeaver.Markdown.Export.json' is denied.
   --- End of inner exception stack trace ---)
Memex.Portal.Shared.Test.RegistryStatesTheServedBuildsModuleVersionTest ‑ ABundleWithoutPerAssemblyVersions_KeepsTheBundleVersion
Memex.Portal.Shared.Test.RegistryStatesTheServedBuildsModuleVersionTest ‑ ASameKeyBuildWithAnotherMvid_IsNeverServedUnderTheRecordsVersion
Memex.Portal.Shared.Test.RegistryStatesTheServedBuildsModuleVersionTest ‑ AnOlderBuildServedUnderANewerPackageVersion_IsLabelledWithItsOwnVersion
Memex.Portal.Shared.Test.RegistryStatesTheServedBuildsModuleVersionTest ‑ TheServedBuildsVersion_ComesFromTheBuild_NeverFromTheSource(provenance: AdoptedUnverified, dirty: False, expected: "1.0.1")
Memex.Portal.Shared.Test.RegistryStatesTheServedBuildsModuleVersionTest ‑ TheServedBuildsVersion_ComesFromTheBuild_NeverFromTheSource(provenance: AdoptedVerified, dirty: False, expected: "1.0.1")
Memex.Portal.Shared.Test.RegistryStatesTheServedBuildsModuleVersionTest ‑ TheServedBuildsVersion_ComesFromTheBuild_NeverFromTheSource(provenance: AdoptionRefused, dirty: True, expected: null)
Memex.Portal.Shared.Test.RegistryStatesTheServedBuildsModuleVersionTest ‑ TheServedBuildsVersion_ComesFromTheBuild_NeverFromTheSource(provenance: Compiled, dirty: False, expected: null)
…

♻️ This comment has been updated with latest results.

rbuergi and others added 2 commits October 15, 2026 21:25
… review)

ModuleVersionOfBuild read the version off provenance and the live source, so
adopt 1.0.1 -> compile 1.1.1 -> delivery hold for 1.1.2 labelled the 1.1.1
bytes 1.0.1 (the hold relabels a compiled build StaleAdopted and
AdoptedModuleVersion survives the compile), and an @@-include-only edit
(invisible to IsDirty) labelled them with the newer CurrentModuleVersion.

NodeTypeDefinition now records BuiltModuleVersion + BuiltModuleVersionMvid in
the same write that names the bytes (adoption and ApplyCompileSuccess); the
version counts only while its MVID is LatestAssemblyMvid. Historical records
fall back only for judged adoptions, otherwise unknown. Both fields are masked
operational members and mirrored on the compile-state satellite.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 15, 2026 19:30
@meshweaver-cloud
meshweaver-cloud Bot disabled auto-merge October 15, 2026 19:30
@rbuergi
rbuergi enabled auto-merge October 15, 2026 19:34

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.

🟡 Changes recommended

In-flight input changes and same-key assembly lookups can still associate bytes with the wrong module version.

2 open findings
1 resolved since last review

🧠 Review effort: Balanced

Comment thread memex/Memex.Portal.Shared/Api/PluginBundleEndpoints.cs Outdated
Comment thread src/MeshWeaver.Compiler.Pipeline/NodeTypeCompilationHelpers.cs Outdated
# Conflicts:
#	src/MeshWeaver.Compiler.Pipeline/NodeTypeCompileState.cs
#	src/MeshWeaver.Compiler.Pipeline/PrebuiltAssemblySeeder.cs
#	src/MeshWeaver.Graph.Contract/NodeTypeDefinition.cs
#	src/MeshWeaver.Mesh.Contract/NodeTypeOperationalContent.cs
Copilot AI balanced review requested due to automatic review settings October 16, 2026 05:32
@meshweaver-cloud
meshweaver-cloud Bot disabled auto-merge October 16, 2026 05:32
@rbuergi
rbuergi enabled auto-merge October 16, 2026 05:33

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.

🟡 Changes recommended

A fallback compilation path can still label newly compiled bytes with a historical adoption’s module version.

3 open findings

🧠 Review effort: Balanced

Comment on lines +542 to +545
return def.BuiltModuleVersionMvid is null
&& def.BuildProvenance is BuildProvenance.AdoptedVerified or BuildProvenance.AdoptedUnverified
? def.AdoptedModuleVersion
: null;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4803bf9. NodeTypeContractHandler.ApplyResolvedSuccess now maintains the version/assembly pair. When the resolved MVID differs from the record's LatestAssemblyMvid (new bytes, e.g. the store-miss or foreign-identity branches that compile directly), it writes BuiltModuleVersion = null and BuiltModuleVersionMvid = <new MVID>. That is unknown keyed to the new bytes, so ModuleVersionOfBuild no longer falls back to the adoption's surviving AdoptedModuleVersion. A hydrate of the bytes the record already names keeps the pair, so the #2895 no-op equality holds (HydrateIsNotACompileTest stays green).

Regression: BuiltModuleVersionFollowsTheBytesTest.ALocalCompileAfterAnOldAdoption_NoLongerAdvertisesTheAdoptedVersion starts from a historical AdoptedVerified record at 1.0.1 with no pair and applies a fresh local compile through ApplyResolvedSuccess. Control: AHydrateOfTheNamedBytes_KeepsTheirVersion. Negative control, with the pair write removed: Assert.Null() Failure: Expected: null Actual: "1.0.1".

…he bytes' own module version

- PluginBundleEndpoints: own lane serves the build the record NAMES (TryGetBuildPath by
  LatestAssemblyPath + LatestAssemblyMvid); the record's version is stated only when the
  served file's MVID is the record's, else unknown (review 4287471936).
- ApplyCompileSuccess: the version stamped is the compiled INPUT's, and unknown when any
  compile input (configuration, module version, fingerprint, includes, source versions)
  moved during the compile (review 4287471990).
- ApplyResolvedSuccess: new bytes record an unknown version keyed to their MVID, so a local
  compile replaces an old adoption's version claim (review 4291278201).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 16, 2026 05:54
@meshweaver-cloud
meshweaver-cloud Bot disabled auto-merge October 16, 2026 05:54
@rbuergi
rbuergi enabled auto-merge October 16, 2026 05:55

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.

🟡 Changes recommended

Historical and cached version metadata can still mislabel builds, and the assembly lookup performs filesystem reads outside the I/O pool.

2 open findings
2 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity Validate module version provenance before stamping compiled bytes

src/​MeshWeaver.Compiler.Pipeline/​NodeTypeCompilationHelpers.cs:4146

Matching these cached definitions does not prove that CurrentModuleVersion describes the compile input. On an active type, changing the partition's content.version and only the type's Configuration does not refresh that field: InstallSourcesWatcher filters own-node changes to Sources/Tests and reads the root version only on a source emission (lines 2234–2344). A requested compile can therefore build the new configuration while both definitions still contain the previous release's version, which this branch stamps onto the new bytes. Capture authoritative module-version and input evidence for the compile, or leave the version unknown when that association cannot be established. Add a live-mesh regression that changes the root version and configuration rather than manually setting CurrentModuleVersion.

Medium severity Do not infer versions for unkeyed historical records

src/​MeshWeaver.Compiler.Pipeline/​PrebuiltAssemblySeeder.cs:545

The new contract-handler stamp protects future compiles, but this fallback still mislabels records compiled before rollout. Previously, ApplyResolvedSuccess could replace an adopted assembly with locally compiled bytes while retaining AdoptedVerified/AdoptedUnverified and AdoptedModuleVersion. Such a record has no keyed version pair, so this branch can advertise locally compiled 1.1.1 bytes as the historical adoption's 1.0.1. Matching the served MVID to the record does not establish that version. Return unknown for unkeyed historical records unless independent build evidence establishes their version, and update the legacy tests and documentation accordingly.

🧠 Review effort: Balanced

Comment on lines +2084 to +2087
=> (servesOwnLane
? store.TryGetBuildPath(
nodePath, version, definition.LatestAssemblyPath, definition.LatestAssemblyMvid)
: store.TryGetAssemblyPath(nodePath, version))
@rbuergi
rbuergi merged commit 12fee82 into main Oct 16, 2026
63 of 65 checks passed
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.

Fix buildProvenance AdoptedUnverified naming a module version whose build was never baked

2 participants