Skip to content

fix(#4367): a dropped native or RID-specific managed asset REFUSES the pack — unless something named carries it - #4444

Merged
rbuergi merged 3 commits into
mainfrom
fix/4367-dropped-native-refuses-the-pack
Sep 15, 2026
Merged

rbuergi merged 3 commits into
mainfrom
fix/4367-dropped-native-refuses-the-pack

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Closes #4367 — the last stage of #4126. Follow-up: #4445 (arming the container-lane refusal). Draft on purpose: held for review before undrafting (undrafting arms auto-merge).

What changes

  1. module-pack --deps-closure REFUSES (exit 2, nothing written) the two shapes a bundle cannot carry by derivation, unless something the module NAMES carries them. The two shapes are a RID-specific MANAGED assembly and a native at a layout the loader does not probe.
  2. The remedy is reachable in the shared lane. A module declares its carrier in its own csproj. node-repo-module-pack.yml's sdk path reads it and appends the flag.
  3. The container lane NAMES both shapes as warning: lines in the same wording, and does not refuse. Arming it is Arming the container-lane refusal is blocked on the FINDING, not on a wave: a RID-specific asset for a RID the bundle does not target is not uncarried (#4367 follow-up) #4445.

Why Closes #4367

#4367 asked to flip ModulePackCommand's two warnings to a non-zero exit, armed on a measured wave, with a test and a negative control per shape. That is done, and the prescribed remedy is now a step the only lane that runs it can take. The container lane was explicitly "not part of this" in #4367. It no longer drops silently: it names both shapes in the same words, so its own wave can be measured. Its refusal is #4445, on #4367's criterion.

🚨 The refusal is satisfiable by its own advice

deps.json keeps declaring the asset after the fix, so a refusal keyed on the findings alone would refuse the fix too. A finding is refused only when nothing the module named carries it (ModulePackCommand.CarrierOf):

finding lifted by (local) lifted by (shared lane: the module's csproj)
RID-specific managed …/<file> --with <file>, named and in the bundle. The RID-agnostic copy that merely rides does not count <MeshWeaverPackWith Include="<file>" />
unprobed native for <rid> --with-native runtimes/<rid>/native/<file>, or --with <file> (flattened, the loader's last probe) <MeshWeaverPackWithNative Include="runtimes/<rid>/native/<file>" />, or <MeshWeaverPackWith Include="<file>" />

Comparisons are ordinal for natives and ignore case for assemblies. A native whose RID cannot form the probed layout is offered only the flat carrier; no slot is ever invented.

The refusal prints the csproj lines to paste, per finding: a Copy after Publish plus the item. A portable publish (what the lane packs) lays every runtimeTargets asset out at its declared key, so no package-cache path is needed:

<Target Name="MeshWeaverPackCarry_System_IO_Ports_unix_System_IO_Ports_dll" AfterTargets="Publish"><Copy SourceFiles="$(PublishDir)runtimes/unix/lib/net9.0/System.IO.Ports.dll" DestinationFolder="$(PublishDir)" /></Target><ItemGroup><MeshWeaverPackWith Include="System.IO.Ports.dll" /></ItemGroup>

The lane's read (sdk path, right after --deps-closure) runs dotnet msbuild <csproj> -getItem:MeshWeaverPackWith -getItem:MeshWeaverPackWithNative, under the build's properties.

  • SDK support: I measured the JSON shape on 10.0.400, and -getItem has existed since SDK 8. The pack job installs 10.0.x.
  • Output: each item becomes --with / --with-native in $packargs.
  • Failure handling: a failed evaluation, or an answer that is not the JSON shape, is RED, never "declares none". module-pack refuses a wrongly shaped value or a missing file.

Measured end to end on the real SDK (10.0.400). The module references System.IO.Ports 9.0.9, which has a real RID-specific managed asset, plus a local package declaring runtimes/linux-x64/nativeassets/net10.0/libodd.so.

  1. The pack was refused with 3 findings.
  2. I pasted two snippets verbatim from the refusal and republished.
  3. I ran the workflow's own extracted block with real -getItem. It produced --with System.IO.Ports.dll --with-native runtimes/linux-x64/native/libodd.so.
  4. The pack exited 0. The bundle's System.IO.Ports.dll is byte-identical to the unix copy, and libodd.so sits at the probed slot.

Measurement the SDK refusal was armed on

repo run bundles on --deps-closure (the refusal's population) occurrences
MeshWeaver.Plugins 34939850754 41 3 (38 container-built) 0
MeshWeaver.SocialMedia 34941532528 1 1 0

Container lane: NAMED, not refused

Until now its derivation dropped both shapes without a line:

  • NativeContributions.DeclaredBy filtered an unprobed native out of the image's native section and the shelf's runtimeTargets;
  • the shelf, a portable publish, had its RID-specific managed assets read past.

Now:

  • NativeContributions.UncarriedBy names both shapes;
  • PrivateClosure.Result.Uncarried carries them;
  • ProjectBuild.ReportUncarried prints one warning: line each.

The finding sentence is one spelling shared by both lanes (MeshWeaver.Plugin.Packaging.UncarriedAssetFindings), so one grep measures both. The remedy it offers is one this lane can take: build on the sdk path and declare the csproj item. The image cannot drop a RID-specific managed asset, because as a RID-specific publish it has already resolved that RID's copy into runtime.

Red/green

NC1 through NC4 each rebuilt the test project. NC-C and NC-D mutate the workflow and re-run the harness.

test red under green
DepsClosure_ARidSpecificManagedAsset_NothingNamesIt_RefusesThePack NC1 arming reverted ✅
DepsClosure_AnUnprobedNative_NothingCarriesIt_RefusesThePack NC1 ✅
DepsClosure_AnUnprobedNative_IsNotCarriedBySomethingNamedThatIsNotIt ×2 NC1 ✅
DepsClosure_ARidSpecificManagedAsset_NamedWithWith_Packs NC2 naive refusal ✅
DepsClosure_AnUnprobedNative_LaidOutAtTheProbedSlotWithWithNative_Packs NC2 ✅
DepsClosure_AnUnprobedNative_FlattenedAndNamedWithWith_Packs NC2 ✅
DepsClosure_TheSharedLaneCsprojLinesInTheRefusal_WhenApplied_LiftIt ×2 NC3 csproj lines stripped ✅
DepsClosureTest.ANativeWhoseRIDIsUnknown_IsOfferedOnlyTheFlatCarrier_NeverAnInventedSlot NC3 ✅
NativeClosureTest: APORTABLERecordsUncarriedShapesAreNAMED…, ARIDSpecificImagesNativeAtAnUnprobedKey_IsNamedToo, EveryNamedFindingIsOneWARNINGLine, BothLanesNameTheSameFinding_inTheSameWords NC4 container naming reverted ✅
test-module-pack-with.py (8 cases) NC-C read deleted: 8/8 red; NC-D append deleted: the positive case and the falsification arm red ✅

My first NC2 attempt didn't compile and ran against a stale binary. I discarded it and re-ran.

Builds, runs, guards

dotnet build -c Release -warnaserror, one project per invocation, gave 0 Warning(s) and 0 Error(s) for each of: MeshWeaver.Plugin.Packaging, MeshWeaver.Plugin.Build, MeshWeaver.PluginTester, MeshWeaver.Documentation, MeshWeaver.Graph.Test, MeshWeaver.PluginTester.Test, MeshWeaver.Compiler.Pipeline.Test.

class / harness result
ModulePackCommandTest 41/41
DepsClosureTest 16/16
NativeClosureTest 12/12
LaneClosuresAgreeTest 2/2
ModuleLoadLineNamesTheBuildBehindTheBytesTest 5/5
test-module-pack-with.py 8/8

Workflow guards all green: check-workflow-timeouts (87 jobs, 0 violations), check-workflow-yaml-keys, check-workflow-permission-pairing (both forms), check-pr-secret-preflight, check-workflow-shell (0 live), test-module-pack-test-runner.py, and test-module-asset-landing.py. check-record-signatures was also green: nothing public was removed, and records only gained init properties.

No What's New entry: a portal user cannot notice this. It is a pack-tool refusal with zero current victims plus a build-lane log line, and module authors read the refusal itself and Doc/Architecture/Modules.

🤖 Generated with Claude Code

…e pack — unless something named carries it

The last stage of #4126. `module-pack --deps-closure` used to print two findings as `warning:`
and pack anyway, leaving the module on the host-supplies-it fallback: a RID-specific MANAGED
assembly (the flat closure has one slot per name and no way to choose a RID) and a native at a
layout the loader never probes. It now exits 2, writes nothing, and names the package, the path
and the exact value that carries it.

Armed on a measurement (the #3240 shape): the full module-pack wave of both publishing repos
printed zero of either finding — MeshWeaver.Plugins run 34939850754 (41 bundles, 3 on this
--deps-closure path) and MeshWeaver.SocialMedia run 34941532528 (1 bundle, on it).

The refusal is SATISFIABLE by its own advice. deps.json keeps declaring the asset after the
author follows the advice, so a refusal keyed on the findings alone would refuse the fix too.
A finding is refused only when nothing the caller named carries it:
  * `--with <file name>` (named, and in the bundle as written) — for either shape. A
    RID-agnostic copy that merely rides by derivation does NOT count: that is the silent
    "which RID's copy" choice the refusal stops;
  * for a native, a payload at exactly runtimes/<its rid>/native/<its file name> — from
    --with-native or from the derivation — the slot the loader probes for that RID and name.

DepsClosure.Result gains `Uncarried` (an init property — no constructor change) carrying the
findings as data; `Warnings` is now exactly their `Describe()` text. A RID fallback reads the
key's second segment only under `runtimes/`, so a ridless native is never advised an invented
slot.

Not armed on the container lane, and why: its derivation (NativeContributions.DeclaredBy) drops
both shapes WITHOUT A LINE, so no measurement of it exists — arming there would be a refusal on
faith. Recorded in Modules.md with the lane's missing per-module --with hook.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 15, 2026 17:48
@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

263 tests  ±0   263 ✅ ±0   2m 38s ⏱️ -1s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 85fb824. ± Comparison against base commit d97160f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 483 tests  ±0   1 483 ✅ ±0   2m 57s ⏱️ +4s
    2 suites ±0       0 💤 ±0 
    2 files   ±0       0 ❌ ±0 

Results for commit 85fb824. ± Comparison against base commit d97160f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

925 tests  ±0   925 ✅ ±0   3m 30s ⏱️ -3s
  2 suites ±0     0 💤 ±0 
  2 files   ±0     0 ❌ ±0 

Results for commit 85fb824. ± Comparison against base commit d97160f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

    4 files  ± 0      4 suites  ±0   5m 8s ⏱️ -1s
1 938 tests +12  1 746 ✅ +12  192 💤 ±0  0 ❌ ±0 
1 939 runs  +12  1 747 ✅ +12  192 💤 ±0  0 ❌ ±0 

Results for commit 85fb824. ± Comparison against base commit d97160f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    6 files  ±0      6 suites  ±0   5m 44s ⏱️ +7s
2 239 tests ±0  2 239 ✅ ±0  0 💤 ±0  0 ❌ ±0 
2 243 runs  ±0  2 243 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 85fb824. ± Comparison against base commit d97160f.

♻️ This comment has been updated with latest results.

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 critical native path matching issue remains unresolved, and the required What's New entry is missing.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Hardens module-pack --deps-closure against silently dropped managed and native assets.

Changes:

  • Adds structured uncarried-asset detection and carrier-aware refusals.
  • Adds refusal, remediation, and derivation tests.
  • Documents behavior and remaining scope.
File summaries
File Summary
test/MeshWeaver.Graph.Test/ModulePackCommandTest.cs Tests refusal and remediation scenarios.
test/MeshWeaver.Graph.Test/DepsClosureTest.cs Tests structured findings and guidance.
src/MeshWeaver.Plugin.Build/ModulePackCommand.cs Implements carrier-aware refusal logic. Critical (3 votes): native lookup is case-insensitive despite ordinal path matching, potentially bypassing refusal for differently cased entries.
src/MeshWeaver.Plugin.Build/DepsClosure.cs Detects and describes uncarried assets.
src/MeshWeaver.Documentation/Data/Architecture/Modules.md Documents packaging behavior. Nit (1 vote): add the required What's New Fix entry.
Review details

Suppressed comments (1)

src/MeshWeaver.Documentation/Data/Architecture/Modules.md:764

  • This changes the externally consumed module-pack CLI from warning-and-success to an exit-2 refusal, so module authors can directly observe the fix even if portal end users cannot. Please add the required What's New Fix entry for this user-visible behavior rather than treating the absence of a portal UI change as an exemption.
**What is refused.** `module-pack --deps-closure` exits 2, writes nothing, and prints one `error:`
line per finding — naming the package, the declared path and the value that carries it — for each
asset the module's own closure DECLARES and the derivation cannot carry: a native at a layout the
loader does not probe, or a RID-specific MANAGED assembly. Both used to print `warning:` and pack,
leaving the module on the "the host happens to supply it" fallback.
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Lite

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

Comment thread src/MeshWeaver.Plugin.Build/ModulePackCommand.cs Outdated
@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

1 121 tests  +4   1 119 ✅ +4   6m 20s ⏱️ +12s
    3 suites ±0       2 💤 ±0 
    3 files   ±0       0 ❌ ±0 

Results for commit 85fb824. ± Comparison against base commit d97160f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   18 files  ± 0     18 suites  ±0   26m 20s ⏱️ +23s
7 969 tests +16  7 775 ✅ +16  194 💤 ±0  0 ❌ ±0 
7 974 runs  +16  7 780 ✅ +16  194 💤 ±0  0 ❌ ±0 

Results for commit 85fb824. ± Comparison against base commit d97160f.

♻️ This comment has been updated with latest results.

rbuergi and others added 2 commits September 15, 2026 20:07
… the container lane NAMES both shapes

Review of #4444: the refusal prescribed --with / --with-native, but node-repo-module-pack.yml's
sdk path passes only --deps-closure — so the first CI-built module to trip it would meet a
message naming a step nobody can take in the only lane that runs it.

1. A per-module remedy the shared lane applies. A module declares its carrier in its OWN csproj:
   <MeshWeaverPackWith Include="<file>" />  -> --with <file>
   <MeshWeaverPackWithNative Include="runtimes/<rid>/native/<file>" />  -> --with-native <path>
   The sdk path reads them with `dotnet msbuild -getItem` (JSON on SDK >= 8; the lane runs 10.0.x)
   under the build's properties and appends one flag per item; a failed evaluation or an answer
   that is not the JSON shape is RED, never "declares none". The refusal now prints the csproj
   lines to paste per finding — a Copy after Publish from the key the portable publish lays the
   file out at, plus the item — while still naming --with / --with-native for a local run.
   Measured end to end on SDK 10.0.400: System.IO.Ports 9.0.9 (a real RID-specific managed asset)
   plus a native at runtimes/linux-x64/nativeassets/ — refused, lines pasted verbatim, republished,
   the lane's own block run with real -getItem, packed (bytes = the unix copy; native at the slot).
   Executed in CI by .github/scripts/test-module-pack-with.py (wired into dotnet-test.yml).

2. The container lane NAMES both shapes as warnings, never refuses. NativeContributions.UncarriedBy
   names what DeclaredBy filtered out silently (image `native` section, shelf runtimeTargets) and
   the shelf's RID-specific managed assets; PrivateClosure.Result.Uncarried carries them; the
   builder prints one `warning:` line each. The finding wording is ONE spelling shared by both
   lanes (MeshWeaver.Plugin.Packaging.UncarriedAssetFindings), so one grep measures both. Arming
   a refusal there is #4445, on the same criterion #4367 was armed on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… spelling reaches the bundle

CarrierOf matched the --with name with the kind-specific comparer (ordinal for a native) but
looked it up in the closure ignoring case. `--with` joins the closure only when no
case-insensitive match is already there, so a native `LIBODD.DLL` named beside a managed
`libodd.dll` never enters it — yet the ignore-case lookup found the managed file and read the
native as carried: the refusal bypassed for bytes that never reach the bundle (Copilot, thread
4018652584). Both lookups now use the same comparer. Ordinal cannot miss a native that IS
carried: one that entered via --with sits in the closure under exactly the spelling it was named
with.

Pinned by DepsClosure_ANativeNamedWithWith_DifferingOnlyInCaseFromAManagedFileInTheClosure_IsRefused,
which asserts the refusal's own words (so it cannot pass by way of "--with file not found" on a
case-sensitive filesystem); red with the ignore-case lookup put back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rbuergi
rbuergi marked this pull request as ready for review September 15, 2026 18:26
@rbuergi
rbuergi added this pull request to the merge queue Sep 15, 2026
Merged via the queue into main with commit 25596ae Sep 15, 2026
37 checks passed
@rbuergi
rbuergi deleted the fix/4367-dropped-native-refuses-the-pack branch October 10, 2026 13:10
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.

A dropped native payload should REFUSE the pack, not warn — arm it on a measured publish wave (#4126 stage 5)

2 participants