diff --git a/.agents/agents/sdk-consumer-setup.md b/.agents/agents/sdk-consumer-setup.md
deleted file mode 100644
index 8e5066c..0000000
--- a/.agents/agents/sdk-consumer-setup.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# sdk-consumer-setup (generic agent spec)
-
-## Goal
-
-Help a consuming repository adopt or troubleshoot `Purview.BuildSdk` correctly, without breaking existing build behaviour.
-
-## Workflow
-
-1. Confirm the SDK is imported in `Directory.Build.props`/`Directory.Build.targets` via
- `` and the matching `Sdk.targets` import.
-2. Check pre-import bootstrap properties are set **before** the `Sdk.props` import when they must affect
- evaluation: `NamespacePrefix`, `UsePackageJsonVersion`, `RootPackageJson`.
-3. If version resolution looks wrong, verify `package.json` discovery: explicit `RootPackageJson`, then CI
- variables, `.git` root, or a nearby `package.json`. `UsePackageJsonVersion=Strict` fails fast instead of
- silently skipping resolution.
-4. If the bundled `.agents/**` content isn't appearing in the repo root, check `EnableAgentFolderInPackage`
- (default `true`) and `AgentPackDestinationFolder` (default `.agents`) — the copy runs before build via
- `EnsureAgentFolderInPackageTarget`.
-5. For test-framework or project-shape questions, confirm the project follows repo naming and placement
- conventions the SDK expects, rather than introducing bespoke structure.
-6. Re-run `dotnet build` (or the repo's canonical build command) after each configuration change to confirm
- the fix.
-
-## Constraints
-
-- Prefer minimal, targeted property changes over broad `Directory.Build.props` rewrites.
-- Do not disable `PurviewAutoSdkPack` or `EnableAgentFolderInPackage` unless the consumer explicitly asks to
- opt out.
-- Do not duplicate SDK-managed properties in individual project files unless the scenario is intentionally
- project-specific.
-
-## Related skill
-
-See `../skills/sdk-configuration-reference/SKILL.md` for the full property reference.
diff --git a/.agents/prompts/sdk-diagnose-agent-folder-copy.md b/.agents/prompts/sdk-diagnose-agent-folder-copy.md
deleted file mode 100644
index 85ad444..0000000
--- a/.agents/prompts/sdk-diagnose-agent-folder-copy.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# sdk-diagnose-agent-folder-copy (generic prompt spec)
-
-Diagnose why the bundled `.agents/**` folder from `Purview.BuildSdk` did not appear at the expected
-destination in a consuming repository.
-
-## Required behaviour
-
-1. Confirm the NuGet package actually contains `.agents/**` content (inspect the `.nupkg` if available).
-2. Confirm the consuming project is packable/buildable and imports the SDK via
- `Sdk.props`/`Sdk.targets`, since the copy runs in `EnsureAgentFolderInPackageTarget` before build.
-3. Check `EnableAgentFolderInPackage` is not set to `false` anywhere in the build (project file,
- `Directory.Build.props`, or command-line `-p:` overrides).
-4. Confirm the destination folder: default is `.agents` at the repo root, overridable per-build with
- `-p:AgentPackDestinationFolder=`.
-5. Verify repo-root discovery succeeded: explicit `RepoRoot`, then a nearby `AGENTS.md`, then source-control
- root metadata.
-6. Re-run the build and confirm the destination folder now contains the copied files (including the
- generated `.gitignore` for skill/prompt/agent subfolders).
-
-## Suggested output
-
-- A short root-cause explanation (missing import, disabled flag, wrong destination override, or repo-root
- discovery miss).
-- The exact command used to reproduce/verify the fix (for example
- `dotnet build -p:AgentPackDestinationFolder=`).
-- Confirmation that the expected files exist at the resolved destination path.
diff --git a/.agents/skills/project-placement-defaults/.gitignore b/.agents/skills/project-placement-defaults/.gitignore
deleted file mode 100644
index 2799754..0000000
--- a/.agents/skills/project-placement-defaults/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Ignore all files
-*
-
-# Don't ignore directories, so Git can traverse them
-!*/
-
-# Keep this file
-!.gitignore
\ No newline at end of file
diff --git a/.agents/skills/sdk-configuration-reference/.gitignore b/.agents/skills/sdk-configuration-reference/.gitignore
deleted file mode 100644
index 2799754..0000000
--- a/.agents/skills/sdk-configuration-reference/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Ignore all files
-*
-
-# Don't ignore directories, so Git can traverse them
-!*/
-
-# Keep this file
-!.gitignore
\ No newline at end of file
diff --git a/.agents/skills/sdk-project-behavior-and-detection/.gitignore b/.agents/skills/sdk-project-behavior-and-detection/.gitignore
deleted file mode 100644
index 2799754..0000000
--- a/.agents/skills/sdk-project-behavior-and-detection/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Ignore all files
-*
-
-# Don't ignore directories, so Git can traverse them
-!*/
-
-# Keep this file
-!.gitignore
\ No newline at end of file
diff --git a/global.json b/global.json
index b476ad4..6cefa81 100644
--- a/global.json
+++ b/global.json
@@ -3,7 +3,7 @@
"allowPrerelease": false
},
"msbuild-sdks": {
- "Purview.BuildSdk": "1.0.0-prerelease.60"
+ "Purview.BuildSdk": "1.0.3"
},
"test": {
"runner": "Microsoft.Testing.Platform"
diff --git a/package.json b/package.json
index f579167..7942507 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "purview-sourcegenerator-framework",
- "version": "1.0.0-prerelease.54",
+ "version": "1.0.0",
"license": "MIT",
"author": {
"name": "Kieron Lanning",
diff --git a/src/src/SourceGeneratorFramework.MergeTool/FrameworkTypeInternalizer.cs b/src/src/SourceGeneratorFramework.MergeTool/FrameworkTypeInternalizer.cs
index 092057c..b0b96c1 100644
--- a/src/src/SourceGeneratorFramework.MergeTool/FrameworkTypeInternalizer.cs
+++ b/src/src/SourceGeneratorFramework.MergeTool/FrameworkTypeInternalizer.cs
@@ -120,6 +120,46 @@ public static ImmutableArray CollectTypeFullNames(string assemblyPath)
return [.. assembly.MainModule.Types.SelectMany(MergeToolRunner.Flatten).Select(static type => type.FullName)];
}
+ ///
+ /// Reads the assembly names an assembly grants internals access to. The merge uses the framework
+ /// assembly's grants to decide which grants in the merged component were copied in by the merge
+ /// (and must go) and which the component itself authored (and must stay) - see
+ /// .
+ ///
+ /// The assembly to read grants from.
+ public static ImmutableArray CollectInternalsGrants(string assemblyPath)
+ {
+ using var assembly = AssemblyDefinition.ReadAssembly(assemblyPath);
+
+ return
+ [
+ .. assembly
+ .CustomAttributes.Where(static attribute =>
+ s_internalsGrantAttributeFullNames.Contains(
+ attribute.AttributeType.FullName,
+ StringComparer.Ordinal
+ )
+ )
+ .Select(static attribute => GrantedAssemblyName(attribute))
+ .Where(static name => name.Length > 0),
+ ];
+ }
+
+ ///
+ /// Reads the granted assembly's simple name from an internals-grant attribute, discarding any
+ /// , PublicKey=... suffix.
+ ///
+ static string GrantedAssemblyName(CustomAttribute attribute)
+ {
+ if (attribute.ConstructorArguments.Count == 0)
+ return string.Empty;
+
+ var value = attribute.ConstructorArguments[0].Value as string ?? string.Empty;
+ var comma = value.IndexOf(',', StringComparison.Ordinal);
+
+ return (comma < 0 ? value : value[..comma]).Trim();
+ }
+
///
/// Internalizes every framework-owned type in the merged component, strips the assembly-level
/// internals grants the merge copied in, and returns a report of the changes plus anything that
@@ -130,12 +170,19 @@ public static ImmutableArray CollectTypeFullNames(string assemblyPath)
/// Optional sink for non-blocking findings, such as component members that expose framework types.
/// Namespace prefixes to internalize; defaults to the framework's own namespaces.
/// Additional type full names to internalize (normally the framework assembly's own type names).
+ ///
+ /// The internals grants declared by the framework assembly. Only these are stripped from the merged
+ /// component, so a grant the component itself authored - the one that lets a companion code-fix
+ /// component read the generator's internal diagnostic identity - survives the merge. When
+ /// , every grant is stripped.
+ ///
public static FrameworkInternalizationReport Apply(
string assemblyPath,
IEnumerable searchDirectories,
Action? warn = null,
ImmutableArray? ownedNamespaces = null,
- ImmutableArray? ownedTypeFullNames = null
+ ImmutableArray? ownedTypeFullNames = null,
+ ImmutableArray? frameworkInternalsGrants = null
)
{
var namespaces = ownedNamespaces ?? DefaultOwnedNamespaces;
@@ -190,7 +237,7 @@ public static FrameworkInternalizationReport Apply(
CollectExposingMembers(type, namespaces, typeFullNames, exposingMembers);
}
- var strippedGrantCount = StripInternalsGrants(assembly);
+ var strippedGrantCount = StripInternalsGrants(assembly, frameworkInternalsGrants);
if (internalizedTypeCount > 0 || strippedGrantCount > 0)
assembly.Write(assemblyPath, new WriterParameters { WriteSymbols = hasSymbols });
@@ -217,21 +264,36 @@ [.. exposingMembers.Select(static entry => $"{entry.Owner}.{entry.Member}")],
}
///
- /// Removes the assembly-level internals grants the merge copied into the artifact. The merged
- /// component is a shipped analyzer, not the component's own assembly: a leftover grant would let
- /// an unrelated assembly (such as the framework's own test assemblies) reach the internalized
- /// framework types, while the component's bin output keeps its grants for in-repo tests.
+ /// Removes the assembly-level internals grants the merge copied into the artifact from the
+ /// framework assembly. The merged component is a shipped analyzer, not the framework's own
+ /// assembly: a leftover framework grant would let an unrelated assembly (such as the framework's
+ /// own test assemblies) reach the internalized framework types.
+ ///
+ /// Grants the component itself authored are kept. Stripping those broke the supported
+ /// component-to-component arrangement: a code-fix component that reads its generator's internal
+ /// diagnostic identity compiles and passes in-repo tests against the generator's unmerged bin
+ /// output, which keeps the grant, and then fails at runtime in the compiler host with a
+ /// FieldAccessException because the merged analyzer it actually loads had the grant
+ /// removed. Keeping the component's own grants makes the merged artifact behave like the assembly
+ /// the author compiled against.
+ ///
///
- static int StripInternalsGrants(AssemblyDefinition assembly)
+ /// The merged component.
+ ///
+ /// Grants declared by the framework assembly, or to strip every grant.
+ ///
+ static int StripInternalsGrants(AssemblyDefinition assembly, ImmutableArray? frameworkInternalsGrants)
{
var removed = 0;
for (var index = assembly.CustomAttributes.Count - 1; index >= 0; index--)
{
+ var attribute = assembly.CustomAttributes[index];
+ if (!s_internalsGrantAttributeFullNames.Contains(attribute.AttributeType.FullName, StringComparer.Ordinal))
+ continue;
+
if (
- !s_internalsGrantAttributeFullNames.Contains(
- assembly.CustomAttributes[index].AttributeType.FullName,
- StringComparer.Ordinal
- )
+ frameworkInternalsGrants is { } frameworkGrants
+ && !frameworkGrants.Contains(GrantedAssemblyName(attribute), StringComparer.Ordinal)
)
{
continue;
diff --git a/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs b/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs
index d889222..76be836 100644
--- a/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs
+++ b/src/src/SourceGeneratorFramework.MergeTool/MergeToolRunner.cs
@@ -132,7 +132,12 @@ logger is not null
ownedTypeFullNames: Extend(
FrameworkTypeInternalizer.DefaultOwnedTypeFullNames,
[.. FrameworkTypeInternalizer.CollectTypeFullNames(frameworkPath), .. options.OwnedTypeFullNames]
- )
+ ),
+ // Only the framework's own grants are stripped. A grant the component authored - the one
+ // that lets a companion code-fix component read the generator's internal diagnostic
+ // identity - has to survive, or the merged analyzer the compiler host loads behaves
+ // differently from the assembly the author compiled and tested against.
+ frameworkInternalsGrants: FrameworkTypeInternalizer.CollectInternalsGrants(frameworkPath)
);
if (internalization.PublicFrameworkTypesRemaining.Length > 0)
diff --git a/src/tests/SourceGeneratorFramework.MergeTool.UnitTests/FrameworkTypeInternalizerTests.cs b/src/tests/SourceGeneratorFramework.MergeTool.UnitTests/FrameworkTypeInternalizerTests.cs
index aefbf0c..aa3882b 100644
--- a/src/tests/SourceGeneratorFramework.MergeTool.UnitTests/FrameworkTypeInternalizerTests.cs
+++ b/src/tests/SourceGeneratorFramework.MergeTool.UnitTests/FrameworkTypeInternalizerTests.cs
@@ -244,6 +244,24 @@ public sealed class PublicType
}
""";
+ ///
+ /// A component carrying both a grant the merge copied in from the framework and the component's own
+ /// grant to its companion code-fix assembly.
+ ///
+ const string ComponentWithMixedInternalsGrantsSource = """
+ using System.Runtime.CompilerServices;
+
+ [assembly: InternalsVisibleTo("Purview.SourceGeneratorFramework.UnitTests")]
+ [assembly: InternalsVisibleTo("Fixture.Component.CodeFixers")]
+
+ namespace Fixture.Component
+ {
+ public sealed class PublicType
+ {
+ }
+ }
+ """;
+
[Test]
public async Task Apply_GivenOwnedPublicTypes_InternalizesThemAndKeepsComponentsPublic(
CancellationToken cancellationToken
@@ -551,6 +569,43 @@ await Assert
.IsFalse();
}
+ // The merge copies the framework assembly's own grants into the artifact, and those must go: a
+ // shipped analyzer is not the framework's assembly. A grant the *component* authored is different -
+ // it is what lets a companion code-fix component read the generator's internal diagnostic identity.
+ // Stripping it made the merged analyzer behave differently from the unmerged bin output the author
+ // compiled and tested against, so the code fix threw FieldAccessException only in the compiler host.
+ [Test]
+ public async Task Apply_GivenFrameworkGrants_StripsOnlyThoseAndKeepsTheComponentsOwn(
+ CancellationToken cancellationToken
+ )
+ {
+ // Arrange
+ cancellationToken.ThrowIfCancellationRequested();
+ using TestWorkspace workspace = new();
+ var componentPath = workspace.Compile("Fixture.Component", ComponentWithMixedInternalsGrantsSource);
+
+ // Act
+ var report = FrameworkTypeInternalizer.Apply(
+ componentPath,
+ [workspace.GetPath("Fixture.Component")],
+ ownedNamespaces: ["Purview.SourceGeneratorFramework"],
+ frameworkInternalsGrants: ["Purview.SourceGeneratorFramework.UnitTests"]
+ );
+
+ // Assert
+ await Assert.That(report.StrippedInternalsGrantCount).IsEqualTo(1);
+
+ using var component = AssemblyDefinition.ReadAssembly(componentPath);
+ var grants = component
+ .CustomAttributes.Where(static attribute =>
+ attribute.AttributeType.FullName == "System.Runtime.CompilerServices.InternalsVisibleToAttribute"
+ )
+ .Select(static attribute => (string)attribute.ConstructorArguments[0].Value)
+ .ToArray();
+
+ await Assert.That(grants).IsEquivalentTo(["Fixture.Component.CodeFixers"]);
+ }
+
[Test]
public async Task Apply_GivenGenericConstraintsExposingFrameworkTypes_ReportsThem(
CancellationToken cancellationToken