Repository navigation
Conversation
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
…vity' into integration/core-batch
…' into integration/core-batch
…tegration/core-batch
…egration/core-batch
…into integration/core-batch
…ng identity `GenerateAssemblyInfo` is a TARGET, and `build-project` runs no targets — so every assembly it emitted carried Roslyn's default identity, `AssemblyVersion=0.0.0.0`, while the whole fleet binds `3.0.0.0`. Nothing went red. Measured first-hand: building `MeshWeaver.Plugins/src/MeshWeaver.Speech.Contract` through a tester image from #2850 produced `AssemblyVersion=0.0.0.0` and a GREEN build, where that repo's `src/Directory.Build.props` pins `3.0.0.0` precisely because #143 shipped 1.0.0.0 assemblies into a 3.0.0.0 process and CrashLoopBackOff'd a migration. The failure would have surfaced in another repo, at runtime, as `FileNotFoundException: … Version=3.0.0.0`. `ProjectFile` now evaluates the identity and `ProjectBuild` synthesizes it into the compilation as one more source document, exactly as the SDK's generated `<Project>.AssemblyInfo.cs` is one more Compile item. Every rule was MEASURED against SDK 10.0.400 rather than recalled — `Version=1.2.3-beta.4` → `1.2.3.0`, `1` → `1.0.0.0`, `01.2.3` → `1.2.3.0`, `FileVersion` follows `AssemblyVersion` and never `$(Version)`. Also promoted out of this evaluator's "metadata that changes nothing" list, which was true of the COMPILE and false of the ASSEMBLY: `InternalsVisibleTo` (sixty-odd projects across these repos), `AssemblyMetadata` and `AssemblyAttribute` items. `GenerateAssemblyInfo=false` synthesizes nothing (a second set is CS0579); each `Generate…Attribute` switch is honoured. A property that cannot be derived is a NAMED failure, never a plausible substitute. Two divergences, both said out loud rather than hidden: `$(SourceRevisionId)` (the SDK reads the commit from git; every build here reports the absent `+<sha>` suffix by name, and the new `-p:Name=Value` gives exact parity) and `TargetFrameworkAttribute` (a different target, with platform companions this evaluator cannot compute). `-p:` also fixes a global property that was documented to win and did not: a `<PropertyGroup>` could overwrite one, so the flag would have meant nothing for exactly the properties a caller passes it for. 🚨 `CreateCompilationOptions` and `OptionsFingerprint` are untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Test Results (shard 0)792 tests 788 ✅ 4m 34s ⏱️ Results for commit 97b5d9b. ♻️ This comment has been updated with latest results. |
Test Results (shard 4)1 158 tests 1 158 ✅ 4m 38s ⏱️ Results for commit 97b5d9b. ♻️ This comment has been updated with latest results. |
Test Results (shard 5) 8 files 8 suites 4m 3s ⏱️ Results for commit 97b5d9b. ♻️ This comment has been updated with latest results. |
Test Results (shard 1)1 052 tests 1 052 ✅ 3m 24s ⏱️ Results for commit 97b5d9b. ♻️ This comment has been updated with latest results. |
Test Results (shard 2)3 057 tests 2 865 ✅ 5m 58s ⏱️ Results for commit 97b5d9b. ♻️ This comment has been updated with latest results. |
Test Results (shard 3)1 142 tests 1 142 ✅ 8m 35s ⏱️ Results for commit 97b5d9b. ♻️ This comment has been updated with latest results. |
Test Results 44 files 44 suites 31m 15s ⏱️ Results for commit 97b5d9b. ♻️ This comment has been updated with latest results. |
…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>
There was a problem hiding this comment.
Pull request overview
This PR hardens the “build-project” path so assemblies compiled inside a MeshWeaver image carry the correct binding identity (e.g., AssemblyVersion=3.0.0.0 instead of Roslyn’s 0.0.0.0) and so successful compilation warnings are surfaced to the compile activity log (rather than being silently dropped).
Changes:
- Extend
mw-plugin-test build-projectto synthesize SDK-equivalent assembly identity attributes into the Roslyn compilation inputs. - Add/extend container reference-set reading from
/app+*.deps.json, and wirememex build projectin the CLI (plus shared docker image runner plumbing). - Propagate warnings from successful in-mesh compiles into the activity log; add comprehensive unit + end-to-end tests and documentation updates.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/MeshWeaver.PluginTester/README.md | Documents build-project behavior, identity rules, and supported/unsupported constructs. |
| tools/MeshWeaver.PluginTester/ProjectBuild.cs | Implements project graph build + Roslyn compilation, including synthesized assembly identity source. |
| tools/MeshWeaver.PluginTester/Program.cs | Adds argv handling for build-project, including -p:Name=Value properties. |
| tools/MeshWeaver.PluginTester/ContainerReferenceSet.cs | Reads /app and *.deps.json to resolve platform references + binding identity. |
| test/MeshWeaver.PluginTester.Test/ProjectFileTest.cs | Expands evaluator contract tests (globs, imports, conditions, constructs). |
| test/MeshWeaver.PluginTester.Test/ProjectBuildTest.cs | End-to-end tests for compiling/emitting/loading a real assembly and streaming diagnostics. |
| test/MeshWeaver.PluginTester.Test/ProjectAssemblyIdentityTest.cs | Verifies emitted-file identity matches SDK semantics via PE metadata inspection. |
| test/MeshWeaver.PluginTester.Test/ContainerReferenceSetTest.cs | Validates fail-closed and file-based package matching rules for the container reference set. |
| test/MeshWeaver.Graph.Test/EmitPipelineAccess.cs | Exposes internal compiler option factories to tests. |
| test/MeshWeaver.Graph.Test/CompileWarningsReachTheActivityTest.cs | Pins warning collection/capping behavior for successful compiles. |
| src/MeshWeaver.Graph/Configuration/MeshNodeCompilationService.cs | Carries compile warnings up so they reach the compile activity record. |
| src/MeshWeaver.Documentation/Data/Architecture/InMeshBuildAndTest.md | Adds documentation for memex build project and its constraints/measurements. |
| src/MeshWeaver.Compiler/MeshWeaver.Compiler.csproj | Adds IVT for mw-plugin-test to reuse the emit pipeline. |
| src/MeshWeaver.Compiler/EmittedArtifact.cs | Extends emitted-artifact record to carry successful-compile warnings. |
| src/MeshWeaver.Compiler/EmitPipeline.cs | Adds warning extraction/capping helper and records warnings in emitted artifacts. |
| src/MeshWeaver.Cli/README.md | Documents memex build project usage and options. |
| src/MeshWeaver.Cli/Program.cs | Wires the memex build project subcommand via BuildProjectCommand. |
| src/MeshWeaver.Cli/ImageRunner.cs | Adds shared docker pull/pin/run plumbing used by build commands. |
| src/MeshWeaver.Cli/BuildProjectCommand.cs | Implements the host-side “trip into the container” for project builds. |
| src/MeshWeaver.Cli/BuildPluginCommand.cs | Refactors to share image-running plumbing with ImageRunner. |
Suppressed comments (23)
src/MeshWeaver.Cli/ImageRunner.cs:57
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Refactor this to a reactiveIObservable<T>flow (and push actual IO through the sanctioned IO boundary), and keep any Task bridge at the outermost CLI boundary only.
var digest = await Capture("docker",
src/MeshWeaver.Cli/ImageRunner.cs:63
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Refactor this to a reactiveIObservable<T>flow (and push actual IO through the sanctioned IO boundary), and keep any Task bridge at the outermost CLI boundary only.
await output.WriteLineAsync(
src/MeshWeaver.Cli/ImageRunner.cs:72
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Refactor this to a reactiveIObservable<T>flow (and push actual IO through the sanctioned IO boundary), and keep any Task bridge at the outermost CLI boundary only.
await error.WriteLineAsync(
src/MeshWeaver.Cli/ImageRunner.cs:74
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Refactor this to a reactiveIObservable<T>flow (and push actual IO through the sanctioned IO boundary), and keep any Task bridge at the outermost CLI boundary only.
await Task.Delay(TimeSpan.FromSeconds(10), ct);
src/MeshWeaver.Cli/ImageRunner.cs:77
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Refactor this to a reactiveIObservable<T>flow (and push actual IO through the sanctioned IO boundary), and keep any Task bridge at the outermost CLI boundary only.
await error.WriteLineAsync(
src/MeshWeaver.Cli/ImageRunner.cs:123
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Refactor this to a reactiveIObservable<T>flow (and push actual IO through the sanctioned IO boundary), and keep any Task bridge at the outermost CLI boundary only.
if ((await Capture("docker", ["image", "inspect", image, "--format", "{{.Id}}"], ct))
src/MeshWeaver.Cli/ImageRunner.cs:126
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Refactor this to a reactiveIObservable<T>flow (and push actual IO through the sanctioned IO boundary), and keep any Task bridge at the outermost CLI boundary only.
await error.WriteLineAsync(
src/MeshWeaver.Cli/ImageRunner.cs:146
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Refactor this to a reactiveIObservable<T>flow (and push actual IO through the sanctioned IO boundary), and keep any Task bridge at the outermost CLI boundary only.
await output.WriteLineAsync($"$ {file} {string.Join(' ', psi.ArgumentList)}");
src/MeshWeaver.Cli/ImageRunner.cs:131
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Refactor this to a reactiveIObservable<T>flow (and push actual IO through the sanctioned IO boundary), and keep any Task bridge at the outermost CLI boundary only.
await output.WriteLineAsync(
src/MeshWeaver.Cli/ImageRunner.cs:153
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Refactor this to a reactiveIObservable<T>flow (and push actual IO through the sanctioned IO boundary), and keep any Task bridge at the outermost CLI boundary only.
await p.WaitForExitAsync(ct);
src/MeshWeaver.Cli/ImageRunner.cs:169
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Refactor this to a reactiveIObservable<T>flow (and push actual IO through the sanctioned IO boundary), and keep any Task bridge at the outermost CLI boundary only.
var stdout = await p.StandardOutput.ReadToEndAsync(ct);
src/MeshWeaver.Cli/ImageRunner.cs:150
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Refactor this to a reactiveIObservable<T>flow (and push actual IO through the sanctioned IO boundary), and keep any Task bridge at the outermost CLI boundary only.
await error.WriteLineAsync($"error: could not start '{file}' — is it installed and on PATH?");
src/MeshWeaver.Cli/ImageRunner.cs:170
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Refactor this to a reactiveIObservable<T>flow (and push actual IO through the sanctioned IO boundary), and keep any Task bridge at the outermost CLI boundary only.
await p.WaitForExitAsync(ct);
src/MeshWeaver.Cli/BuildProjectCommand.cs:61
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Convert this command implementation to a reactive pipeline and keep any Task bridge at the CLI boundary only.
await error.WriteLineAsync(
src/MeshWeaver.Cli/BuildProjectCommand.cs:96
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Convert this command implementation to a reactive pipeline and keep any Task bridge at the CLI boundary only.
await error.WriteLineAsync(
src/MeshWeaver.Cli/BuildProjectCommand.cs:110
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Convert this command implementation to a reactive pipeline and keep any Task bridge at the CLI boundary only.
? await _runner.UseLocalImage(image, ct)
src/MeshWeaver.Cli/BuildProjectCommand.cs:106
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Convert this command implementation to a reactive pipeline and keep any Task bridge at the CLI boundary only.
return await _runner.Exec(InImageBuilder, local, ct);
src/MeshWeaver.Cli/BuildProjectCommand.cs:111
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Convert this command implementation to a reactive pipeline and keep any Task bridge at the CLI boundary only.
: await _runner.PullImage(image, ct);
src/MeshWeaver.Cli/BuildProjectCommand.cs:113
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Convert this command implementation to a reactive pipeline and keep any Task bridge at the CLI boundary only.
await output.WriteLineAsync($"image: {pinned}");
src/MeshWeaver.Cli/BuildProjectCommand.cs:114
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Convert this command implementation to a reactive pipeline and keep any Task bridge at the CLI boundary only.
await output.WriteLineAsync($"source root: {root} → /repo; project: /repo/{relative}");
src/MeshWeaver.Cli/BuildProjectCommand.cs:132
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Convert this command implementation to a reactive pipeline and keep any Task bridge at the CLI boundary only.
var exit = await _runner.RunInImage(pinned, mounts, [], builderArgs, ct);
src/MeshWeaver.Cli/BuildProjectCommand.cs:121
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Convert this command implementation to a reactive pipeline and keep any Task bridge at the CLI boundary only.
await error.WriteLineAsync(
src/MeshWeaver.Cli/BuildProjectCommand.cs:134
- Async/await is forbidden in
src/in this repo because it can park turn-based schedulers and/or loseAccessContextacross async boundaries. Convert this command implementation to a reactive pipeline and keep any Task bridge at the CLI boundary only.
await output.WriteLineAsync($"assemblies: {outputDirectory}");
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| 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; | ||
|
|
| internal ActivityLog Seal(bool green) | ||
| { | ||
| lock (_gate) | ||
| { | ||
| _log = green | ||
| ? _log with { Status = ActivityStatus.Succeeded } | ||
| : _log with { Status = ActivityStatus.Failed }; | ||
| _options.Log?.OnCompleted(); | ||
| return _log; | ||
| } | ||
| } |
| { | ||
| for (var attempt = 1; attempt <= PullAttempts; attempt++) | ||
| { | ||
| if (await Exec("docker", ["pull", image], ct) == 0) |
| 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."); |
Removed from the merge queue —
|
… a 22nd parameter
The binary-compat gate refused this batch — correctly, this time:
record Model — arity: primary constructor went from 21 to 22 parameter(s)
#2855's original commit passed AssemblyInfo positionally. Its green attested to
a stale merge base (integration/core-batch) where the arity comparison ran
against a tree that already carried the change — the wrong-base shape its own
PR thread documented for a DIFFERENT record the same evening. Against main the
break is real, and the record's own doc block states the house rule the commit
violated:
"These four are init PROPERTIES, not primary-constructor parameters. Adding
a parameter — even with a default — REPLACES the record's constructor
signature, and scripts/check-record-signatures.py refuses that for exactly
the reason it should."
So AssemblyInfo now follows RazorItems/EmbeddedResources onto the initializer:
* Model stays at arity 21;
* `public AssemblyInfo AssemblyInfo { get; init; } = AssemblyInfo.None;`
* AssemblyInfo.None — the "nothing was stamped" identity — keeps the property
non-nullable with a valid default;
* ToModel assigns it in the object initializer beside the other features.
No allow-file entry: the gate found a real break and the fix is to not break,
not to document breaking.
MeshWeaver.PluginTester / .Test -c Release -warnaserror 0/0
MeshWeaver.PluginTester.Test 265/265
scripts/check-record-signatures.py ✓ no binary-breaking changes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Superseded by #2892, now merged as |
The defect: a wrong binding identity that nothing turns red
GenerateAssemblyInfois an MSBuild target, andbuild-project(#2850) deliberately runs notargets. So the SDK's
[assembly: AssemblyVersion("…")]was never written, and Roslyn emitted itsown default:
0.0.0.0, where the whole fleet binds3.0.0.0.Measured first-hand, not inferred —
MeshWeaver.Plugins/src/MeshWeaver.Speech.Contractbuiltthrough a tester image from
feat/cli-build-project:The build was GREEN. That repo's
src/Directory.Build.propspins<AssemblyVersion>3.0.0.0precisely because #143 shipped 1.0.0.0 assemblies into a 3.0.0.0 process and CrashLoopBackOff'd a
migration; its own comment records why the class is nasty — "a target that runs
BeforeTargets="CoreGenerateAssemblyInfo"still builds 1.0.0.0, silently." The failure lands atruntime binding, in a different repo, as
FileNotFoundException: Could not load file or assembly '…, Version=3.0.0.0'.The fix
ProjectFileevaluates the identity;ProjectBuildsynthesizes it into the compilation asone more source document — exactly as the SDK's generated
<Project>.AssemblyInfo.csis one moreCompile item.
🚨
EmitPipeline.CreateCompilationOptionsand everything feedingOptionsFingerprintareuntouched; #2850's own
CSharpCompilationOptionsis where this lives.The SDK semantics, MEASURED rather than recalled
Every rule below was produced by building the same project with SDK 10.0.400 and reading the
generated
*.AssemblyInfo.cs:$(Version)AssemblyVersion1.2.3-beta.41.2.3.0(label dropped)1.2.3+meta1.2.3.0(metadata dropped)1.2.3.41.2.3.44.5/14.5.0.0/1.0.0.0(padded)01.2.31.2.3.0(zeros normalise)1.0.0.0viaVersionPrefix1.2.3.4.5Plus the fallback ORDER, which is the easy thing to get backwards: an explicit
AssemblyVersionwins,
FileVersionfollowsAssemblyVersion(not$(Version)),InformationalVersionfollows$(Version), andCompany←Authors←AssemblyName/Product,Title←AssemblyName.GenerateAssemblyInfo=falsesynthesizes nothing — the project supplies its own and a second setis CS0579. Each individual
Generate…Attributeswitch is honoured.Three item types that were being dropped
InternalsVisibleTo,AssemblyMetadataandAssemblyAttributewere on this evaluator's "metadatathat does not change which sources compile against what" list. True of the compile, false of the
assembly: an
InternalsVisibleTodropped here makes the friend project fail to compile in alater, unrelated run. Sixty-odd projects across these repos declare one.
One consequence worth flagging: now that
AssemblyMetadataValue metadata is read, a projectwhose value uses a property function outside this evaluator's grammar becomes a named refusal
where it used to be a silent drop. Exactly one project in either repo is affected —
MeshWeaver.Plugins/src/MeshWeaver.AI.Test, whose<AssemblyMetadata Include="MeshWeaverRoot" Value="$([System.IO.Path]::GetFullPath('$(MeshWeaverRoot)'))" />now fails the load by name. That is this file's stated doctrine, and no CI lane calls
build-projectyet, so nothing regresses.Where the SDK cannot be matched, it FAILS BY NAME
An unparseable
$(Version);PublishRepositoryUrl=truewith no$(RepositoryUrl)(the SDK readsthat off the git remote). No plausible-looking substitute — a wrong binding identity is the entire
defect.
Two divergences, both said out loud
$(SourceRevisionId)— the SDK appends+<sha>from git. There is no git here, so everybuild reports the absent suffix by name, and the new
-p:Name=Valuegives exact parity.TargetFrameworkAttribute— a different target (GenerateTargetFrameworkMonikerAttribute)with its own switches and its own
TargetPlatform/SupportedOSPlatformcompanions for aplatform-suffixed TFM this evaluator cannot compute. Emitting half of that set would be worse than
the gap, so the gap is written down in the code and the README instead.
One bug found on the way
ProjectFile'sglobalPropertieswere documented to "win over anything the files setunconditionally" and did not — a
<PropertyGroup>overwrote them. Fixed (a global property is nowimmutable during evaluation, as under MSBuild), because otherwise
-p:AssemblyVersion=…would meannothing for exactly the property a caller passes it for.
Verified by RUNNING
Unit — 23 new cases in
test/MeshWeaver.PluginTester.Test/ProjectAssemblyIdentityTest.cs; suite198/198 green (175 before, +23). Every assertion reads the emitted file's metadata (
PEReader— not
Assembly.LoadFrom, which refuses a second assembly of the same simple name and would turn anidentity suite into a load-order suite).
Mutation proof — with the synthesized tree disabled, the key test fails exactly as the defect
predicts:
End to end, the reproduction that found the defect — tester image built from this branch,
MeshWeaver.Plugins/src/MeshWeaver.Speech.Contractbuilt inside it:Parity against a real
dotnet buildof the same project, attribute for attribute — Company,Configuration, Description, FileVersion, InformationalVersion, Product, Title, AssemblyVersion all
identical; the only difference is
TargetFrameworkAttribute(above). With-p:SourceRevisionId=7e0750c…theInformationalVersionmatches the SDK's exactly too:1.0.0+7e0750c274faa2ecf4a918ed6891ef96e326c8b2.A recommendation, not a change:
--accept targetsbuild-projectrefuses to run againstMeshWeaver.Plugins/srcwithout--accept targets, becauseof
<Target Name="VerifyAssemblyVersionMatchesPlatform">— the guard that exists to catch exactlythis drift. So today that flag is what lets a wrong binding identity through. I have not changed
it. What I'd suggest instead, for a maintainer call:
ContainerReferenceSetalready knowsPlatformAssemblyVersion(3.0.0.0, read from the image).build-projectcould WARN when the identity it is about to emit differs from the platform's —naming both numbers. That is the same check the skipped target performs, done from data the builder
already has, and it needs no MSBuild. A warning rather than an error because a standalone tool may
legitimately differ; under the default no-warn policy a module build would still go red. That would
make
--accept targetssafe again, rather than an accepted blind spot.Base — read this before reviewing the diff
The branch is cut from
integration/core-batch(#2853), because #2850 has not landed onmainyet and the
build-projectfiles this changes do not exist there.The PR nonetheless targets
main, not the batch: a PR merged into a feature branch looks mergedwhile never reaching
main, and this has to land on the trunk. The cost is that GitHub currentlyshows the batch's files in the diff as well. The five files this PR actually changes are:
tools/MeshWeaver.PluginTester/ProjectFile.cs— evaluate the identitytools/MeshWeaver.PluginTester/ProjectBuild.cs— synthesize it into the compilationtools/MeshWeaver.PluginTester/Program.cs—-p:Name=Valuetools/MeshWeaver.PluginTester/README.mdtest/MeshWeaver.PluginTester.Test/ProjectAssemblyIdentityTest.cs(new, 23 cases)The clean diff is
integration/core-batch...fix/build-project-assembly-version.Once the batch merges, this diff collapses to those five files on its own; no rebase needed.
Not merged, no auto-merge armed, as requested.
🤖 Generated with Claude Code