Repository navigation
feat: add curated target framework sets and stop stripping Roslyn com… - #34
Merged
Merged
Conversation
…ponent namespaces
Roslyn component names are no longer stripped from RootNamespace. SourceGenerator, SourceGenerators,
SourceGeneration, Generators, Analyzers, CodeFixers and CodeFixes were in the suffix list, and because
the stripped value is what reaches the compiler as build_property.RootNamespace, every file whose
namespace kept the suffix failed IDE0130. That was not an edge case: it broke five of the six
repositories consuming this SDK, each of which keeps the suffix because a generator, its analyzers and
its code fixes are separate assemblies that each need their own namespace. A component's suffix is part
of its identity rather than noise. Add one back per project with NamespaceRemoveSuffix Include if a
repository really wants it collapsed.
Curated target framework sets replace a hard-coded literal as the way a project's targets are decided:
<PurviewTargetFrameworkSet>Supported</PurviewTargetFrameworkSet>
Current net10.0 (the default)
Latest net11.0
Supported net10.0;net11.0
Broad net8.0;net9.0;net10.0 every shipped release
All net8.0;net9.0;net10.0;net11.0 Broad plus the newest major
A multi-targeting repository otherwise hard-codes its TFM list in every project, so a lifecycle change
is an edit in every repository that has to be found and kept consistent. 'Current' applies when nothing
is selected and resolves to the same single TFM this SDK used to hard-code, so the out-of-box result is
unchanged. Broad and All differ by one real product decision - whether a package commits to the newest
release - so they are separate names rather than one set that silently widens every consumer.
Overrides are layered: an explicit TargetFramework/TargetFrameworks in the project always wins, then
PurviewTargetFrameworks<Set> redefines a set for a repository, then the definitions above. A
single-entry set resolves to TargetFramework so it does not pay for an outer build. An unrecognised
name fails with PurviewInvalidTargetFrameworkSet rather than silently falling back and dropping every
target asked for. Roslyn components keep netstandard2.0; one that declares a set itself is told through
PurviewTargetFrameworkSetIgnored, while inheriting a repository-wide selection stays silent because
that is the normal arrangement.
Fixed: an explicitly declared RootNamespace was overwritten. FixRootNamespaceTarget runs after the
project body, so it could not tell an author's value from a derived one and rewrote
<RootNamespace>Foo.CodeFixers</RootNamespace> to Foo - then every file in it failed IDE0130 against a
namespace nobody asked for. The declaration is now recorded during evaluation and the target leaves it
alone.
Fixed: project detection read commented-out declarations. The SDK is imported before the project body
is evaluated, so TargetFramework, TargetFrameworks, IsRoslynComponent, IsRoslynComponentOnly and
IsPackable are read from the project XML. XML comments were not stripped, so a TargetFramework left
inside a comment counted as a real declaration - and for a Roslyn component that meant skipping the
netstandard2.0 default, letting the generic .NET default fill in, and shipping an analyzer the IDE
cannot load. Comments are stripped now. Only unconditional declarations are matched, because a
Condition cannot be evaluated that early and guessing either way is worse than not matching; a
declaration that exists *only* under a Condition is reported as PurviewConditionedProjectDeclaration.
Declaring a property unconditionally and then augmenting it conditionally is correct and stays silent.
Fixed: 168 InternalsVisibleTo attributes per shipped assembly, 53 of them malformed. Three grants were
emitted per test type - from AssemblyName, TargetProjectName and MSBuildProjectName - with no empty
guard, and TargetProjectName has no default, so every test type produced a grant with an empty assembly
name ('.UnitTests'). The three are usually the same string, so each real grant was emitted up to three
times. Measured on a project using this SDK: 168 grants down to 116, malformed 53 down to 0, duplicates
0. The existing tests only asserted the attribute type appeared, which is why this went unnoticed; the
generated values are asserted now.
Fixed: mirrored .agents content dirtied every consuming repository. The packaged blanket .gitignore is
only written for a folder this SDK wholly owns - a skill is its own folder - so files mirrored directly
into .agents/agents and .agents/prompts received none and stayed tracked, and an SDK upgrade that
changed a prompt showed up as a local modification. A blanket rule there is not an option: a consuming
repository keeps its own authored prompts and agents beside the mirrored ones. The sync now writes
.agents/.gitignore from the manifest it already builds, listing only the mirrored paths, so authored
files keep their normal status. Both that file and the packaged per-skill one ignore themselves rather
than being committed - '!.gitignore' left a mirrored, never-committed file permanently untracked, and
because the nearest .gitignore wins it also overrode the generated parent list.
Tests: 436 passing, up from 409. The new coverage is the part that was missing rather than an addition
for its own sake - the detection, namespace and InternalsVisibleTo behaviour had no assertions on
observable output, which is precisely why these defects survived.
kieronlanning
enabled auto-merge
October 6, 2026 22:31
kieronlanning
disabled auto-merge
October 6, 2026 22:32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
…ponent namespaces
Roslyn component names are no longer stripped from RootNamespace. SourceGenerator, SourceGenerators, SourceGeneration, Generators, Analyzers, CodeFixers and CodeFixes were in the suffix list, and because the stripped value is what reaches the compiler as build_property.RootNamespace, every file whose namespace kept the suffix failed IDE0130. That was not an edge case: it broke five of the six repositories consuming this SDK, each of which keeps the suffix because a generator, its analyzers and its code fixes are separate assemblies that each need their own namespace. A component's suffix is part of its identity rather than noise. Add one back per project with NamespaceRemoveSuffix Include if a repository really wants it collapsed.
Curated target framework sets replace a hard-coded literal as the way a project's targets are decided:
Current net10.0 (the default)
Latest net11.0
Supported net10.0;net11.0
Broad net8.0;net9.0;net10.0 every shipped release
All net8.0;net9.0;net10.0;net11.0 Broad plus the newest major
A multi-targeting repository otherwise hard-codes its TFM list in every project, so a lifecycle change is an edit in every repository that has to be found and kept consistent. 'Current' applies when nothing is selected and resolves to the same single TFM this SDK used to hard-code, so the out-of-box result is unchanged. Broad and All differ by one real product decision - whether a package commits to the newest release - so they are separate names rather than one set that silently widens every consumer.
Overrides are layered: an explicit TargetFramework/TargetFrameworks in the project always wins, then PurviewTargetFrameworks redefines a set for a repository, then the definitions above. A single-entry set resolves to TargetFramework so it does not pay for an outer build. An unrecognised name fails with PurviewInvalidTargetFrameworkSet rather than silently falling back and dropping every target asked for. Roslyn components keep netstandard2.0; one that declares a set itself is told through PurviewTargetFrameworkSetIgnored, while inheriting a repository-wide selection stays silent because that is the normal arrangement.
Fixed: an explicitly declared RootNamespace was overwritten. FixRootNamespaceTarget runs after the project body, so it could not tell an author's value from a derived one and rewrote Foo.CodeFixers to Foo - then every file in it failed IDE0130 against a namespace nobody asked for. The declaration is now recorded during evaluation and the target leaves it alone.
Fixed: project detection read commented-out declarations. The SDK is imported before the project body is evaluated, so TargetFramework, TargetFrameworks, IsRoslynComponent, IsRoslynComponentOnly and IsPackable are read from the project XML. XML comments were not stripped, so a TargetFramework left inside a comment counted as a real declaration - and for a Roslyn component that meant skipping the netstandard2.0 default, letting the generic .NET default fill in, and shipping an analyzer the IDE cannot load. Comments are stripped now. Only unconditional declarations are matched, because a Condition cannot be evaluated that early and guessing either way is worse than not matching; a declaration that exists only under a Condition is reported as PurviewConditionedProjectDeclaration. Declaring a property unconditionally and then augmenting it conditionally is correct and stays silent.
Fixed: 168 InternalsVisibleTo attributes per shipped assembly, 53 of them malformed. Three grants were emitted per test type - from AssemblyName, TargetProjectName and MSBuildProjectName - with no empty guard, and TargetProjectName has no default, so every test type produced a grant with an empty assembly name ('.UnitTests'). The three are usually the same string, so each real grant was emitted up to three times. Measured on a project using this SDK: 168 grants down to 116, malformed 53 down to 0, duplicates 0. The existing tests only asserted the attribute type appeared, which is why this went unnoticed; the generated values are asserted now.
Fixed: mirrored .agents content dirtied every consuming repository. The packaged blanket .gitignore is only written for a folder this SDK wholly owns - a skill is its own folder - so files mirrored directly into .agents/agents and .agents/prompts received none and stayed tracked, and an SDK upgrade that changed a prompt showed up as a local modification. A blanket rule there is not an option: a consuming repository keeps its own authored prompts and agents beside the mirrored ones. The sync now writes .agents/.gitignore from the manifest it already builds, listing only the mirrored paths, so authored files keep their normal status. Both that file and the packaged per-skill one ignore themselves rather than being committed - '!.gitignore' left a mirrored, never-committed file permanently untracked, and because the nearest .gitignore wins it also overrode the generated parent list.
Tests: 436 passing, up from 409. The new coverage is the part that was missing rather than an addition for its own sake - the detection, namespace and InternalsVisibleTo behaviour had no assertions on observable output, which is precisely why these defects survived.