Summary
The repository-mandated type-check gate msbuild TaskMaster.sln /t:Build /p:Nullable=enable /p:TreatWarningsAsErrors=true cannot fail in practice. MSBuild's incremental up-to-date check does not invalidate on a command-line /p: change alone, so when the outputs are already current from a prior /t:Build, CoreCompile is skipped entirely and the gate returns exit 0 without ever re-running nullable analysis. The gate has been reporting success while never executing.
Environment
- OS/version: Windows 11
- Runtime: .NET Framework 4.8.1
- Command/flags used:
msbuild TaskMaster.sln /t:Build /p:Configuration=Debug /p:Platform="Any CPU" /p:Nullable=enable /p:TreatWarningsAsErrors=true
- Data source or fixture: none
Steps to Reproduce
- Run the analyzer build:
msbuild TaskMaster.sln /t:Build /p:Configuration=Debug /p:Platform="Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true. It succeeds.
- Immediately run the mandated type-check gate:
msbuild TaskMaster.sln /t:Build /p:Configuration=Debug /p:Platform="Any CPU" /p:Nullable=enable /p:TreatWarningsAsErrors=true. It returns exit 0.
- Now force recompilation of the same code with the same properties:
msbuild TaskMaster\TaskMaster.csproj /t:Rebuild /p:Configuration=Debug /p:Platform="Any CPU" /p:Nullable=enable /p:TreatWarningsAsErrors=true.
- Observe it returns exit 1 with a large number of
CS86xx nullable errors.
Expected Behavior
The type-check gate actually performs nullable flow analysis on every invocation and fails when nullable violations exist, so that "the nullable gate passed" is a meaningful statement.
Actual Behavior
Step 2 returns exit 0 because MSBuild considers the outputs up to date and skips CoreCompile. Step 3, which forces recompilation of identical source under identical properties, returns exit 1 with 195 errors, 64 of them CS86xx in TaskMaster.csproj. A separate measurement during the same session counted 220 CS86xx errors concentrated in AppOlObjects.cs, AppAutoFileObjects.cs, AppToDoObjects.cs, AppOlObjects.FolderTreeService.cs, AppStagingFilenames.cs, ApplicationGlobals.cs, and AppItemEngines.cs.
The same source, the same properties, and two different verdicts depending only on whether the output timestamps happened to be current.
Logs / Screenshots
# /t:Build with /p:Nullable=enable /p:TreatWarningsAsErrors=true
EXIT=0
# /t:Rebuild, same properties, same source
EXIT=1
195 errors (64 CS86xx)
Precedent: .github/workflows/ci.yml already carries a comment documenting exactly this MSBuild behavior and uses /t:Rebuild for its own TreatWarningsAsErrors step for that reason. The repository's own CI has the workaround; the policy command documented in CLAUDE.md and .claude/rules/csharp.md does not.
Impact / Severity
This is a quality gate that reports success without running. Every agent and developer who has run the documented toolchain has recorded a passing nullable check that did not execute, which means the recorded evidence across prior features overstates what was verified. It also masks a substantial pre-existing nullable debt in TaskMaster.csproj.
Severity is High rather than Blocker because the debt it masks is pre-existing rather than newly introduced, and CI's /t:Rebuild step does exercise TreatWarningsAsErrors (though CI does not pass /p:Nullable=enable, so CI does not surface the CS86xx set either).
Source
From: docs/features/potential/2026-08-08-nullable-gate-cannot-fail-incremental-build.md
Summary
The repository-mandated type-check gate
msbuild TaskMaster.sln /t:Build /p:Nullable=enable /p:TreatWarningsAsErrors=truecannot fail in practice. MSBuild's incremental up-to-date check does not invalidate on a command-line/p:change alone, so when the outputs are already current from a prior/t:Build,CoreCompileis skipped entirely and the gate returns exit 0 without ever re-running nullable analysis. The gate has been reporting success while never executing.Environment
msbuild TaskMaster.sln /t:Build /p:Configuration=Debug /p:Platform="Any CPU" /p:Nullable=enable /p:TreatWarningsAsErrors=trueSteps to Reproduce
msbuild TaskMaster.sln /t:Build /p:Configuration=Debug /p:Platform="Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true. It succeeds.msbuild TaskMaster.sln /t:Build /p:Configuration=Debug /p:Platform="Any CPU" /p:Nullable=enable /p:TreatWarningsAsErrors=true. It returns exit 0.msbuild TaskMaster\TaskMaster.csproj /t:Rebuild /p:Configuration=Debug /p:Platform="Any CPU" /p:Nullable=enable /p:TreatWarningsAsErrors=true.CS86xxnullable errors.Expected Behavior
The type-check gate actually performs nullable flow analysis on every invocation and fails when nullable violations exist, so that "the nullable gate passed" is a meaningful statement.
Actual Behavior
Step 2 returns exit 0 because MSBuild considers the outputs up to date and skips
CoreCompile. Step 3, which forces recompilation of identical source under identical properties, returns exit 1 with 195 errors, 64 of themCS86xxinTaskMaster.csproj. A separate measurement during the same session counted 220CS86xxerrors concentrated inAppOlObjects.cs,AppAutoFileObjects.cs,AppToDoObjects.cs,AppOlObjects.FolderTreeService.cs,AppStagingFilenames.cs,ApplicationGlobals.cs, andAppItemEngines.cs.The same source, the same properties, and two different verdicts depending only on whether the output timestamps happened to be current.
Logs / Screenshots
Precedent:
.github/workflows/ci.ymlalready carries a comment documenting exactly this MSBuild behavior and uses/t:Rebuildfor its ownTreatWarningsAsErrorsstep for that reason. The repository's own CI has the workaround; the policy command documented inCLAUDE.mdand.claude/rules/csharp.mddoes not.Impact / Severity
This is a quality gate that reports success without running. Every agent and developer who has run the documented toolchain has recorded a passing nullable check that did not execute, which means the recorded evidence across prior features overstates what was verified. It also masks a substantial pre-existing nullable debt in
TaskMaster.csproj.Severity is High rather than Blocker because the debt it masks is pre-existing rather than newly introduced, and CI's
/t:Rebuildstep does exerciseTreatWarningsAsErrors(though CI does not pass/p:Nullable=enable, so CI does not surface theCS86xxset either).Source
From: docs/features/potential/2026-08-08-nullable-gate-cannot-fail-incremental-build.md