Skip to content

feat(build-project): embedded resources, with the SDK's manifest names - #2860

Closed
rbuergi wants to merge 15 commits into
mainfrom
feat/build-project-embedded-resources
Closed

rbuergi wants to merge 15 commits into
mainfrom
feat/build-project-embedded-resources

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Cut from origin/integration/core-batch, targeting main. #2850 (feat/cli-build-project) introduced this builder and is not on main yet — it sits inside batch PR #2853, which is blocked on an unrelated regression. This branch needs #2850's three files to work against, so it is based on the batch and must land after it. Nothing here was pushed to #2850's branch or to the batch branch.

<EmbeddedResource> items are embedded now, each under the manifest name the SDK's own CreateCSharpManifestResourceName would have produced.

Measured over the 54 non-test projects in MeshWeaver.Plugins/src, this was the single biggest coverage gap: 19 failed on nothing else, most on one line — <EmbeddedResource Include="Data\**\*.md">.

Why #2850 refused it, and why that was right

Its PR body says a wrong manifest-resource name "is exactly the silent difference the design exists to prevent". That judgement was correct and this PR does not relax it. A wrong resource name fails nothing: the assembly compiles, the emit is verified, the DLL loads — and Assembly.GetManifestResourceStream(name) returns null at run time, in another process, later. Core's own MeshWeaver.Messaging.Hub.csproj already carries a comment describing that exact outcome for its strings.*.json: "the build succeeds, the main assembly carries ZERO manifest resources, every lookup falls through to the key-fallback path, and the UI renders raw chat.new tokens."

So every rule here was established by measurement, not recall — 15 probe projects built with the real .NET SDK, the names read back out of the emitted PE's ManifestResource table. Where a rule could not be matched, the construct is refused by name.

The rules, and how each was measured

The SDK's pipeline, reproduced step for step: AssignTargetPath assigns %(TargetPath) (Microsoft.Common.CurrentVersion.targets says so out loud — "AssignTargetPath generates TargetPath metadata that is consumed by CreateManifestResourceNames target for manifest name generation"), AssignCulture splits culture-carrying items off towards satellites, CreateCSharpManifestResourceName turns the survivors into $(RootNamespace).<mangled dir>.<file name>.

