Skip to content

[ModuleLoad] cannot name the commit a bundle was built from, nor the assembly a NodeType's content type resolved to — a stale adopted build and a stale registry shelf are indistinguishable from the consumer #4158

Closed

Description

@rbuergi

What is missing

[ModuleLoad] prints, per module: the path, source=, mvid= and written=. Two questions that decided a night of production diagnosis are NOT on that line, and nothing else answers them from the consumer:

  1. Which commit the loaded bundle was built from. On 2026-09-10 (MeshWeaver.Plugins#1585) the loaded MeshWeaver.AI@a7971ab5… had the newest generation, the newest written=, and types that predated two merged PRs. mvid and written said "newest"; only the served JSON schema said "old". The reading "the registry serves stale bytes" was written down, acted on (three RefreshModules, two restarts), and later falsified by /health (bundle_adoption: … AI: FrameworkDeclined — the bundle was never adopted; the previously adopted build kept serving under the same-MAJOR rule of fix(compile): a NodeType never errors because of delivery — last build keeps serving, keyed on module-version compatibility (#3583) #3844).
  2. Which assembly a NodeType's content type actually resolved to. The /schema/<Type> endpoint and the serializer answer from whatever assembly won the TypeRegistry, and [ModuleLoad] names the assembly that was loaded, which is not the same statement when two builds of one assembly name are in the process (A downstream publication republishes its upstream's modules and ships image siblings — two builds of one assembly, and every skinned StackControl falls back to HTML #3732's shape).

Ask

  • Record the built-from commit (and the framework identity it was compiled against) in the bundle manifest at pack time, and print both on [ModuleLoad] beside mvid/written — so "the generation is newest" and "its types are current" stop being read as one fact.
  • Make the per-NodeType diagnostics name the assembly (path + mvid) the content type resolved to — the thing /schema/<Type> is actually serialising from — so a stale adopted build and a stale registry shelf can be told apart from the consumer.

Both are additive diagnostics; neither changes what an installation runs. Measured context and the 05:52Z falsification: MeshWeaver.Plugins#1585 (closed on the portal outcome). Adjacent: #3583 (adoption vs sweep count), #3732 (two builds of one assembly name), #3844 (stale-but-serving rule).

🤖 Generated with Claude Code

https://claude.ai/code/session_01BRLdhAiX5DjdZC2WUfPg9E

Activity

  1. rbuergi commented on Sep 13, 2026

    @rbuergi
    ContributorAuthor

    2026-09-13 check — precisely stated; half the data exists one level up

    On main (99a3600de): ModuleLoadReport.cs:120 prints source=, mvid=, written= and nothing else per module — the built-from commit is not on the line. It DOES exist per publication: SealedPublicationIndex.SourceCommit reads source-commit.txt (src/MeshWeaver.Hosting/SealedPublicationIndex.cs:14), so the first ask is a plumbing move (publication → bundle manifest → load line), not new data. The second ask (which assembly a NodeType's content type resolved to) has no source today. Both additive; left open, no work in this pass.

  2. rbuergi commented on Sep 13, 2026

    @rbuergi
    ContributorAuthor

    Ask 1 is built (#4195, draft, green) — ask 2 still has no source

    Ask 1. [ModuleLoad] now carries built-from=<commit> and framework=<identity> beside
    mvid=/written=. framework= needed no new data — ModuleActivationEntry.FrameworkMvid has been
    recorded since #3154 and was simply never printed. built-from= is plumbed end to end, additively,
    public surface growing by init properties only: module-pack --source-commit → the bundle manifest's
    sourceCommit → BundleReader.Manifest.SourceCommit → ModulePublish.Accepted /
    PluginBundleClient.LandFromBundle → ModuleLandingService → ModuleActivationEntry.SourceCommit
    (and PreviousSourceCommit, so a fallback generation names its own build rather than the head's).

    Absence prints (unrecorded) and is never derived from the version, the generation, the MVID or the
    path — proved able to fail, not just asserted: with the fix in place, injecting
    entry.SourceCommit ?? entry.Version turns the negative control red with
    Expected: "(unrecorded)" / Actual: "1.4.0".

    Two hops still read (unrecorded), both deliberate and both now written down in
    Doc/Architecture/Modules → "The file is not the source" (committed with the change, not left in
    this thread):

    1. The CI producer hop. node-repo-module-pack.yml does not pass --source-commit, for two
      measured reasons. A satellite pins the lane at @main while the pack tool is published from
      the resolved sealed platform-ref, which lags main — an unconditional new flag would exit-2
      (unrecognised argument) every satellite's pack jobs for that whole window. And github.sha is
      the wrong value anyway: the lane reuses content-addressed module builds from earlier runs
      (module-build-ledger.py), so the run's commit is not the commit that built those bytes. Stamping
      it would put a newer commit on older bytes — this issue's own defect with one more field to be
      misled by. The lane must take the commit from the build's provenance.
    2. The registry's re-pack. A consumer landing a producer-packed bundle records the commit, and a
      registry's own [ModuleLoad] names it (its shelf entry is written by ShelveModule). But when a
      registry re-serves a shelved module it composes a fresh manifest, and that composer is in the
      MeshWeaver.Plugins route rather than core src/ — one additive line there, enabled by feat: [ModuleLoad] names the commit a module was built from, and what it was built against (#4158 ask 1) #4195.

    Ask 2 — which assembly a NodeType's content type actually resolved to — has no source today, as
    the 2026-09-13 check said, and #4195 does not invent one. Leaving this issue open on it; #4195 uses
    Refs, not Fixes.

    🤖 Generated with Claude Code

  3. rbuergi commented on Sep 13, 2026

    @rbuergi
    ContributorAuthor

    Ask 2 is DONE — the source exists, and it is a property of the BYTES. Merged in #4236.

    The brief on this was: find whether a correct source exists — the bundle's sealed provenance,
    SealedPublicationIndex.SourceCommit, the assembly's MVID, anything that is a property of the bytes
    rather than of the run. One does, and it was already in hand and being discarded.

    Where it was

    • ContentDegradationRegistry.IsResolvable resolves the content Type and throws it away with
      out _ (ContentDegradationRegistry.cs:113-114).
    • DataExtensions.GenerateJsonSchema holds the very Type it is about to serialise from
      (DataExtensions.cs:3840) and passes on only CollectionName.

    From a resolved Type, three coordinates are properties of the loaded bytes and cannot be stale:

    field source
    mvid Assembly.ManifestModule.ModuleVersionId — stamped INTO the bytes by the compiler
    assembly Assembly.Location — the file this process actually loaded
    collectible Assembly.IsCollectible — a runtime compile has no file and a collectible context; a shipped module has both the other way round

    That last pair is the #3732 discriminator: it is what separates two builds of ONE assembly name in
    one process, which is the state in which a stale ADOPTED build and a stale registry SHELF read
    identically from a consumer — the night this issue was filed about.

    What shipped

    get_diagnostics carries a contentType block on every branch (Ok, Error, Compiling,
    Unavailable, Foreign, Unknown, and the degraded-JsonElement path, which needs it most), read from
    the live IMeshContentTypeRegistry at the moment of the call. Nothing gates on it — a reading, like
    ask 1's built-from=.

    Watched failing on unmodified main before the fix, both cases, with the payload quoted in #4236.

    Three rules, each held by a case that fails without it:

    1. Absence is PRINTED, never inferred. unresolved (asked, nothing registered) and not-asked
      (no registry in this process) are two different sentences; the unresolved one names the
      consequence — its views render empty.
    2. Every key is written, always. Measured on the first attempt: the hub's options stamp a
      polymorphic $type on a NESTED object (putting <>f__AnonymousType156[…]into an envelope people read) and their ignore condition DROPS null and default members —typeName, assembly, mvidand afalse collectibleall vanished. The block is now an explicitJsonObject`.
    3. It is read from the TYPE, never from the path. The control loads the test assembly's own
      bytes into a collectible AssemblyLoadContext, so the same full name is present twice; the two
      readings must differ.

    The two residues from ask 1 are unchanged, and both are still (unrecorded)

    Neither is fixed here and neither is new — restated so this issue's closure does not imply they are:

    1. The CI producer hop. node-repo-module-pack.yml still does not pass --source-commit,
      because github.sha is the wrong value: the lane reuses content-addressed module builds from
      earlier runs, so the run's commit is not the commit that built those bytes. The lane must take it
      from the BUILD's provenance. (The equivalent one level up — reading a publication's real source
      from the bake's own receipt instead of head_sha — is now an option on the canonical resolver,
      ci: the publication's OWN source is a SECOND option on the canonical resolver (#4171) #4238.)
    2. The registry's re-pack, which composes a fresh manifest in the MeshWeaver.Plugins route —
      one additive line there, enabled by feat: [ModuleLoad] names the commit a module was built from, and what it was built against (#4158 ask 1) #4195.

    Both are written up in Doc/Architecture/Modules → "The file is not the source"; ask 2's half is
    beside it as "…and the RECORD is not the resolution".

    Closing: ask 1 shipped in #4195, ask 2 in #4236.

    🤖 Generated with Claude Code

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