diff --git a/Directory.Packages.props b/Directory.Packages.props
index 7223b1a..988fb0e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -18,6 +18,7 @@
+
diff --git a/docs/wiki/Analyzers.md b/docs/wiki/Analyzers.md
index 4723a6d..5ae0c2a 100644
--- a/docs/wiki/Analyzers.md
+++ b/docs/wiki/Analyzers.md
@@ -19,6 +19,20 @@ The analyzers enforce two families of rules:
- **C# 14 extension-member conventions** — `PSGFR34`–`PSGFR38`, plus the associated
`ReorganizeExtensionClassCodeFixProvider` and `ConvertToExtensionBlockCodeFixProvider`.
+## Build-time validation diagnostics
+
+These are MSBuild diagnostics rather than compiler analyzers, so they are not tracked in
+`AnalyzerReleases.*.md`:
+
+| Code | Raised by | Summary |
+|------|-----------|---------|
+| `PSGF0001` | `Purview.BuildSdk` | A Roslyn component did not produce (or did not declare) a source-generator analyzer file. |
+| `PSGF0003` | `Purview.SourceGeneratorFramework` | A `PurviewGeneratorVisibleProperty` is not compiler-visible in the declaring project or its `Sdk/build`/`Sdk/buildTransitive` assets, so consumers cannot read `build_property.`. |
+| `PRSGD0005` | `Purview.BuildSdk` | A file in the returned analyzer closure references an assembly that is neither part of the closure nor a compiler-host assembly. |
+
+Opt out with `PurviewSourceGeneratorFrameworkAnalyzerValidation=false` (`PRSGD0005`) or
+`PurviewSourceGeneratorFrameworkGeneratorPropertyValidation=false` (`PSGF0003`).
+
## Rule reference
| Rule | Summary |
diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md
index b483b9e..e32cbd1 100644
--- a/docs/wiki/Getting-Started.md
+++ b/docs/wiki/Getting-Started.md
@@ -47,6 +47,21 @@ type identity. Specifying `Targets="GetSourceGeneratorAnalyzerFiles"` explicitly
is not required. Set `PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles` to `false` only when the
unmerged assembly + loose framework DLL shape is required (see [Packaging.md](Packaging.md)).
+### A component that references another component
+
+A code-fix component can reference the generator component normally (`ProjectReference`,
+`ReferenceOutputAssembly` not `false`) when it needs the generator's internal diagnostic identity.
+The generator's **analyzer artifact** is the merged, self-contained assembly, while its **bin output**
+stays unmerged; the framework assembly is copied beside that bin output and flows transitively
+through `ProjectReference`, so the dependent component's bin folder is self-sufficient. The dependent
+component's analyzer closure includes the referenced component's merged artifact — it is never
+IL-merged a second time, which would duplicate its types. See
+[Packaging.md](Packaging.md#a-component-that-references-another-component).
+
+Declare each MSBuild property the generator reads with
+``; `PSGF0003` fails the build when
+the property is not compiler-visible.
+
### Referencing a generator from its test project
A test project can need the source-generator project in two different roles at the same time:
diff --git a/docs/wiki/Packaging.md b/docs/wiki/Packaging.md
index fc96158..d8ed517 100644
--- a/docs/wiki/Packaging.md
+++ b/docs/wiki/Packaging.md
@@ -99,10 +99,70 @@ different shape:
| Path | Trigger | Output |
| --- | --- | --- |
-| `GetSourceGeneratorAnalyzerFiles` (default) | A consuming project references the component as an analyzer | The **merged**, self-contained component, returned from the intermediate `purview-merged/` directory. The component's bin output stays unmerged, so its in-process test harness retains shared framework type identity and `InternalsVisibleTo` access without `CS0433` collisions. |
+| `GetSourceGeneratorAnalyzerFiles` (default) | A consuming project references the component as an analyzer | The **merged**, self-contained component, returned from the content-addressed intermediate `purview-merged//` directory. The component's bin output stays unmerged, so its in-process test harness retains shared framework type identity and `InternalsVisibleTo` access without `CS0433` collisions. |
| `GetSourceGeneratorAnalyzerFiles` (opt-out) | Same, with `PurviewMergeSourceGeneratorFrameworkForAnalyzerFiles=false` | Unmerged component + the loose `Purview.SourceGeneratorFramework.dll` copied from the framework package `lib/`. |
| `GetPurviewMergedAnalyzerFile` | The framework package's own bundled-component pack, or a third-party package embedding the generator | The **merged** component from the intermediate output; the component's bin is never overwritten. |
-| `GenerateNuspec` (`EmbedPurviewSourceGeneratorFrameworkForPack`) | Packing a standalone, packable generator project | The generator's bin is replaced by the **merged** self-contained DLL and the loose framework DLL is deleted before the package is written. |
+| `GetPurviewMergedAnalyzerFileForPack` (pack-time, via `TargetsForTfmSpecificContentInPackage`) | Packing a standalone, packable generator project | The **merged**, self-contained DLL (and its PDB) is contributed directly to `analyzers/dotnet/cs`. The component's bin output is never mutated, so its on-disk shape does not depend on whether `build` or `pack` ran last. |
+
+### A component that references another component
+
+A code-fix (or analyzer) component can reference the generator component normally
+(`ProjectReference`, `ReferenceOutputAssembly` not `false`) when it needs the generator's internal
+diagnostic identity. Both components are `IsRoslynComponent`; only the generator references the
+framework.
+
+- The generator component merges at build time. Its **analyzer artifact** is the merged,
+ self-contained assembly from `obj/.../purview-merged//`; its **bin output** stays unmerged and
+ references `Purview.SourceGeneratorFramework`.
+- Because a package consumer's framework `lib/` asset is not copied to output, the framework
+ assembly is declared as copy-to-output content beside the generator's bin output. Content with
+ `CopyToOutputDirectory` flows transitively through `ProjectReference`, so the dependent code-fix
+ component's bin folder is self-sufficient too — that is what lets Visual Studio load the code-fix
+ provider from its own bin without a `FileNotFoundException` for the framework assembly.
+- The dependent component's **analyzer closure** (what `GetSourceGeneratorAnalyzerFiles` returns)
+ includes the referenced component's analyzer artifact. The referenced component is returned as its
+ own merged assembly and is **never** IL-merged into the dependent component, which would duplicate
+ its types (including `InternalsVisibleTo`-visible internals) inside a second analyzer in the same
+ host.
+- Visual Studio's project system resolves an `OutputItemType=Analyzer` project reference to the
+ referenced project's default target path — the component's **unmerged** bin assembly — and adds it
+ to the compiler's analyzers, which the command-line build does not. `Purview.BuildSdk` removes that
+ item before adding the resolved closure, so Roslyn only ever receives the merged artifact.
+ Otherwise the unmerged copy drags `Purview.SourceGeneratorFramework` into the compiler host
+ (`CS8784 FileNotFoundException: Could not load file or assembly 'Purview.SourceGeneratorFramework'`)
+ and the generators are registered twice.
+
+### Build-time analyzer-closure validation
+
+`Purview.BuildSdk` validates the whole closure returned by `GetSourceGeneratorAnalyzerFiles`: every
+file's PE `AssemblyRef` must resolve to another file in the returned set or to a compiler-host
+assembly (`Microsoft.CodeAnalysis*`, `System.Composition.*`, `System.*`, `netstandard`, …). A missing
+component artifact fails the build with `PRSGD0005`. Extend the permitted set with
+`` (or the
+`PurviewAnalyzerClosurePermittedReferences` property). Opt out with
+`PurviewSourceGeneratorFrameworkAnalyzerValidation=false`.
+
+### Generator-read MSBuild properties
+
+A generator that reads `build_property.` is only correct if `` is a
+`CompilerVisibleProperty` wherever the generator runs. Declare the properties a component reads:
+
+```xml
+
+
+
+```
+
+`PSGF0003` fails the build when a declared property is neither a `CompilerVisibleProperty` in the
+project nor declared by the project's own `Sdk/build` or `Sdk/buildTransitive` assets (which is what
+consumers receive). Opt out with
+`PurviewSourceGeneratorFrameworkGeneratorPropertyValidation=false`.
+
+NuGet imports `buildTransitive` assets for **PackageReference** consumers only. While developing the
+package repository itself every project uses `ProjectReference`, so
+`Purview.BuildSdk`'s `ImportProjectReferencedBuildTransitiveAssets` target registers the referenced
+project's `Sdk/buildTransitive/*.props|*.targets` `CompilerVisibleProperty` items for in-repo
+consumers. Opt out with `PurviewImportProjectReferenceBuildTransitive=false`.
In the opt-out path the loose framework DLL is declared as a `SourceGeneratorRuntimeDependency`
(statically from the framework package `lib/` for package consumers, with a target-time fallback for
@@ -110,10 +170,36 @@ in-repo `ProjectReference` components) so the SDK copies it beside the generator
it. The merged paths never declare it.
The merge itself (`_PurviewMergeSourceGeneratorFramework`) only writes to the component's
-intermediate `purview-merged/` directory. `GetSourceGeneratorAnalyzerFiles` returns that result by
-substituting the merged path into `TargetPathWithTargetPlatformMoniker` immediately before its body
-runs, leaving `GetTargetPath` — which resolves assembly references — pointing at the unmerged bin.
-This is what keeps the in-repo test harness working while shipped assemblies stay self-contained.
+intermediate output. `GetSourceGeneratorAnalyzerFiles` returns that result by substituting the merged
+path into `TargetPathWithTargetPlatformMoniker` immediately before its body runs, leaving
+`GetTargetPath` — which resolves assembly references — pointing at the unmerged bin. This is what
+keeps the in-repo test harness working while shipped assemblies stay self-contained.
+
+The merged artifact is **content-addressed**: it lives in
+`$(IntermediateOutputPath)purview-merged//$(TargetFileName)`, where `` is a
+SHA-256 of the component, the framework assembly and the merge tool (the merge tool computes it via
+its `--tag` mode). The tag is in the *directory*, never the file name, because ILRepack derives the
+merged assembly's simple name from the output file name and an analyzer must keep
+`.dll`.
+
+That gives two properties the earlier fixed-name output could not:
+
+- a rebuild with identical inputs reuses the existing file and **skips the merge**, keeping
+ `_PurviewMergeSourceGeneratorFramework` idempotent across the per-consumer (and potentially
+ parallel) invocations of `GetSourceGeneratorAnalyzerFiles`; and
+- changed inputs select a **new directory**, so the merge never overwrites a merged assembly that a
+ compiler host (csc/`VBCSCompiler`/Visual Studio) already has loaded. Overwriting such a file fails
+ on Windows with a sharing violation, which surfaced as `MSB3073` (merge tool exit code
+ `-532462766`) and left consumers loading a stale or partially written analyzer — the root cause of
+ `CS8784 FileNotFoundException: Could not load file or assembly 'Purview.SourceGeneratorFramework'`.
+
+The merge tool merges into a per-process `.staging-` directory and publishes it with a
+non-overwriting directory rename; a concurrent invocation that loses the race simply observes the
+published artifact instead of failing.
+
+Packaging needs the stable `.dll` name, so the pack paths
+(`GetPurviewMergedAnalyzerFile`, `GetPurviewMergedAnalyzerFileForPack`) copy the content-addressed
+artifact into `$(IntermediateOutputPath)purview-pack/` and pack that plainly named staging copy.
### Framework type internalization in merged components
diff --git a/global.json b/global.json
index f8c7bba..b476ad4 100644
--- a/global.json
+++ b/global.json
@@ -3,7 +3,7 @@
"allowPrerelease": false
},
"msbuild-sdks": {
- "Purview.BuildSdk": "1.0.0-prerelease.57"
+ "Purview.BuildSdk": "1.0.0-prerelease.60"
},
"test": {
"runner": "Microsoft.Testing.Platform"
diff --git a/package.json b/package.json
index ed999bc..c20d121 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "purview-sourcegenerator-framework",
- "version": "1.0.0-prerelease.51",
+ "version": "1.0.0-prerelease.52",
"license": "MIT",
"author": {
"name": "Kieron Lanning",
diff --git a/purview-build.json b/purview-build.json
index 3d71580..4d12dca 100644
--- a/purview-build.json
+++ b/purview-build.json
@@ -16,8 +16,11 @@
"analyzers/dotnet/cs/Purview.SourceGeneratorFramework.Generators.dll",
"build/Purview.SourceGeneratorFramework.props",
"build/Purview.SourceGeneratorFramework.targets",
+ "content/Packaging.md",
+ "contentFiles/any/netstandard2.0/Packaging.md",
"lib/netstandard2.0/Purview.SourceGeneratorFramework.dll",
"lib/netstandard2.0/Purview.SourceGeneratorFramework.xml",
+ "packaging.md",
"purview-logo-light.png",
"README.md",
"tools/*/ILRepack.dll",
@@ -48,7 +51,9 @@
]
},
"ForbiddenContent": {
- "*": ["analyzers/**/Purview.SourceGeneratorFramework.dll"]
+ "*": [
+ "analyzers/**/Purview.SourceGeneratorFramework.dll"
+ ]
}
},
"Release": {
diff --git a/src/SourceGeneratorFramework.slnx b/src/SourceGeneratorFramework.slnx
index 685c9ad..7546cfb 100644
--- a/src/SourceGeneratorFramework.slnx
+++ b/src/SourceGeneratorFramework.slnx
@@ -40,6 +40,7 @@
+
diff --git a/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs b/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs
index d145ce9..a77b9e5 100644
--- a/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs
+++ b/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs
@@ -1,3 +1,5 @@
+using System.Security.Cryptography;
+using System.Text;
using ILRepacking;
using Mono.Cecil;
@@ -5,12 +7,17 @@ static class MergeToolRunner
{
const string IsExternalInitName = "IsExternalInit";
const string IsExternalInitNamespace = "System.Runtime.CompilerServices";
+ const string TagCommand = "--tag";
public static int Run(string[] args, TextWriter error, ILogger? logger = null)
{
+ if (args.Length > 0 && string.Equals(args[0], TagCommand, StringComparison.Ordinal))
+ return WriteContentTag(args, error);
+
if (args.Length < 3)
{
error.WriteLine("Usage: Purview.SourceGeneratorFramework.MergeTool