rule measured
the DIRECTORY is mangled, the FILE NAME is not Data\with-dash\Three.md → …Data.with_dash.Three.md; Weird-File.Name.md keeps hyphen and dot
a leading digit is PREFIXED, not replaced 9digits → _9digits
a dot in a directory is a SEPARATOR Dot.9Dir → Dot._9Dir — each half mangled alone
a segment reducing to one _ DOUBLES -- and _ both → __; a project with both sibling dirs fails the real SDK with CS1508 — that failure is the measurement
$(RootNamespace) defaults to the PROJECT name not $(AssemblyName) — proven with a project whose two differ; empty ⇒ no prefix at all
%(TargetPath) > %(Link) > item spec a Link renames a file already inside the project
a file OUTSIDE the project loses its directory ..\shared\Shared.md → <ns>.Shared.md — no .., no shared
resources are emitted PUBLIC ManifestResourceAttributes.Public, every one
the SDK default glob is **/*.resx reproduced, so a stray .resx is a refusal not a silent omission
a missing LITERAL include is CS1566; an empty glob is legal measured both ways
.resources needs no special case the SDK's strip-and-re-append branch is arithmetically identical, and mangles the directory too

Refused by name, each with its own --accept token

construct why a name cannot be promised
.resx / .restext the NAME is reproducible; the CONTENT needs resgen. Embedding the XML under the .resources name compiles green and throws at run time. Refusing loudly is the honest outcome
a CULTURE in the file name the SDK routes it to a SATELLITE assembly and out of the main one — and an explicit LogicalName does not rescue it (measured). WithCulture="false" is the project-side fix and needs no acceptance
DependentUpon the name becomes the first CLASS in that file, fully qualified, extracted by MSBuild's own tokenizer — which skips struct/interface/enum, takes record, drops generic arity
ManifestResourceName makes the SDK skip its own naming task, which also sets %(LogicalName), so csc falls back to the bare file name — the metadata does not do what it appears to
an outside-the-project GLOB this evaluator expands from the project down, so it matched nothing, silently — an empty glob is legal
LinkBase outside the project synthesizes a %(Link) this evaluator does not compute. Ignored inside the project, so only the outside case is refused
the build's OWN OUTPUT Include="bin\…\$(AssemblyName).xml" builds green under the real SDK from clean (csc writes /doc: and reads /resource: in one invocation); named and skipped rather than failing the project

Under invariant globalization the runtime reports no predefined culture even for de, so the culture question cannot be answered — the capability is probed, and any dotted-basename resource is refused rather than guessed.

Three findings worth calling out

  1. An explicit %(Culture) beats %(WithCulture)='false' — the opposite of what the names suggest. My first implementation had the order backwards and would have embedded, under a name the SDK never produces, a resource the SDK puts in a satellite.
  2. $(AssemblyName) and $(RootNamespace) were never seeded, so $(AssemblyName) expanded to the empty string. MeshWeaver.Northwind.Domain reads it in an <EmbeddedResource Include="bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml">, which resolved to a file called .xml. Both now default to the project name, exactly where Microsoft.NET.Sdk.props sets them.
  3. A glob reaching outside the project silently matched nothing. The same is true of Compile globs today (pre-existing, not touched here) — worth a follow-up.

Verified by RUNNING

  • 229 tests green (34 new; the suite was 225). MeshWeaver.Graph.Test 1602 green — the EmitPipeline consumers are unaffected.
  • Every resource assertion reads the emitted PE, never this repo's source.
  • Parity test: one probe project carrying every hard case is built by the real SDK and by build-project, and the manifest name sets must be identical. It does not skip when the SDK is missing — a parity test that passes because it could not find its oracle is worse than none.
  • Mutation proofs — three, each reverted: dropping the leading-digit prefix rule → 5 tests red incl. parity; mangling the file name too → 3 red; disabling culture detection → 4 red.
  • Real-world parity, end to end in the container: MeshWeaver.Northwind.Model (11 glob-matched CSVs) built by the real SDK and by build-project inside the combined tester+portal image — all 11 names AND their contents identical, byte for byte (SHA-256 per resource).

The sweep: 9 → 10 of 54, and why that is the honest number

Both columns from the same image (memex-portal-ai@sha256:6f38db08…, the pin in MeshWeaver.Plugins/.github/workflows/ci.yml), --accept targets, no --extra-refs. The "before" column was produced by building the pre-change builder from origin/integration/core-batch into its own image and sweeping it — it reproduces the reported baseline of 9 exactly.

before after
green 9 10
<EmbeddedResource> refused 19 0
Razor/Blazor (CS0115) 12 15
source generators (CS8795) 3 14
additional libraries 5 8
<Protobuf> 2 3
<Import> above the mount 2 2
Aspire <Sdk> element 1 1
other (pre-existing errors) 1 1

🚨 This is not the 9 → ~24 the gap size suggested, and the reason matters. A refusal stops the load, so it MASKS every gap behind it. Those 19 projects were never "19 projects away from green" — they were 19 projects about which nothing further was known. Closing the gap turned one green and revealed the real blocker for the other eighteen: ten want SDK source generators, three want Razor, three want an additional library, one wants protoc. That is why the old sweep counted 3 source-generator failures when the true figure is 14.

In a builder designed to refuse rather than guess, a failure ranking is a ranking of FIRST refusals. Removing the top entry redistributes its count; it does not add it to the green column. Razor (#2856) is now the largest remaining gap at 15, with source generators right behind at 14.

No project regressed, and none fails on an embedded resource any more.

Notes for review

🤖 Generated with Claude Code

rbuergi and others added 14 commits August 30, 2026 22:52
Maintainer, 2026-08-30: "also doc safety etc. same standard as with normal
build … see that you tune it to same level. nullability etc … and doc
references and comment references etc … diagnostic must go to activity."

MEASURED FROM THE OUTSIDE FIRST. A deliberate CS0219 — an unused local — added
to an in-mesh source compiled `ok` with "0 warning(s)". Not a clean build: the
ABSENCE OF A REPORT. `emitResult.Diagnostics` is read only when Success is
false, so every warning a green compile produced was discarded.

That is why in-mesh C# was not held to the standard the compiled half is held
to under -warnaserror. No unused-code warnings, and therefore no doc-comment
and no cref ones either — even though CreateParseOptions has ALWAYS asked for
DocumentationMode.Diagnose. The diagnostics were being produced and thrown away.

They now travel to the compile ACTIVITY, which is where a diagnostic belongs: it
is the record of that compile, what a reader opens, what the Overview/Tests
areas render, and what a runner can stream. A warning that only reached a server
log reached nobody.

The route respects the log-once contract rather than working around it.
EmitPipeline deliberately does not log — logging failures there once
double-counted every compile failure in production (~150 ERROR lines/24h were
~72 real failures logged twice, the duplicate arriving first and context-free,
which is what made "your C# does not compile" read like an emit/IO defect). So
the warnings are RETURNED, on EmittedArtifact, and appended by the same single
funnel that reports a failure — as Warning severity, which ActivityLog.Finish
leaves alone, so surfacing them cannot turn a green compile red on its own.

Both emit paths report the same thing. The in-memory path reads
compilation.GetDiagnostics() explicitly: a warning that appeared only when the
disk cache was on would be a warning that depends on a deployment's caching
mode, which is exactly the kind of difference nobody finds.

Deduped, ordered, and CAPPED at 50 with the remainder COUNTED in the last
entry — one bad using-directive produces hundreds of identical diagnostics, and
an activity that is 400 lines of the same warning is one nobody reads. A
truncation a reader cannot see is a lie about how much was wrong.

Four cases pin it, including the two that matter for a cross-repo refactor: an
unresolvable cref (CS1574/CS1584) is reported, and a clean compile reports
nothing — the empty case only means something now that a non-empty one is
possible.

🚨 NOT CHANGED HERE, deliberately: the compilation OPTIONS. Adding a nullable
context is the other half of "the same level", and it moves
EmitPipeline.OptionsFingerprint — which is reflected over the options, so every
cached assembly in the fleet is invalidated and recompiles. That is an
operational event to schedule, not a rider on a diagnostics fix. This change is
fingerprint-neutral: it reads diagnostics that were already being computed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vRaDpW29KDS9NX1gvCjw3
… ours

Maintainer, 2026-08-30: "we are about to remove all nuget dependencies — for
tests. All repos. Check plugins how we do it."

Checked and measured rather than assumed. The Plugins idiom is stricter than
"no xUnit": across all 206 in-mesh test node sets, `using Xunit` occurs ZERO
times, and so does every other assertion library — including our own
Reactive.Assertions. A test is plain C# that throws, with a local four-line
Expect(bool, string) helper owned by the file; a TestsArea enumerates
(name, Action) cases and renders the pass/fail table, registered with a
LITERAL .WithView("Tests", …) because the gate reads that field as text; the
gate executes it on a probe instance and the Tests-area ratchet holds the
population.

So a test compiles against exactly what the image's framework provides plus
its own type's Source: nothing to restore, nothing to version, nothing that
can drift against the framework under test. What retires with the compiled
test projects is the whole test-dependency graph — xUnit v3, FluentAssertions,
the runner, the .trx machinery, and the restore step feeding them — not just
MeshWeaver.Fixture.

Deliberately-given-up list stated so it is a decision, not a discovery:
parametrised theories, runner-process isolation, IDE test explorer, .trx
artifacts. The gate log and the rendered table are the record; the trade has
been paid 206 times in production without a request for any of the four back.

This also RESOLVES the doc's own open question ("deciding what the in-mesh
unit-test surface is" for module tests): the surface exists, it is this, and
it has 206 production instances.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…as optional

`PackageInstaller` waited up to 30 s for a partition's cover grant while the
wait's own doc comment called that grant optional — "a partition whose node
type does not gate never writes it". On the shape the code itself called
*normal* the query can never emit, so every install burned the entire budget
and then reported success, leaving one Information line worded to cover both
outcomes. Measured in `MeshWeaver.PluginCatalog.Test`: 30.25 s / 30.29 s /
30.33 s / 30.24 s per install and 60.28 s for the test that installs twice —
181 s of a 336 s suite. `PackageInstaller.Install` is the production install
path, so a live mesh paid the same 30 s per non-gating package.

Core cannot ask a node type whether it gates (`Store/Plugin`, `PluginContent`
and `AddPluginGating` live in MeshWeaver.Plugins), which is why the grant is a
well-known PATH. It does not have to: `EnsureDeclaredAccess` is the ONE
access-establishment step of an install and already states, per manifest,
whether it published the partition or left it gated. `DeclaredAccessMarker` is
null exactly when it wrote nothing — the commercial branch — and that is the
only shape a gating pass owes a cover grant. `CoverGrantExpected` is that, and
the watch now runs only there.

- `GatingSettleTimeout` (30 s) → `GatingDetectorBudget` (5 s), renamed for what
  it now is. Justified by measurement: when a grant IS owed it is observable in
  44–55 ms (`GatingDetectorTest` measures the real write→observe latency on a
  live mesh), and phase 1 has already paid the activation, so the only
  outstanding work is one access-table write.
- The two outcomes stop looking identical: the expiry is a WARNING naming the
  missing path and the consequence (the partition denies every viewer,
  including on the Subscribe cover that would sell it); "nothing owed" is Debug.
  Warning, not Error — the expiry proves the grant is not there yet, not that
  it never will be, and `VerifyDeclaredAccess` owns the Error verdict for
  access core itself promised to write.
- Still never fatal, and the query is unchanged: `path:{grant}` exact,
  empty-on-absent, never a stream read (#2229) and never a children listing.

`GatingDetectorTest` pins all three outcomes and is mutation-proven: downgrading
the warning, disabling `CoverGrantExpected`, or restoring the 30 s budget each
turns it red.

Suite: 707/707 green, 5 m 36 s → 2 m 49 s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…er, no SDK, no NuGet

Maintainer directive, 2026-08-30: "the platform builds dll completely without any external
dotnet kit or nuget."

`memex build project <csproj|dir> --image <image>` compiles a .NET project with NO dotnet SDK
and NO NuGet restore, against the assemblies of a MeshWeaver image. The CLI half is only the
trip into the container — it shares ImageRunner (pull retry, digest pin, process launch) with
`build plugin` rather than carrying a second copy. The work is `mw-plugin-test build-project`,
three types in the tool already in the image:

* ProjectFile — evaluates the .csproj WITHOUT MSBuild: properties, items, the default **/*.cs
  glob minus bin/obj, Compile Include/Remove, ProjectReference, PackageReference, implicit
  usings, the target-framework symbol ladder, the SDK's own default NoWarn (1701;1702), the
  nearest Directory.Build.props/.targets/Directory.Packages.props, and every <Import> whose
  condition holds. Anything it cannot reproduce FAILS the load by name; --accept acknowledges
  one. A silently dropped Nullable or NoWarn is a green build that is not the SDK's build.

