This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is an MSBuild SDK package (ktsu.Sdk) that provides standardized configuration, metadata management, and build workflows for .NET projects. The SDK automatically discovers solution structures, generates namespaces from directory paths, and manages project metadata through markdown files.
dotnet build --configuration Release --verbosity normal --no-incrementaldotnet test -m:1 --configuration Release --verbosity normal --no-builddotnet pack --configuration Release --output ./stagingdotnet publish <project>.csproj --no-build --configuration Release --framework net10.0 --output ./output/<project>Version management is handled through PowerShell scripts in the scripts/ directory using the PSBuild module:
- make-version.ps1: Calculates semantic version from git history
- make-license.ps1: Generates LICENSE.md from template
- make-changelog.ps1: Generates CHANGELOG.md from git commits
- commit-metadata.ps1: Commits metadata changes with proper attribution
Version calculation rules:
[major]tag in commit: major version increment (breaking changes)[minor]tag or public API changes: minor version increment[patch]tag or code changes: patch version increment[pre]tag or minimal changes: prerelease increment
The PSBuild module automatically detects public API changes by analyzing diffs for modifications to public classes, interfaces, methods, properties, etc.
The SDK consists of multiple sub-SDKs:
-
Sdk/: Core SDK with MSBuild props and targets (all project types)
Sdk.props: Hierarchical solution discovery, metadata file loading, namespace generation, package configurationSdk.targets: Project type detection, automatic references, package inclusion logic
-
Sdk.ConsoleApp/: Console application SDK
- Sets
OutputType=ExeandTargetFramework=net10.0
- Sets
-
Sdk.App/: GUI application SDK (ImGui/Windows apps)
- Sets
OutputType=WinExeon Windows,Exeon other platforms - Sets
TargetFramework=net10.0 - Configures runtime identifiers for cross-platform GUI support
- Sets
-
Sdk.Tool/: .NET tool SDK (
dotnet tool install)- Sets
PackAsTool=true,OutputType=Exe,TargetFramework=net10.0 - Clears
RuntimeIdentifiers: underPackAsToolthe .NET 10 SDK turns each RID in the inherited desktop list into a separate RID-specific tool package, so onedotnet packemits seven packages racing over a single intermediate output directory. Tools here are framework-dependent and RID-agnostic — consumers need the .NET 10 runtime. - Derives
ToolCommandNamefrom the lowercased solution name (stripping a trailing.tool/.cli), because the default would beAssemblyName, which the core SDK forces to the fully-qualified namespace (ktsu.KtsuBuild.Tool). Derived inSdk.props, notSdk.targets, so the value is set before Microsoft.NET.Sdk defaults it. - Disables package validation and
IncludeSource, which are library-oriented - Sets
IsPublishable=trueinSdk.props.PackAsToolbuilds thetools/payload from a publish, which is gated onIsPublishable; without it the package contains onlyDotnetToolSettings.xmland none of the assemblies it points at — it installs, then fails at run time. The core SDK'sSdk.targetsflip (false in props, true forOutputType=Exein targets) is too late, for the same import-ordering reason asToolCommandName. Tool projects stay out of CI's RID zip publishing by project selection (KtsuBuild scans the csproj text), not by this property. - Errors (KTSU1001) if
TargetFrameworksis set: a tool package cannot multi-target
- Sets
-
Sdk.Windows/, Sdk.Linux/, Sdk.macOS/: Desktop per-OS app SDKs
- RID-based presets on the base
net10.0runtime (no extra prerequisites) - Narrow
RuntimeIdentifiersto the target OS and defaultRuntimeIdentifier - Windows uses
OutputType=WinExe; Linux/macOS useExe
- RID-based presets on the base
-
Sdk.iOS/, Sdk.Android/: Mobile app SDKs (TFM + workload based)
- Set
TargetFramework=net10.0-ios/net10.0-androidplusSupportedOSPlatformVersion - Consuming projects require the
ios/androidworkloads (dotnet workload install android ios maui); iOS additionally needs a macOS host - The SDK packages themselves carry no workload dependency and pack on any host
- Set
The SDK searches up to 5 directory levels from the project directory to find solution files. This enables nested project structures without manual configuration.
Namespaces are automatically generated from directory structure:
MySolution/src/Core/Utils/MyProject.csproj
→ ProjectNamespace: src.Core.Utils.MyProject
→ RootNamespace: {AuthorsNamespace}.src.Core.Utils.MyProject
The SDK intelligently handles cases where the directory name matches the project name to avoid duplication.
The SDK automatically detects project types based on naming conventions:
- Primary Project:
{SolutionName}or{SolutionName}.Core - Console Projects:
{SolutionName}.CLI,{SolutionName}.Cli,{SolutionName}Cli,{SolutionName}CLI,{SolutionName}.ConsoleApp,{SolutionName}.Console - GUI Projects:
{SolutionName}.App,{SolutionName}App,{SolutionName}.WinApp,{SolutionName}WinApp,{SolutionName}.ImGuiApp,{SolutionName}ImGuiApp - iOS Projects:
{SolutionName}.iOS,{SolutionName}iOS,{SolutionName}.Ios - Android Projects:
{SolutionName}.Android,{SolutionName}Android,{SolutionName}.Droid - Windows Projects:
{SolutionName}.Windows,{SolutionName}Windows,{SolutionName}.Win - Linux Projects:
{SolutionName}.Linux,{SolutionName}Linux - macOS Projects:
{SolutionName}.macOS,{SolutionName}.MacOS,{SolutionName}.Mac - Tool Projects:
{SolutionName}.Tool,{SolutionName}Tool— deliberately not.CLI, so no existing console project silently starts publishing itself as a tool package - Test Projects:
{SolutionName}.Test,{SolutionName}.Tests,{SolutionName}Test,{SolutionName}Tests
Properties set based on detection: IsPrimaryProject, IsCliProject, IsAppProject, IsToolProject, IsIosProject, IsAndroidProject, IsWindowsProject, IsLinuxProject, IsMacProject, IsTestProject
The SDK automatically includes the ktsu.Sdk.Analyzers package (with version synchronization via {version} placeholder) to enforce proper project configuration:
- KTSU0001 (Error): Projects must include required standard packages (Polyfill, System.Memory, System.Threading.Tasks.Extensions). Requirements vary based on project type and target framework. SourceLink is intentionally not required: the .NET 8+ SDK bundles SourceLink and enables it implicitly, and an explicit
Microsoft.SourceLink.*PackageReferencere-enables the noisy "Source control information is not available" warning for any build without a usable remote. Consumers should not reference SourceLink packages directly. - KTSU0002 (Error): Projects must expose internals to test projects using
[assembly: InternalsVisibleTo(...)]. A code fixer is available to automatically add this attribute. - KTSU0003 (Error): Use
Ensure.NotNull()overArgumentNullException.ThrowIfNull()for better framework compatibility. A code fixer is available to automatically replace the invocation. - KTSU0004 (Error): Use
Ensure.NotNull()instead of manual null checks with ArgumentNullException. Detects patterns likeif (x == null) throw new ArgumentNullException(...),if (x is null) throw ..., andx ?? throw .... A code fixer is available. - KTSU0005 (Error): Orphaned
PackageVersionentries. FlagsPackageVersionentries inDirectory.Packages.props(Central Package Management) that no project in the solution references viaPackageReference/GlobalPackageReference. A code fixer removes the orphaned entry. Disable with<KtsuEnableOrphanedPackageVersionAnalysis>false</KtsuEnableOrphanedPackageVersionAnalysis>. An ignore list (Sdk.targets) keeps SDK-governed packages from being flagged even without a directPackageReference: the KTSU0001 standard packages (Polyfill,System.Memory,System.Threading.Tasks.Extensions) and theMicrosoft.Testing.Extensions.*runner family that test SDKs (e.g.MSTest.Sdk) inject into test projects (which the scan skips). Consumers can extend it via<KtsuOrphanedPackageVersionIgnore Include="..." />. - KTSU0006 (Error): Transitive package used directly. Flags use of a type or member that originates from a transitive package dependency when the project does not declare a direct
PackageReferenceto it. A code fixer adds thePackageReference(and, under Central Package Management, a matchingPackageVersion). Disable with<KtsuEnableTransitivePackageAnalysis>false</KtsuEnableTransitivePackageAnalysis>.
These properties are passed to analyzers via CompilerVisibleProperty: IsTestProject, TestProjectExists, TestProjectNamespace, TargetFramework, TargetFrameworkIdentifier, HasPolyfill, HasSystemMemory, HasSystemThreadingTasksExtensions, ManagePackageVersionsCentrally.
Package-graph analyzer inputs: KTSU0005 and KTSU0006 require solution-wide / post-restore facts that a per-project Roslyn analyzer cannot observe on its own. The SDK targets _KtsuGenerateOrphanedPackageVersionInputs and _KtsuGenerateTransitivePackageInputs (in Sdk/Sdk.targets) compute these facts at build time and surface them to the analyzers as AdditionalFiles (orphan list, assembly→package map, direct-package set), alongside the Directory.Packages.props and .csproj XML files that the code fixers edit via AdditionalDocuments.
Polyfill Configuration: For non-test projects, the SDK automatically sets:
PolyEnsure=true- Enables ensure/guard clause polyfillsPolyNullability=true- Enables nullability-related polyfillsPolyArgumentExceptions=true- Enables argument exception polyfillsPolyStringInterpolation=true- Enables string interpolation polyfills
The SDK reads markdown files from the solution root and uses them to populate package metadata:
AUTHORS.md→ Authors, AuthorsNamespaceVERSION.md→ Version, PackageVersionDESCRIPTION.md→ Description, PackageDescription (checked in project directory first, then solution directory)CHANGELOG.md→ PackageReleaseNotes (truncated at 35KB if needed)TAGS.md→ Tags, PackageTags (checked in project directory first, then solution directory)LICENSE.md→ PackageLicenseFileREADME.md→ PackageReadmeFile (checked in project directory first, then solution directory)COPYRIGHT.md→ CopyrightPROJECT.url→ ProjectUrl, PackageProjectUrlAUTHORS.url→ AuthorsUrlicon.png→ PackageIcon
All metadata files are automatically included in NuGet packages.
Default: net10.0;net9.0;net8.0;net7.0;net6.0;net5.0;netstandard2.0;netstandard2.1
Individual SDK sub-projects (ConsoleApp, App) override TargetFrameworks to target a single framework (net10.0).
LangVersion=latestNullable=enableTreatWarningsAsErrors=trueAnalysisLevel=latest-allEnforceCodeStyleInBuild=true
EnablePackageValidation=trueApiCompatValidateAssemblies=trueEnableStrictModeForBaselineValidation=true— real breaking changes vs a published baseline are caughtEnableStrictModeForCompatibleFrameworksInPackage=falseandEnableStrictModeForCompatibleTfms=false— strict cross-TFM validation is intentionally off. This SDK mandates Polyfill + broad multi-targeting, and Polyfill source-embeds framework shim types whose shape legitimately differs per TFM; strict mode reports those as false-positive breaking changes (CP0002/CP0014/CP0015/CP0016). Baseline validation stays on, and package consumers are unaffected (validation is producer-side only). Repos capture any residual non-strict compatible-framework diffs in a regenerableCompatibilitySuppressions.xml(dotnet pack -p:ApiCompatGenerateSuppressionFile=true).
Default RIDs: win-x64;win-x86;win-arm64;osx-x64;linux-x64;osx-arm64;linux-arm64
The GitHub Actions workflow (.github/workflows/dotnet-sdk.yml) runs on:
- Push to
mainordevelopbranches - Pull requests
- Nightly schedule (11 PM UTC)
- Manual workflow dispatch
The workflow uses .NET SDK 10.0.
Release process (only on main branch, non-fork):
- Generate VERSION.md, LICENSE.md, CHANGELOG.md from git history
- Update analyzer releases with
make-analyzer-releases.ps1 - Commit metadata changes with bot attribution
- Commit Sdk.props/Sdk.targets version updates
- Build all projects
- Run tests
- Create NuGet packages
- Publish to GitHub Packages, NuGet.org, and ktsu.dev package feeds
- Create GitHub release with artifacts
- Create directory:
Sdk.{Name}/ - Create
Sdk.{Name}.csprojwith appropriateTargetFrameworks - Create
Sdk.propswith project-type-specific property overrides - Create
Sdk.targetsif custom build logic needed - Package structure: SDK packages must include
Sdk/Sdk.propsandSdk/Sdk.targetsin the package
- Solution/project discovery: Edit
Sdk/Sdk.props(lines 1-70) - Project type detection: Edit
Sdk/Sdk.props(lines 72-187) - Metadata file loading: Edit
Sdk/Sdk.props(lines 189-248) - Namespace generation: Edit
Sdk/Sdk.props(lines 249-287) - Package configuration: Edit
Sdk/Sdk.props(lines 289-330) - Package reference detection: Edit
Sdk/Sdk.targets(lines 29-53) - Polyfill configuration: Edit
Sdk/Sdk.targets(lines 76-82)
- Build the SDK:
dotnet build --configuration Release - Pack the SDK:
dotnet pack --configuration Release --output ./local-packages - In consuming project, add local package source:
<PropertyGroup> <RestoreAdditionalProjectSources>C:\dev\ktsu-dev\Sdk\local-packages</RestoreAdditionalProjectSources> </PropertyGroup>
- Reference the local version in consuming project's csproj or global.json
The SDK projects (Sdk, Sdk.ConsoleApp, Sdk.App) use a modular architecture with shared configuration files:
- Sdk.Common.SolutionDiscovery.props: Shared solution/project discovery logic
- Sdk.Common.MetadataFiles.props: Shared metadata file loading logic
- Sdk.Common.PackageProperties.props: Shared package configuration
- Sdk.Common.SdkContent.targets: Shared SDK content packaging logic
- Sdk.Common.PackageContent.targets: Shared package content inclusion logic
Each SDK project imports these modular files to avoid code duplication and ensure consistency.
The SDK uses careful property evaluation to ensure correct values:
- Early evaluation: Solution discovery, file path resolution
- Mid evaluation: Metadata file reading, namespace calculation
- Late evaluation: Derived properties (IsExecutable, IsPackable, etc.)
Properties are set conditionally to avoid overwriting user-specified values.
The SDK includes robust null/empty checks to prevent MSBuild failures:
- Solution file array access uses
.Split(';')[0]with validation - String operations check for null/empty before manipulation
- File existence validated before
File.ReadAllText()calls
The core SDK sets PackageType=MSBuildSdk in Directory.Build.props, which is required for proper MSBuild SDK packaging. The Directory.Build.targets file includes the SDK props/targets files in the package at the correct paths.