This file provides guidance to Claude Code (claude.ai/code) and other AI agents when
working with code in this repository. CLAUDE.md is a symlink to this file. Per-directory
CLAUDE.md files also exist under src/, tools/, and tests/ — read them when working
in those areas.
This is an opinionated MSBuild SDK (not an application): it ships standardized defaults,
policy enforcement, and analyzer injection that consuming .csprojs opt into with
<Project Sdk="ANcpLua.NET.Sdk">. The "code" is mostly .props/.targets/.editorconfig.
Branch protection, auto-merge, release flow, and the cross-repo bootstrap rules for the four ANcpLua framework repos are documented once at ANcpLua/renovate-config. This file documents conventions specific to this repo only.
| Package | Base SDK | Purpose |
|---|---|---|
ANcpLua.NET.Sdk |
Microsoft.NET.Sdk | Standard .NET libraries / apps |
ANcpLua.NET.Sdk.Web |
Microsoft.NET.Sdk.Web | ASP.NET Core web projects |
ANcpLua.NET.Sdk.Test |
Microsoft.NET.Sdk | Test projects (xUnit v3 on MTP) |
ANcpLua.NET.Sdk.Templates |
— | dotnet new templates (not an MSBuild SDK) |
The first three are consumed via msbuild-sdks in a consumer's global.json.
This repo does not build with its own SDK — that would be a bootstrap cycle. The repo's
own projects use plain Microsoft.NET.Sdk and only dogfood the shipped editorconfigs:
Directory.Build.props injects src/Config/*.editorconfig via <EditorConfigFiles> (see
EnableEditorConfigDogfooding). Consequence: dotnet build does not exercise the SDK's
import chain — the test suite does, by packing the SDK and building throwaway sample
projects against it (see Testing below).
# Build the repo's own projects + tools (dogfoods editorconfigs, uses Microsoft.NET.Sdk)
dotnet build ANcpLua.NET.Sdk.slnx
# Run the SDK behavior tests (xUnit v3 on Microsoft Testing Platform)
dotnet test tests/ANcpLua.Sdk.Tests/ANcpLua.Sdk.Tests.csproj
# Filter (MTP syntax — see tests/ANcpLua.Sdk.Tests/CLAUDE.md):
# --filter-method "*BannedApi*" --filter-class "*MtpDetectionTests"
# Pack all 4 NuGet packages to artifacts/ (stamps the version into Version.props first)
pwsh ./build.ps1 -Version <x.y.z> # local pack only; CI computes the real version from the latest tag
# or a single package: dotnet pack src/ANcpLua.NET.Sdk.csproj -c Release -o artifacts
# Regenerate GENERATED files after changing their inputs (do not hand-edit the outputs):
dotnet run --project tools/SdkGenerator # -> src/Sdk/<variant>/Sdk.props|targets
dotnet run --project tools/ConfigFilesGenerator # -> src/Config/Analyzer.*.editorconfig, BannedSymbols.*.txtglobal.json pins .NET SDK 10.0.300 (rollForward latestFeature, prerelease allowed) and
sets the test runner to Microsoft.Testing.Platform.
Per-variant entry points live in src/Sdk/<variant>/{Sdk.props,Sdk.targets} and are
generated by tools/SdkGenerator — they are intentionally minimal; all real logic lives
in src/Build/. Sdk.props wires the shared logic in via MSBuild's standard extension hooks
rather than direct imports:
src/Sdk/<variant>/Sdk.props
├─ sets CustomBeforeDirectoryBuildProps += Build/Common/Common.props
├─ sets BeforeMicrosoftNETSdkTargets += Build/Common/Common.targets
├─ Import Build/Common/Version.props # version constants (source of truth)
├─ Import Build/Common/GlobalPackages.props # analyzer injection (GlobalPackageReference)
├─ Import Microsoft.NET.Sdk[.Web] Sdk.props # base SDK
├─ Import Build/Enforcement/Enforcement.props # banned-package policy
└─ Import Build/Enforcement/DeterminismAndSourceLink.props
Build/Common/Common.props → imports ContinuousIntegrationBuild.props
Build/Common/Common.targets → imports, in order:
SourceGenerators.targets
Tests.targets (only when IsTestProject == true)
Npm.targets
../Enforcement/VersionEnforcement.targets # AL0018: verifies Version.props was imported
The Common.props/Common.targets files run because the base Microsoft SDK honors the
CustomBeforeDirectoryBuildProps / BeforeMicrosoftNETSdkTargets hooks — they are not
imported directly by Sdk.props. (Common.props is imported directly only in the rarer
"inner SDK" case where the project already imports Microsoft.NET.Sdk.)
| Property | Detection logic |
|---|---|
IsTestProject |
ANcpLuaSdkName == ANcpLua.NET.Sdk.Test, or name ends in .Test/.Tests, or an AncpLuaTest ProjectCapability |
ANcpLuaSingleFileApp |
FileBasedProgram == true (.NET 10+ #:sdk file-based program) |
_IsSourceGeneratorProject |
IsRoslynComponent == true, or lowercased project name contains analyzer or generator, or IsSourceGenerator == true |
IsWebProject / IsApiProject |
name ends in .Web/.Api, or an AncpLuaWeb/AncpLuaApi ProjectCapability |
Capability-based detection (ProjectCapability items) is the preferred path; filename
matching is the fallback. See Directory.Build.targets.
Set in src/Build/Common/Common.props (for consumers) and mirrored in Directory.Build.props
(for this repo's own dogfooding). All use Condition="'$(X)' == ''" so consumers can override.
| Property | Value | Notes |
|---|---|---|
LangVersion |
latest |
|
Nullable |
enable |
|
ImplicitUsings |
enable |
|
Deterministic |
true |
reproducible builds |
EnableNETAnalyzers |
true |
|
AnalysisLevel |
latest-all |
|
TreatWarningsAsErrors |
true |
only in CI (ContinuousIntegrationBuild) or Release |
EnforceCodeStyleInBuild |
true |
same CI/Release gate |
NuGetAudit |
true, mode all, level low |
audit warnings become errors in CI/Release |
ManagePackageVersionsCentrally |
true |
CPM is mandatory (see below) |
CentralPackageTransitivePinningEnabled |
true |
Every consuming repo MUST ship a Directory.Packages.props (even an empty one with
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>). Without it, restore
fails with NU1015 because the SDK injects its analyzers as GlobalPackageReference, which
only resolves under CPM. This is the #1 consumer support issue.
Analyzers are injected as GlobalPackageReference (immutable under CPM) by
GlobalPackages.props. Injection is skipped for the analyzer's own package
(PackageId == ANcpLua.Analyzers) and has a non-CPM fallback branch.
| Injected analyzer | Scope |
|---|---|
ANcpLua.Analyzers |
all projects (the AL-rule set; counts drift — see that repo's AnalyzerReleases.*.md) |
Microsoft.CodeAnalysis.BannedApiAnalyzers |
all projects (src/Config/BannedSymbols*.txt) |
AwesomeAssertions.Analyzers |
test projects only |
Opt out entirely with <DisableANcpLuaAnalyzers>true</DisableANcpLuaAnalyzers>.
Banned packages (enforced in Enforcement.props): PolySharp (use
ANcpLua.Roslyn.Utilities.Polyfills), FluentAssertions (license — use AwesomeAssertions),
Microsoft.NET.Test.Sdk when MTP is enabled, and DisableTransitiveProjectReferences
(breaks CPM).
When IsTestProject == true, Tests.targets sets OutputType=Exe, injects xunit.v3.mtp-v2
AwesomeAssertions(+ its analyzer), adds global usingsXunitandAwesomeAssertions, and configures TRX reporting. It auto-detects TUnit/NUnit/MSTest and setsSkipXunitInjectionso those frameworks coexist; you can also set<SkipXunitInjection>true</SkipXunitInjection>manually.
Projects detected as source generators/analyzers get Microsoft.CodeAnalysis.CSharp pinned to
SourceGeneratorRoslynVersion (defaults to $(RoslynVersion) from Version.props) via
VersionOverride, keeping CPM intact. This implicit injection is fragile under IDE language
servers (Rider/Roslyn LSP/OmniSharp evaluating Sdk.props before Common.props); consumers
hitting that can set <DisableImplicitRoslynPackageReference>true</DisableImplicitRoslynPackageReference>
and declare the PackageReference explicitly. Override the version with
<SourceGeneratorRoslynVersion>…</SourceGeneratorRoslynVersion>.
The SDK does not pin or auto-inject ANcpLua.Roslyn.Utilities, ANcpLua.Agents, or
ANcpLua.Analyzers as runtime deps for consumers — consumers reference them like any other
NuGet package (use …Roslyn.Utilities.Sources for netstandard2.0 generators,
…Roslyn.Utilities for net10.0+ runtime). Decoupled release cadence is intentional.
SDK-internal exception (not part of the SDK contract): this repo's own test project is
an ordinary consumer of ANcpLua.Roslyn.Utilities.Testing, so its version is pinned in
Directory.Packages.props — ANcpLuaRoslynUtilitiesTestingVersion, in the
PropertyGroup Label="SDK-internal first-party pins". It lives there, not in
Version.props, on purpose: Version.props ships inside the packages and flows to every
consumer, so a self-test-only dependency must stay out of it. The pin exists so the AL analyzer
lint and CPM (which require a version for every PackageReference) stay happy.
src/Build/Common/Version.props defines all package versions used across the SDK
ecosystem and is shipped inside the NuGet packages, flowing into every consumer. Notes:
ANcpSdkPackageVersionis a999.9.9placeholder;build.ps1stamps the real, CI-computed version at pack time (do not hard-code it).- Versions carry inline comments explaining floors/pins (e.g.
YamlDotNetfloored by a transitiveMeziantou.Framework.DependencyScanningconstraint) — read them before bumping.
Die vier Repos bilden eine Bootstrap-Kette: Roslyn.Utilities → NET.Sdk → (Analyzers, Agents).
Truth-Source für Paket-Versionen ist src/Build/Common/Version.props, in die SDK-Packages
gepackt und in jedes Consumer-Projekt geladen. Ein lokales Version.props (sofern vorhanden)
wird nach der SDK-Datei importiert (last-wins) — um lokal AHEAD der publizierten SDK zu pinnen.
Bevor du eine Variable bumpst:
- Truth fließt durch GlobalPackageReference. Zeigt Truth auf eine Version, die noch nicht
auf nuget.org liegt, scheitert jeder Restore mit
NU1102— auch die SDK-eigenen Tests (sie packen ein Sample-Projekt und builden es). Reihenfolge: erst das ausgeschriebene Repo taggen + auf NuGet bringen, dann Truth nachziehen. - Self-Reference zeigt auf last-PUBLISHED. Eine Variable für das eigene Paket des Repos
muss auf die zuletzt publizierte Version zeigen, nicht auf die hochkommende — beim Restore
(vor Pack) existiert die neue Version noch nicht; CI stampt sie per
-p:Version=X.Y.Z. - Bumps haben transitive Konsequenzen unter CPM. Downgrade ist
NU1109(Hard-Error). Wenn ein Bump nicht greift, steht der Grund in der Restore-Fehlermeldung — lesen. - Lokales Override gleich/unter Truth ist Müll (Doppelpflege bzw. stille Regression) — prunen, sobald die SDK mit matching Werten publisht.
- Publish-Trigger ist repo-abhängig. Roslyn.Utilities, NET.Sdk und Agents publishen auf
Push nach
main(CI auto-bumpt den Patch vom letztenv*-Tag und legt den Tag danach an); Analyzers publisht aufv*-Tag-Push. So oder so: ein build-broken Commit publisht nicht — nächste Patch-Version nehmen statt einen Remote-Tag umzuhängen. - Verifiziere Versionen vor dem Bump via
https://api.nuget.org/v3-flatcontainer/<lowercased-id>/index.json.
src/
ANcpLua.NET.Sdk[.Web|.Test|.Templates].csproj / .nuspec # the packaging projects
Build/
Common/ # Version.props (truth), Common.props/.targets, GlobalPackages.props,
# SourceGenerators.targets, Tests.targets, Npm.targets, CI detection
Enforcement/ # Enforcement.props (banned pkgs), Determinism…, VersionEnforcement.targets
Config/ # FLAT since v3.3.0: Analyzer.*.editorconfig, *.editorconfig style files,
# BannedSymbols*.txt, default.runsettings (GENERATED — see tools/)
Sdk/<variant>/ # GENERATED Sdk.props/Sdk.targets entry points (see tools/SdkGenerator)
tests/ANcpLua.Sdk.Tests/ # SDK behavior tests (pack SDK → build sample projects → assert)
tools/ # SdkGenerator, ConfigFilesGenerator (standalone exes, net10.0, not packed)
templates/ # ancplua-app / ancplua-lib / ancplua-web (dotnet new sources)
Tests build real throwaway projects against the packed SDK. PackageFixture packs the SDK
once per run; SdkProjectBuilder.Create(fixture) spins up an isolated project you configure
fluently and BuildAsync(). Tests parametrize over three import styles — <Project Sdk="…">,
<Sdk Name="…"/>, and SDK-in-Directory.Build.props — to prove all wiring paths work.
- nuspec
<file>entries must be enumerated, not globbed.<file src="Config/*"/>expanded to zero matches on Windows pack agents (v3.3.0 regression); seesrc/*.nuspec. - Git tags use a
vprefix (v3.4.x); CI'sgh release createmakes the tag so it matches the CLI push convention. Don't re-assign an existing remote tag — use the next patch version. - The publish gate watches
'*.nuspec' 'src/**/*' 'tests/**/*'. Editing only a workflow or docs leaveshas_changesfalse by design (the deploy job reports success with zero steps) — touch a watched path to force a publish.