* ContainerReferenceSet — the C# port of MeshWeaver.Plugins/scripts/container-refs.py, read
  from /app instead of an extracted image: the image's own .deps.json for versions and the
  binding identity, /app plus the container's shared frameworks for the assemblies. Fails
  closed on an unreadable /app, a missing or ambiguous .deps.json, or MeshWeaver assemblies
  that disagree on their binding identity (MeshWeaver#143, caught in the image rather than at
  run time). A package is matched by the ASSEMBLY FILE on disk, never by its id alone; one the
  container does not supply is an ADDITIONAL library, reported by name and never skipped.

* ProjectBuild — sequences the ProjectReference graph on the existing Cascade (a cycle is
  refused up front and named), compiles with Roslyn, and emits through the platform's own
  EmitPipeline. It builds its OWN CSharpCompilationOptions and never touches
  CreateCompilationOptions, which feeds GeneratedInputIdentity.OptionsFingerprint. Every
  progress line and diagnostic is streamed into an ActivityLog (ActivityCategory.Compilation)
  as it is produced; the console is a rendering of that stream.

Measured against memex-portal-ai@sha256:15c49ee over MeshWeaver.Plugins/src: 12 of 54 non-test
projects green, including MeshWeaver.Import (90 sources, 0 warnings under warnings-as-errors)
with ClosedXML + CsvHelper as --extra-refs. The rest are named limitations: Razor/Blazor (15),
SDK source generators (15), <Protobuf> (3), additional libraries (5), portal hosts (3).

