Context
The Scribe SDK (#4) and LocalScribe infrastructure currently support one workflow well: standalone analyzer/generator packages developed in their own repository and consumed via NuGet (e.g. Hermetic consumed by Mish). This is the "framework" or "library" use case — the analyzer is a public product with its own release cycle.
But there's a second, equally common workflow that has no first-class support today: solution-local analyzers. These are analyzers and generators that live inside the solution they serve, generating code specific to that solution's domain. They are not published to NuGet. They are not consumed cross-repo. They exist to automate this codebase and nothing else.
The two kinds of analyzer projects
| Kind |
Example |
Published? |
Dev mechanism |
| Standalone |
Hermetic (public framework) |
Yes, to NuGet |
LocalScribe for cross-repo dev |
| Solution-local |
A solution-specific generator that emits DTOs from domain models |
No |
Should "just work" intra-solution |
Examples of solution-local analyzers
- A generator that reads domain model types and emits mapping code, DTOs, or validation rules
- An analyzer that enforces solution-specific coding conventions (e.g. "all command handlers must be nested inside their command type")
- A generator that emits strongly-typed configuration accessors from appsettings schema
- Hermetic itself, before it was extracted into a separate repo — it started as a solution-local generator inside Mish
The problem today
To use a solution-local analyzer, developers must:
- Build the analyzer project manually (or rely on build ordering)
- Reference it with
<ProjectReference OutputItemType="Analyzer" ReferenceOutputAssembly="false"> — a verbose, error-prone incantation
- Manage TFM negotiation (
SetTargetFramework="TargetFramework=netstandard2.0") since the analyzer targets netstandard2.0 but the consuming project targets net8.0+
- Handle dependency DLL exposure via
GetDependencyTargetPaths / TargetPathWithTargetPlatformMoniker so the Roslyn host can load private deps
- Rebuild manually when the analyzer changes — no automatic rebuild-on-change
This is the same boilerplate wall that the SDK was designed to eliminate, but applied at the consumption side rather than the production side.
Goal
Add first-class support for solution-local analyzers to the Scribe SDK. When the SDK detects a marker on an analyzer project (e.g. a project property or reference pattern), it should automatically:
- Pack the analyzer on build — produce a local
.nupkg without publishing to any feed
- Inject it as a local NuGet reference for consuming projects in the same solution
- Just work — no sentinel file, no cross-repo setup, no manual configuration
This closes the full dev loop for ~90% of analyzer workflows without requiring the LocalScribe mechanism.
Desired developer experience
<!-- Analyzer project — uses the SDK, marks itself as solution-local -->
<Project Sdk="BulletsForHumanity.Scribe.Sdk">
<PropertyGroup>
<ScribeSolutionAnalyzer>true</ScribeSolutionAnalyzer>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="BulletsForHumanity.Scribe" PrivateAssets="all" />
</ItemGroup>
</Project>
<!-- Consuming project — references the analyzer like any other package -->
<ItemGroup>
<PackageReference Include="MyAnalyzer" />
</ItemGroup>
The SDK handles the rest: auto-pack on build, local feed injection, version management, rebuild-on-change.
Design Considerations
Mechanism: auto-pack + local NuGet injection
Unlike the ProjectReference approach (which requires TFM negotiation and dependency exposure hacks), this uses NuGet's own resolution — the analyzer is packed into a proper .nupkg and placed in a solution-local package directory. Consuming projects reference it as a PackageReference, getting all the same analyzer wiring that any published analyzer package gets.
This is similar in spirit to LocalScribe's auto-pack mechanism but:
- No
.localscribe sentinel — triggered by ScribeSolutionAnalyzer=true on the project
- No cross-repo concerns — everything stays within the solution
- No version override files — version is managed automatically (e.g. always
0.0.0-local)
- No shared
.artifacts/ directory — uses a solution-local directory (e.g. .packages/)
Detection mechanism
Options for marking a project as a solution-local analyzer:
- Property-based —
<ScribeSolutionAnalyzer>true</ScribeSolutionAnalyzer> in the project. Simple, explicit.
- Convention-based — Any Scribe SDK project that is
IsPackable=false (or has no PackageId) is assumed solution-local.
- Reference-based — Detect when a non-SDK project references a Scribe SDK project and auto-wire.
Recommendation: start with explicit property (#1).
Relationship to LocalScribe
This is not the same as LocalScribe. Key differences:
| Concern |
LocalScribe |
Solution Analyzer |
| Scope |
Cross-repo (Scribe to Hermetic to Mish) |
Intra-solution |
| Trigger |
.localscribe sentinel file |
ScribeSolutionAnalyzer=true property |
| Published? |
Yes (eventually to NuGet) |
Never |
| Version management |
NBGV + override files |
Fixed local version (e.g. 0.0.0-local) |
| Package directory |
Shared .artifacts/packages/ |
Solution-local (e.g. .packages/) |
Interaction with templates (#5)
The scribe-analyzer and scribe-generator templates should consider adding a --solution-local flag (or a separate template variant) that:
- Adds
<ScribeSolutionAnalyzer>true</ScribeSolutionAnalyzer>
- Skips NuGet publishing configuration
- Includes a sample consuming project with the correct
PackageReference
What the SDK targets need to do
On the analyzer project side:
- Detect
ScribeSolutionAnalyzer=true
- Auto-pack on build (not just on explicit
dotnet pack)
- Output
.nupkg to a well-known solution-local directory
- Use a fixed version (e.g.
0.0.0-local) to avoid cache/restore issues
- Ensure the package contains analyzer DLLs in
analyzers/dotnet/cs/ (already handled by SDK)
On the consuming project side:
- A
NuGet.config or Directory.Build.props adds the solution-local package directory as a NuGet source
- Standard
PackageReference resolution handles the rest
- IDE picks up analyzer diagnostics and generated code automatically
Open questions
- Package directory location —
.packages/ at solution root? obj/ of the analyzer project? A well-known subdirectory?
- Version strategy — Fixed
0.0.0-local? Timestamp-based? Does NuGet cache invalidation work correctly?
NuGet.config management — Should the SDK auto-generate/update it? Or require the developer to add the local source once?
- Multi-analyzer solutions — What if a solution has 3 solution-local analyzers? All pack to the same directory?
- IDE experience — Does VS/Rider pick up changes automatically when the
.nupkg is rebuilt, or is a restore needed?
Acceptance criteria
References
Context
The Scribe SDK (#4) and LocalScribe infrastructure currently support one workflow well: standalone analyzer/generator packages developed in their own repository and consumed via NuGet (e.g. Hermetic consumed by Mish). This is the "framework" or "library" use case — the analyzer is a public product with its own release cycle.
But there's a second, equally common workflow that has no first-class support today: solution-local analyzers. These are analyzers and generators that live inside the solution they serve, generating code specific to that solution's domain. They are not published to NuGet. They are not consumed cross-repo. They exist to automate this codebase and nothing else.
The two kinds of analyzer projects
Examples of solution-local analyzers
The problem today
To use a solution-local analyzer, developers must:
<ProjectReference OutputItemType="Analyzer" ReferenceOutputAssembly="false">— a verbose, error-prone incantationSetTargetFramework="TargetFramework=netstandard2.0") since the analyzer targets netstandard2.0 but the consuming project targets net8.0+GetDependencyTargetPaths/TargetPathWithTargetPlatformMonikerso the Roslyn host can load private depsThis is the same boilerplate wall that the SDK was designed to eliminate, but applied at the consumption side rather than the production side.
Goal
Add first-class support for solution-local analyzers to the Scribe SDK. When the SDK detects a marker on an analyzer project (e.g. a project property or reference pattern), it should automatically:
.nupkgwithout publishing to any feedThis closes the full dev loop for ~90% of analyzer workflows without requiring the LocalScribe mechanism.
Desired developer experience
The SDK handles the rest: auto-pack on build, local feed injection, version management, rebuild-on-change.
Design Considerations
Mechanism: auto-pack + local NuGet injection
Unlike the
ProjectReferenceapproach (which requires TFM negotiation and dependency exposure hacks), this uses NuGet's own resolution — the analyzer is packed into a proper.nupkgand placed in a solution-local package directory. Consuming projects reference it as aPackageReference, getting all the same analyzer wiring that any published analyzer package gets.This is similar in spirit to LocalScribe's auto-pack mechanism but:
.localscribesentinel — triggered byScribeSolutionAnalyzer=trueon the project0.0.0-local).artifacts/directory — uses a solution-local directory (e.g..packages/)Detection mechanism
Options for marking a project as a solution-local analyzer:
<ScribeSolutionAnalyzer>true</ScribeSolutionAnalyzer>in the project. Simple, explicit.IsPackable=false(or has noPackageId) is assumed solution-local.Recommendation: start with explicit property (#1).
Relationship to LocalScribe
This is not the same as LocalScribe. Key differences:
.localscribesentinel fileScribeSolutionAnalyzer=trueproperty0.0.0-local).artifacts/packages/.packages/)Interaction with templates (#5)
The
scribe-analyzerandscribe-generatortemplates should consider adding a--solution-localflag (or a separate template variant) that:<ScribeSolutionAnalyzer>true</ScribeSolutionAnalyzer>PackageReferenceWhat the SDK targets need to do
On the analyzer project side:
ScribeSolutionAnalyzer=truedotnet pack).nupkgto a well-known solution-local directory0.0.0-local) to avoid cache/restore issuesanalyzers/dotnet/cs/(already handled by SDK)On the consuming project side:
NuGet.configorDirectory.Build.propsadds the solution-local package directory as a NuGet sourcePackageReferenceresolution handles the restOpen questions
.packages/at solution root?obj/of the analyzer project? A well-known subdirectory?0.0.0-local? Timestamp-based? Does NuGet cache invalidation work correctly?NuGet.configmanagement — Should the SDK auto-generate/update it? Or require the developer to add the local source once?.nupkgis rebuilt, or is a restore needed?Acceptance criteria
ScribeSolutionAnalyzer=trueauto-packs on buildPackageReference.nupkgfiles are published to any external feeddotnet build,dotnet test, and IDE (Visual Studio / Rider)References
--solution-localflag)docs/— the cross-repo mechanism this complements but does not replace