Also here: BuildPluginCommand's docker plumbing moves to ImageRunner (and gains `--init`, the
convention every workflow already uses), and MeshWeaver.Compiler grants InternalsVisibleTo to
mw-plugin-test so the emit is SHARED rather than re-implemented.

51 new tests, executed: csproj evaluation (globs, Remove, Directory.Build.props inheritance,
the condition grammar, every loud refusal), reference resolution (assembly-on-disk matching,
the metapackage case, the additional-library case, all five fail-closed paths), ProjectReference
ordering and cycle detection, and the diagnostic standard — an unresolvable cref and an unused
local are asserted to be REPORTED, and the emitted assembly is LOADED and invoked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	src/MeshWeaver.Cli/BuildPluginCommand.cs
…r kills the whole build

The first real `mw-plugin-test build` run on a dev machine died with exit 70
and ZERO bytes on stdout and stderr. Two defects compounded.

Every early fatal was mute: Print lives at the end of Finish, which only the
successful pipeline reaches — LoadSync throwing, no packages, an unknown
package id, LoadExternalModules refusing, and Run's outer Catch all returned
their Report straight to Program, which turned it into an exit code and
nothing else. Run now guarantees at its one seam that a FatalError report
prints (full exception, stack included) and writes report.json when asked;
Finish never sets FatalError, so nothing prints twice.

With the silence fixed the real cause self-identified: LocalNodeRepo.Sweep
eagerly reads EVERY file under the repo root and died on a dead Playwright
symlink (e2e/.auth/profile/RunningChromeVersion) — gitignored, so invisible
to every CI checkout, fatal for exactly the local no-dotnet loop the verb
exists for. The sweep now skips an unreadable file loudly and continues;
such a file crashed both bake paths before, so the shared swept-content
contract cannot fork.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoG4USP5fd8wsxCD7e9q4x
`<EmbeddedResource>` items are EMBEDDED now, each under the manifest name the
SDK's own `CreateCSharpManifestResourceName` would have produced — the single
biggest coverage gap in the SDK-free builder, blocking 19 of the 54 non-test
projects in MeshWeaver.Plugins/src on nothing but
`<EmbeddedResource Include="Data\**\*.md">`.

#2850 refused them deliberately, and the reasoning was right: a wrong
manifest-resource name is exactly the silent difference that design exists to
prevent. The assembly compiles, ships, loads, and GetManifestResourceStream
returns null at run time in another process — no error, no failing test, nothing
a review can see. Core's own MeshWeaver.Messaging.Hub.csproj already carries a
comment describing that outcome verbatim.

So no naming rule here was recalled. Each was established by building a probe
project with the real .NET SDK and reading the manifest-resource table back out
of the emitted PE — including the asymmetry that makes the whole thing
treacherous: the DIRECTORY is mangled into identifiers and the FILE NAME is not.

Supported, measured: the default $(RootNamespace).<mangled dir>.<file name>;
%(TargetPath) beating %(Link) beating the item spec; a file outside the project
losing its directory entirely; LogicalName as attribute or child element;
Include/Exclude/Remove/Update; the SDK's default **/*.resx glob; PUBLIC
resources; CS1566 for a missing literal include; and $(AssemblyName) /
$(RootNamespace) defaulting to the PROJECT name, which fixes an
$(AssemblyName)-expands-to-empty bug this change exposed.

Refused BY NAME, each with its own --accept token: .resx/.restext (the name is
reproducible, the content needs resgen), a culture in the file name (the SDK
routes it to a SATELLITE assembly and an explicit LogicalName does NOT rescue
it), DependentUpon (the name becomes a class name extracted by MSBuild's own
tokenizer), ManifestResourceName (the SDK's handling is a quirk), and embedding
the build's own output.

EmitPipeline gains an OVERLOAD rather than an optional parameter: adding a
parameter replaces a method's signature, and the four-argument entry point stays
byte-identical for MeshNodeCompilationService and NodeSetCompiler.
CreateCompilationOptions and OptionsFingerprint are untouched.

Verified by running: 226 tests green (32 new), including a PARITY test that
builds one probe project with the real SDK and with build-project and asserts
the manifest name sets are identical, plus three mutation proofs. Sweep over all
54 non-test projects in MeshWeaver.Plugins/src against the pinned portal image.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… obvious thing with

All three found by continuing to MEASURE rather than by reading the code back,
and each would have been silent.

1. An EXPLICIT %(Culture) beats %(WithCulture)='false' — the opposite of what
   the two names suggest. `Include="A.md" Culture="fr" WithCulture="false"`
   still emits a fr/ SATELLITE and leaves the main assembly without the
   resource, while `Include="C.de.md" WithCulture="false"` keeps it. The check
   order was the other way round, which would have embedded, under a name the
   SDK never produces, a resource the SDK does not put in this assembly at all.

2. A glob reaching OUTSIDE the project expanded to NOTHING — this evaluator
   enumerates from the project directory down — and an empty glob is legal, so
   the resources were simply absent with no line of output saying so. The real
   SDK expands `..\lb\**\*.md` and embeds what it finds. Now a named refusal.

3. %(LinkBase) synthesizes a %(Link) of `<LinkBase>\%(RecursiveDir)%(Filename)
   %(Extension)` for a file OUTSIDE the project cone and is IGNORED for one
   inside it — measured both ways. Only the outside case changes a name, so only
   the outside case is refused; refusing the inside one would be a false refusal
   on a no-op.

Also: the measured sweep table, and the WhatsNew entry.

The sweep number is 9 → 10 green of 54, not the 9 → ~24 the gap size suggested,
and the reason is worth writing down: a refusal STOPS THE LOAD, so it masks
every gap behind it. The 19 projects failing on the resource refusal were not 19
projects away from green — they were 19 projects about which nothing further was
known. Ten of them want SDK source generators (the previous sweep counted 3),
three want Razor, three want an additional library, one wants protoc. No project
regressed, and none fails on an embedded resource any more.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 31, 2026 00:32

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 expands the SDK-free build-project pipeline so compiled projects can embed <EmbeddedResource> items under the same manifest-resource names the .NET SDK would generate, and tightens build/install observability (compile warnings surfaced in activities; install gating stall detection documented and tested). It also wires memex build project into the CLI and factors Docker image execution into a shared runner.

Changes:

  • Add SDK-faithful manifest resource naming and container reference-set reading for mw-plugin-test build-project, plus extensive new unit/integration tests.
  • Extend the compiler emit pipeline to carry compile warnings (even on successful emits) up to activities, and add tests pinning warning capture/formatting.
  • Add docs + tests around install readability / cover-grant detection, and add a new CLI verb (memex build project) with shared image-running plumbing.

Reviewed changes

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

Show a summary per file
File Description
tools/MeshWeaver.PluginTester/README.md Documents build-project behavior and supported/refused constructs.
tools/MeshWeaver.PluginTester/Program.cs Adds argv parsing + verb dispatch for build-project.
tools/MeshWeaver.PluginTester/ManifestResourceNames.cs Implements SDK-equivalent manifest resource naming logic.
tools/MeshWeaver.PluginTester/LocalNodeRepo.cs Skips unreadable files loudly during sweeps (avoid silent/fatal failures).
tools/MeshWeaver.PluginTester/ContainerReferenceSet.cs Reads /app + deps.json to derive container reference set and package versions.
tools/MeshWeaver.PluginTester/CascadeBuild.cs Ensures fatal errors are printed and report JSON is written for machine consumption.
test/MeshWeaver.PluginTester.Test/ProjectFileTest.cs Pins evaluator contract, including EmbeddedResource behavior.
test/MeshWeaver.PluginTester.Test/ProjectBuildTest.cs End-to-end tests for container-based compile + emit + load.
test/MeshWeaver.PluginTester.Test/ManifestResourceNamesTest.cs Pins measured SDK manifest-resource naming edge cases.
test/MeshWeaver.PluginTester.Test/ContainerReferenceSetTest.cs Pins fail-closed behavior and “match by assembly file on disk” semantics.
test/MeshWeaver.PluginCatalog.Test/InstallGatingHandshakeTest.cs Updates contract commentary for cover-grant watch semantics.
test/MeshWeaver.PluginCatalog.Test/GatingDetectorTest.cs Adds live-mesh tests pinning detector outcomes + budget + logging level.
test/MeshWeaver.Graph.Test/EmitPipelineAccess.cs Thin internal access for warning tests to use canonical compiler options.
test/MeshWeaver.Graph.Test/CompileWarningsReachTheActivityTest.cs Pins warning capture behavior (including doc/cref diagnostics) and capping.
src/MeshWeaver.Graph/Configuration/MeshNodeCompilationService.cs Plumbs warnings from emit/diagnostics into compile activity logs.
src/MeshWeaver.Compiler/MeshWeaver.Compiler.csproj Adds IVT for mw-plugin-test to reuse internal emit pipeline pieces.
src/MeshWeaver.Compiler/EmittedArtifact.cs Extends emitted artifact model to carry warnings alongside digest/length.
src/MeshWeaver.Compiler/EmitPipeline.cs Adds warning extraction/capping + emit overload supporting manifest resources.
src/MeshWeaver.Cli/README.md Documents memex build project usage and option meanings.
src/MeshWeaver.Cli/Program.cs Wires build project subcommand into CLI.
src/MeshWeaver.Cli/ImageRunner.cs New shared Docker pull/pin/run plumbing used by CLI build verbs.
src/MeshWeaver.Cli/BuildProjectCommand.cs New CLI command that runs mw-plugin-test build-project in an image.
src/MeshWeaver.Cli/BuildPluginCommand.cs Refactors to reuse shared ImageRunner logic.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-31-the-sdk-free-builder-embeds-resources-under-the-sdks-own-names.md What’s New entry for embedded-resource support in SDK-free builder.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-31-installs-no-longer-wait-30-seconds-for-a-node-that-is-optional.md What’s New entry for install gating stall detector behavior change.
src/MeshWeaver.Documentation/Data/Architecture/Plugins.md Adds cross-link/context for install readability and gating grant path contract.
src/MeshWeaver.Documentation/Data/Architecture/InstallReadability.md New architecture page documenting the two “readability doors” + detector rationale.
src/MeshWeaver.Documentation/Data/Architecture/InMeshBuildAndTest.md Adds sections documenting compiled-project build without SDK + embedded-resource semantics.
src/MeshWeaver.Documentation/Data/Architecture.md Adds Install Readability to the Architecture index table.
Suppressed comments (1)

src/MeshWeaver.Cli/BuildProjectCommand.cs:106

  • When running in "already inside the image" mode (no --image), ExtraReferenceDirectories are never forwarded to the in-image builder. That makes memex build project ... --extra-refs ... behave differently depending on whether it is invoked outside vs. inside a MeshWeaver image.
            var local = new List<string> { BuilderVerb, entry, "--output", outputDirectory };
            local.AddRange(builderArgs.Skip(4));
            return await _runner.Exec(InImageBuilder, local, ct);

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

Comment on lines +222 to +230
if (!doc.RootElement.TryGetProperty("targets", out var targets)
|| targets.ValueKind != JsonValueKind.Object)
throw new UnreadableContainerException(
$"{Path.GetFileName(path)} has no targets section, so no assembly version can be read.");

JsonElement runtimeTarget = default;
foreach (var target in targets.EnumerateObject())
runtimeTarget = target.Value;

Comment on lines +43 to +51
public async Task<int> RunAsync(BuildProjectOptions options, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(options);

var entry = Path.GetFullPath(options.ProjectPath);
if (!File.Exists(entry) && !Directory.Exists(entry))
{
await error.WriteLineAsync($"error: '{entry}' is neither a project file nor a directory.");
return 2;
Comment on lines +51 to +55
public async Task<string?> PullImage(string image, CancellationToken ct)
{
for (var attempt = 1; attempt <= PullAttempts; attempt++)
{
if (await Exec("docker", ["pull", image], ct) == 0)
…d CLI conflict as #2850

This PR was DIRTY, so it was running no CI at all.

Its branch is built on integration/core-batch (#2853), so it carries #2850's
ImageRunner extraction and hit the identical conflict in
src/MeshWeaver.Cli/BuildPluginCommand.cs — one 159-line block with an empty
"ours" side, holding TWO unrelated things:

  * PullImage, which this history relocated to src/MeshWeaver.Cli/ImageRunner.cs;
  * main's ComposeSealedModules (#2851, #2858), which is genuinely new.

"Take theirs" duplicates PullImage; "take ours" silently deletes main's method.
Split at the seam instead: dropped 39 lines (relocated), kept 120 (main's new).
Then the same two follow-ons the compiler names — ImageRunner.Capture is static
so main's auto-merged call needed qualifying, and the split orphaned PullImage's
`/// <summary>` opener (CS1570).

🚨 Verified neither side was lost, not just that it builds:
    ComposeSealedModules/StageModuleBundle present   5 references
    PullImage defined once — ImageRunner 1, BuildPluginCommand 0

    MeshWeaver.Cli / PluginTester / PluginTester.Test  -c Release -warnaserror, 0/0
    MeshWeaver.PluginTester.Test                       229/229

Note #2853 is now closed — three of its four members landed individually — so
this branch's core-batch ancestry is history rather than a live dependency.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rbuergi added a commit that referenced this pull request Aug 31, 2026
…ompat red was a BASE artefact

This PR was DIRTY (no CI at all) and additionally red on
"Public surface (binary compatibility)". Both are addressed by merging main.

── the conflict (same as #2850/#2860) ──
One 159-line block in src/MeshWeaver.Cli/BuildPluginCommand.cs holding TWO
unrelated things: PullImage (relocated to ImageRunner.cs by this lane) and main's
new ComposeSealedModules (#2851, #2858). Split at the seam — dropped 39 lines
relocated, kept 120 of main's — then qualified ImageRunner.Capture (now static)
and removed the `/// <summary>` opener the split orphaned.

── the binary-compat failure was measured against the WRONG BASE ──
The gate said:

    Comparing against merge base ... branch integration/core-batch
    ✗ tools/MeshWeaver.PluginTester/ProjectFile.cs
        record Model — arity: primary constructor went from 21 to 22 parameter(s)
    🚨 1 binary-breaking record change(s)

That is a correct verdict about the wrong comparison. `ProjectFile.cs` DOES NOT
EXIST ON MAIN — the Model record is introduced by #2850's lane, and the gate was
comparing against integration/core-batch (#2853), where it existed at arity 21.
Against main the record is NEW, so there is no prior signature to break.

Not suppressed and NOT added to scripts/record-signatures.allow: an allow entry
would assert a deliberate break where there is none, and would then go stale the
moment #2850 lands (the shape that has blocked unrelated PRs before). Re-basing
onto main is the fix; the gate re-runs against main and should now pass on its
own. If it does not, the break is real and belongs in the allow file with a
reason — but that is not what this data shows.

    MeshWeaver.PluginTester / .Test   -c Release -warnaserror, 0 Warning(s) 0 Error(s)
    MeshWeaver.PluginTester.Test      198/198

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

792 tests   788 ✅  4m 23s ⏱️
  7 suites    4 💤
  7 files      0 ❌

Results for commit 38d2142.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 083 tests   1 083 ✅  3m 43s ⏱️
    7 suites      0 💤
    7 files        0 ❌

Results for commit 38d2142.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    8 files      8 suites   3m 59s ⏱️
1 490 tests 1 489 ✅ 1 💤 0 ❌
2 188 runs  2 187 ✅ 1 💤 0 ❌

Results for commit 38d2142.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 158 tests   1 158 ✅  4m 28s ⏱️
    7 suites      0 💤
    7 files        0 ❌

Results for commit 38d2142.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

3 057 tests   2 865 ✅  6m 36s ⏱️
    8 suites    192 💤
    8 files        0 ❌

Results for commit 38d2142.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

1 140 tests   1 140 ✅  8m 11s ⏱️
    7 suites      0 💤
    7 files        0 ❌

Results for commit 38d2142.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   44 files     44 suites   31m 22s ⏱️
8 720 tests 8 523 ✅ 197 💤 0 ❌
9 418 runs  9 221 ✅ 197 💤 0 ❌

Results for commit 38d2142.

@rbuergi
rbuergi added this pull request to the merge queue Aug 31, 2026
@rbuergi
rbuergi removed this pull request from the merge queue due to a manual request Aug 31, 2026
@rbuergi

rbuergi commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Removed from the merge queue — CLEAN here, UNMERGEABLE there, and the difference is real

This PR reads CLEAN on its own page and came back UNMERGEABLE from the queue. That is not a glitch, and re-queueing it will reproduce it exactly.

Why. This branch descends from integration/core-batch (#2853) and still carries its merge commits:

Merge remote-tracking branch 'origin/docs/tests-carry-no-dependencies'  -> #2848  MERGED
Merge remote-tracking branch 'origin/fix/gating-settle-stall'          -> #2849  MERGED
Merge remote-tracking branch 'origin/fix/cascade-build-silent-fatal'   -> #2852  MERGED
Merge remote-tracking branch 'origin/feat/cli-build-project'           -> #2850  in queue

Three of those landed on main as squash merges. So this branch reintroduces their content as separate commits, and the queue's speculative merge — which builds this PR on top of main@now — sees it arriving twice. The PR page cannot show this, because it compares against a base where the duplication does not exist. That gap between the two answers is the whole failure mode, and an ejection outranks the page.

It was also occupying a queue slot and would be ejected each time the stack reached it, costing everyone a cycle — hence removing it rather than leaving it to fail repeatedly.

What it needs, and when

It is genuinely stacked on #2850, which is MERGEABLE in the queue right now. Once that lands:

git fetch origin main
git reset --hard origin/main
git cherry-pick <this PR's own commits only>   # NOT the integration/core-batch merges

That drops the stale batch ancestry entirely and produces a branch whose diff is only what this PR contributes. It cannot be done correctly before #2850 is on main, because this work genuinely builds on it.

I have not done that reset myself: it rewrites your branch, and the "own commits only" selection is a judgement about your work rather than a mechanical merge. Happy to do it once #2850 lands if you would rather — say the word.

(#2853 itself is now closed — three of its four members landed individually, so the batch no longer saves a cycle. The main-merge and conflict resolution already pushed here stay valid; only the stale ancestry needs dropping.)

@rbuergi
rbuergi added this pull request to the merge queue Aug 31, 2026
@rbuergi

rbuergi commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #2892, now merged as 9fb74fd9. This PR's unique commits are on main byte-for-byte (cherry-picked and union-merged with the Razor work); only this branch's history — the integration/core-batch ancestry that made it permanently UNMERGEABLE in the queue — was dropped. The arity-21 fix the binary-compat gate demanded (AssemblyInfo as an init property rather than a 22nd positional parameter) is included; this PR's own green had attested to a stale base where that break was invisible.

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