Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

Part X: Roslyn Analyzers & Quality Gates

Overview

The type-safe chain from Part IX is only as strong as its enforcement. Without enforcement, [ForRequirement] is decoration. Without enforcement, a developer can skip writing tests and the build still passes.

This chapter covers the enforcement mechanisms: four families of Roslyn analyzers that run at compile time, and a post-test quality gate tool that validates pass rates, code coverage, test duration, flakiness, and fuzz testing results.

Three enforcement points, one build pipeline:

  1. dotnet build -- Roslyn analyzers catch structural gaps (REQ1xx, REQ2xx, REQ3xx)
  2. dotnet test -- tests execute against the implementation
  3. dotnet quality-gates check -- post-test analysis validates behavioral compliance (REQ4xx)
Diagram

Analyzer Family 1: REQ1xx -- Requirement to Specification Coverage

The REQ1xx analyzers scan Layer 1 (MyApp.Requirements) for all types inheriting RequirementMetadata with abstract AC methods, then scan Layer 2 (MyApp.Specifications) for [ForRequirement] attributes. They report gaps between what is required and what is specified.

Diagnostics

ID Severity Trigger Message
REQ100 Error Feature type has no [ForRequirement(typeof(Feature))] on any interface in Specifications UserRolesFeature has 3 acceptance criteria but no specification interface references it
REQ101 Error Feature AC method has no matching [ForRequirement(..., nameof(AC))] on any spec method UserRolesFeature.RoleChangeTakesEffectImmediately has no matching spec method
REQ102 Warning Story type has no specification interface JwtRefreshStory has 2 ACs but no specification
REQ103 Info Requirement fully specified -- all ACs have matching spec methods UserRolesFeature: all 3 ACs specified

How REQ1xx Works

The analyzer operates as a Roslyn DiagnosticAnalyzer registered in the Requirements.Generators project. During compilation, it:

  1. Finds all types where base type chain includes RequirementMetadata
  2. Extracts all abstract methods returning AcceptanceCriterionResult (these are ACs)
  3. Finds all interfaces decorated with [ForRequirement(typeof(T))]
  4. For each AC method, checks whether any interface method has [ForRequirement(typeof(T), nameof(T.ACMethod))]
  5. Emits diagnostics for gaps

Build Output Examples

// Scenario: Feature exists but no spec interface references it
error REQ100: UserRolesFeature has 3 acceptance criteria but no ISpec
              interface references it via [ForRequirement(typeof(UserRolesFeature))]
              Location: MyApp.Requirements/Features/UserRolesFeature.cs(12,0)
              Fix: Create IUserRolesSpec with [ForRequirement(typeof(UserRolesFeature))]

// Scenario: Feature has spec, but one AC is not covered by a spec method
error REQ101: UserRolesFeature.RoleChangeTakesEffectImmediately has no matching
              spec method with [ForRequirement(typeof(UserRolesFeature),
              nameof(UserRolesFeature.RoleChangeTakesEffectImmediately))]
              Location: MyApp.Requirements/Features/UserRolesFeature.cs(28,0)
              Fix: Add a method to IUserRolesSpec with the matching [ForRequirement]

// Scenario: Story has ACs but no spec (acceptable in some projects)
warning REQ102: JwtRefreshStory has 2 acceptance criteria but no specification
                interface references it. Consider creating IJwtRefreshSpec.
                Location: MyApp.Requirements/Stories/JwtRefreshStory.cs(5,0)

// Scenario: Feature fully specified
info REQ103: UserRolesFeature: all 3 acceptance criteria have matching spec methods
             (AdminCanAssignRoles, ViewerHasReadOnlyAccess,
              RoleChangeTakesEffectImmediately)

Analyzer Family 2: REQ2xx -- Specification to Implementation Coverage

The REQ2xx analyzers scan Layer 2 interfaces decorated with [ForRequirement] and Layer 3 (MyApp.Domain) for classes implementing those interfaces. They report unimplemented specifications.

Diagnostics

ID Severity Trigger Message
REQ200 Error Spec interface has no implementing class in Domain IUserRolesSpec is not implemented by any class
REQ201 Warning Implementing class is missing [ForRequirement] attribute (works but loses traceability) AuthorizationService implements IUserRolesSpec but lacks [ForRequirement]
REQ202 Warning Method-level [ForRequirement] missing on implementing method AuthorizationService.AssignRole implements IUserRolesSpec.AssignRole but lacks method-level [ForRequirement]
REQ203 Info Spec fully implemented with all traceability attributes IUserRolesSpec: fully implemented by AuthorizationService with complete [ForRequirement] coverage

How REQ2xx Works

  1. Finds all interfaces with [ForRequirement] attribute
  2. Searches the compilation for classes implementing those interfaces
  3. For each implementing class, checks for class-level [ForRequirement(typeof(Feature))]
  4. For each implementing method, checks for method-level [ForRequirement(typeof(Feature), nameof(AC))]
  5. Emits diagnostics for gaps

Build Output Examples

// Scenario: Spec interface exists but no class implements it
error REQ200: IUserRolesSpec is not implemented by any class in the compilation.
              IUserRolesSpec is linked to UserRolesFeature (3 ACs).
              Location: MyApp.Specifications/IUserRolesSpec.cs(8,0)
              Fix: Create a class that implements IUserRolesSpec

// Scenario: Class implements spec but forgot class-level attribute
warning REQ201: AuthorizationService implements IUserRolesSpec but is missing
                [ForRequirement(typeof(UserRolesFeature))] on the class declaration.
                The implementation works, but IDE navigation and traceability
                reports will not discover it.
                Location: MyApp.Domain/Auth/AuthorizationService.cs(10,0)
                Fix: Add [ForRequirement(typeof(UserRolesFeature))] to the class

// Scenario: Class has class-level attribute but method-level is missing
warning REQ202: AuthorizationService.VerifyImmediateRoleEffect implements
                IUserRolesSpec.VerifyImmediateRoleEffect but is missing
                [ForRequirement(typeof(UserRolesFeature),
                nameof(UserRolesFeature.RoleChangeTakesEffectImmediately))].
                Location: MyApp.Domain/Auth/AuthorizationService.cs(45,0)
                Fix: Add method-level [ForRequirement] for IDE navigation

// Scenario: Full coverage
info REQ203: IUserRolesSpec: fully implemented by AuthorizationService
             with complete [ForRequirement] coverage on class and all 3 methods.

Analyzer Family 3: REQ3xx -- Implementation to Test Coverage

The REQ3xx analyzers scan Layer 4 (MyApp.Tests) for [TestsFor] and [Verifies] attributes. They cross-reference with Layer 1 AC methods and report untested acceptance criteria.

Diagnostics

ID Severity Trigger Message
REQ300 Error Feature has zero [TestsFor] test classes UserRolesFeature has 3 ACs but no test class with [TestsFor(typeof(UserRolesFeature))]
REQ301 Warning AC method has no [Verifies(..., nameof(AC))] test UserRolesFeature.RoleChangeTakesEffectImmediately has no test with [Verifies]
REQ302 Error [Verifies] references an AC method that no longer exists (stale test) UserRolesTests.OldTest references nameof(UserRolesFeature.DeletedAC) which does not exist
REQ303 Info Feature fully tested -- all ACs have at least one [Verifies] test UserRolesFeature: all 3 ACs have [Verifies] tests

How REQ3xx Works

  1. Finds all test classes with [TestsFor(typeof(T))]
  2. Finds all test methods with [Verifies(typeof(T), nameof(T.AC))]
  3. Cross-references against all AC methods discovered in Layer 1
  4. Validates that nameof() references still resolve to actual methods on the feature type
  5. Emits diagnostics for gaps and stale references

Build Output Examples

// Scenario: Feature has no test class at all
error REQ300: UserRolesFeature has 3 acceptance criteria but no test class
              with [TestsFor(typeof(UserRolesFeature))].
              Location: MyApp.Requirements/Features/UserRolesFeature.cs(12,0)
              Fix: Create a test class with [TestsFor(typeof(UserRolesFeature))]

// Scenario: Test class exists but one AC has no [Verifies] test
warning REQ301: UserRolesFeature.RoleChangeTakesEffectImmediately has no test
                method with [Verifies(typeof(UserRolesFeature),
                nameof(UserRolesFeature.RoleChangeTakesEffectImmediately))].
                Location: MyApp.Requirements/Features/UserRolesFeature.cs(28,0)
                Fix: Add a test method with the matching [Verifies] attribute

// Scenario: Test references a deleted AC (stale reference)
error REQ302: UserRolesTests.OldTest_DeletedACStillWorks has
              [Verifies(typeof(UserRolesFeature), "DeletedAC")]
              but UserRolesFeature has no method named "DeletedAC".
              This test verifies a criterion that no longer exists.
              Location: MyApp.Tests/Auth/UserRolesTests.cs(88,0)
              Fix: Remove or update the stale [Verifies] attribute

// Scenario: Full coverage
info REQ303: UserRolesFeature: all 3 acceptance criteria have [Verifies] tests
             AdminCanAssignRoles: 2 tests
             ViewerHasReadOnlyAccess: 2 tests
             RoleChangeTakesEffectImmediately: 1 test

Analyzer Family 4: REQ4xx -- Quality Gates

The REQ4xx family is different from REQ1xx-REQ3xx. The first three families are Roslyn analyzers that run at compile time inside dotnet build. REQ4xx is enforced by the dotnet quality-gates tool -- a pre-existing command-line tool consumed by the CMF, not built by it.

dotnet quality-gates runs after dotnet test completes. It reads test results (.trx files), code coverage reports (Cobertura XML), and the generated TraceabilityMatrix to evaluate behavioral compliance.

Quality Gate Definitions

Gate ID What It Checks Fails When
Pass rate REQ400 All [Verifies] tests passed in .trx results Any test marked [Verifies] has outcome "Failed"
AC coverage REQ401 Every AC in TraceabilityMatrix has a passing [Verifies] test AC exists in requirements but has no passing test
Code coverage REQ402 [ForRequirement] methods have >= N% line coverage Implementation method below configured threshold
Duration REQ403 Individual test execution time within budget Any [Verifies] test exceeds max duration (ms)
Flakiness REQ404 Test result consistency across retries A [Verifies] test has inconsistent pass/fail across runs
Fuzz testing REQ405 Auto-generated inputs for AC method signatures Implementation crashes on auto-generated inputs

dotnet quality-gates check Invocation

dotnet quality-gates check \
    --trx TestResults/*.trx \
    --coverage TestResults/**/coverage.cobertura.xml \
    --traceability obj/TraceabilityMatrix.g.cs \
    --min-pass-rate 100 \
    --min-coverage 80 \
    --max-duration 5000 \
    --fuzz-inputs 500
Flag Purpose
--trx Path to .trx test result files from dotnet test --logger:trx
--coverage Path to Cobertura XML from dotnet test --collect:"XPlat Code Coverage"
--traceability Path to generated TraceabilityMatrix (knows which tests map to which ACs)
--min-pass-rate Minimum percentage of [Verifies] tests that must pass (0-100)
--min-coverage Minimum line coverage percentage for [ForRequirement] methods
--max-duration Maximum allowed execution time per test in milliseconds
--fuzz-inputs Number of auto-generated inputs per AC method signature

Gate: Pass Rate (REQ400)

The pass rate gate reads .trx files and cross-references test method names with [Verifies] attributes from the TraceabilityMatrix. Every test that claims to verify an AC must have passed.

REQ400 PASS: UserRolesFeature.AdminCanAssignRoles
             2/2 [Verifies] tests passed
REQ400 PASS: UserRolesFeature.ViewerHasReadOnlyAccess
             2/2 [Verifies] tests passed
REQ400 FAIL: JwtRefreshStory.TokensExpireAfterOneHour
             1/1 [Verifies] test FAILED
             -> JwtRefreshTests.Token_expires_after_one_hour: Assert.That failed

Gate: AC Coverage (REQ401)

The AC coverage gate verifies that every acceptance criterion in the TraceabilityMatrix has at least one passing [Verifies] test. A test that exists but fails does not count.

REQ401 PASS: UserRolesFeature -- 3/3 ACs have passing tests
REQ401 FAIL: JwtRefreshStory -- 0/2 ACs have passing tests
             Missing: TokensExpireAfterOneHour, RefreshExtendsBySevenDays

Gate: Code Coverage (REQ402)

The code coverage gate reads Cobertura XML and maps coverage data to methods annotated with [ForRequirement]. Each implementation method must meet the configured threshold.

REQ402 PASS: AuthorizationService.AssignRole
             94% line coverage (threshold: 80%)
REQ402 PASS: AuthorizationService.EnforceReadOnlyAccess
             88% line coverage (threshold: 80%)
REQ402 FAIL: AuthorizationService.VerifyImmediateRoleEffect
             62% line coverage (threshold: 80%)
             -> Lines 45-52 not covered (cache invalidation error path)

Gate: Duration (REQ403)

The duration gate reads individual test execution times from .trx files. Tests that verify acceptance criteria have a performance budget.

REQ403 PASS: Admin_can_assign_role_to_another_user          12ms (max: 5000ms)
REQ403 PASS: Non_admin_cannot_assign_roles                   8ms (max: 5000ms)
REQ403 PASS: Viewer_cannot_write                             6ms (max: 5000ms)
REQ403 PASS: Viewer_can_read                                 5ms (max: 5000ms)
REQ403 FAIL: Role_change_invalidates_cache_immediately    6200ms (max: 5000ms)
             -> Test exceeds duration budget by 1200ms

Gate: Flakiness (REQ404)

The flakiness gate requires multiple test runs (controlled by --retries flag or CI configuration). If a [Verifies] test passes on one run and fails on another, it is flagged as flaky.

REQ404 PASS: Admin_can_assign_role_to_another_user       3/3 runs passed
REQ404 PASS: Non_admin_cannot_assign_roles               3/3 runs passed
REQ404 FAIL: Role_change_invalidates_cache_immediately   2/3 runs passed (FLAKY)
             -> Run 1: PASS (14ms), Run 2: FAIL (timeout), Run 3: PASS (12ms)
             -> Flaky tests cannot verify acceptance criteria reliably

Gate: Fuzz Testing (REQ405)

The fuzz testing gate deserves detailed treatment. It exploits a unique property of the Requirements DSL: every acceptance criterion is an abstract method with a typed signature. This means the tool knows exactly what inputs to generate.

How Fuzz Testing Works

For each AC method signature, the fuzz generator produces boundary and adversarial inputs:

// AC signature:
public abstract AcceptanceCriterionResult AdminCanAssignRoles(
    UserId actingUser, UserId targetUser, RoleId role);

// The tool knows:
//   - actingUser is UserId(Guid, string, string, Email)
//   - targetUser is UserId(Guid, string, string, Email)
//   - role is RoleId(string)
//
// It generates 500 inputs including:
//   - Guid.Empty for all Guid fields
//   - null strings (where nullable)
//   - empty strings
//   - extremely long strings (10,000+ characters)
//   - Unicode edge cases (RTL, zero-width, emoji, surrogate pairs)
//   - Duplicate actingUser == targetUser
//   - Same UserId assigned to both parameters
//   - RoleId with whitespace, special characters

The tool calls the implementation (not the abstract method) with each generated input. The contract is:

  • AcceptanceCriterionResult.Satisfied() is acceptable
  • AcceptanceCriterionResult.Failed(reason) is acceptable (the method correctly rejected bad input)
  • Result.Failure(reason) from the spec method is acceptable
  • An unhandled exception is NOT acceptable -- this is a crash, not a graceful rejection

This is property-based testing derived from the requirement signature itself. The developer did not write these test cases -- the tool generated them from the AC parameter types.

Fuzz Output Example

REQ405 PASS: UserRolesFeature.AdminCanAssignRoles
             500 inputs generated, 0 crashes
             488 -> Result.Success (valid inputs)
             12  -> Result.Failure (correctly rejected: null role, empty Guid, etc.)

REQ405 PASS: UserRolesFeature.ViewerHasReadOnlyAccess
             500 inputs generated, 0 crashes
             492 -> Result.Success
             8   -> Result.Failure

REQ405 FAIL: UserRolesFeature.RoleChangeTakesEffectImmediately
             500 inputs generated, 1 CRASH
             Input: UserId(Guid.Empty, "", "", Email("")), RoleId(""),  RoleId("")
             Exception: NullReferenceException at
               AuthorizationService.VerifyImmediateRoleEffect:L47
               _cache.Invalidate(user.Id) -- user.Id.Value is Guid.Empty
             -> Implementation must handle empty/default inputs gracefully

Why Fuzz Testing Works Here

In a traditional codebase, fuzz testing requires manual setup: identify target functions, define input grammars, set up harnesses. The Requirements DSL eliminates this setup cost because:

  1. Target functions are known: Every method with [ForRequirement] is a target
  2. Input types are known: The AC method signature defines the input grammar
  3. Success criteria are known: Result.Failure is acceptable; crashes are not
  4. Coverage is measurable: Fuzz results map directly to specific ACs

Full Quality Gates Output

$ dotnet quality-gates check --trx results.trx --coverage coverage.xml \
    --min-pass-rate 100 --min-coverage 80 --max-duration 5000 --fuzz-inputs 500

Quality Gates Report
====================

FEATURE: UserRolesFeature (3 ACs, 5 tests)
  [REQ400] Pass Rate
    AdminCanAssignRoles:                 2/2 passed    PASS
    ViewerHasReadOnlyAccess:             2/2 passed    PASS
    RoleChangeTakesEffectImmediately:    1/1 passed    PASS

  [REQ401] AC Coverage
    AdminCanAssignRoles:                 covered       PASS
    ViewerHasReadOnlyAccess:             covered       PASS
    RoleChangeTakesEffectImmediately:    covered       PASS

  [REQ402] Code Coverage
    AuthorizationService.AssignRole:              94%  PASS (>= 80%)
    AuthorizationService.EnforceReadOnlyAccess:   88%  PASS (>= 80%)
    AuthorizationService.VerifyImmediateRoleEffect: 62% FAIL (< 80%)
      -> Lines 45-52 not covered

  [REQ403] Duration
    Admin_can_assign_role:                12ms          PASS (< 5000ms)
    Non_admin_cannot_assign_roles:         8ms          PASS
    Viewer_cannot_write:                   6ms          PASS
    Viewer_can_read:                       5ms          PASS
    Role_change_invalidates_cache:        14ms          PASS

  [REQ405] Fuzz Testing
    AdminCanAssignRoles:        500 inputs, 0 crashes   PASS
    ViewerHasReadOnlyAccess:    500 inputs, 0 crashes   PASS
    RoleChangeTakesEffectImmediately: 500 inputs, 1 CRASH  FAIL
      -> NullReferenceException with empty RoleId

STORY: JwtRefreshStory (2 ACs, 0 tests)
  [REQ400] Pass Rate:    N/A (no tests)
  [REQ401] AC Coverage:  0/2 ACs covered               FAIL
    Missing: TokensExpireAfterOneHour, RefreshExtendsBySevenDays

Summary
-------
UserRolesFeature:  4/5 gates passed (code coverage FAIL, fuzz FAIL)
JwtRefreshStory:   0/1 gates passed (AC coverage FAIL)
Overall: FAIL (exit code 1)

Severity Configuration

.editorconfig (for REQ1xx-REQ3xx)

The compile-time analyzers (REQ1xx, REQ2xx, REQ3xx) are configured via .editorconfig:

# .editorconfig
[*.cs]

# ── REQ1xx: Requirement → Specification ──────────────────
# Missing spec for feature: always an error
dotnet_diagnostic.REQ100.severity = error
# Missing spec method for AC: always an error
dotnet_diagnostic.REQ101.severity = error
# Missing spec for story: warning (not all stories need specs)
dotnet_diagnostic.REQ102.severity = warning
# Fully specified: informational
dotnet_diagnostic.REQ103.severity = suggestion

# ── REQ2xx: Specification → Implementation ───────────────
# Spec with no implementation: error
dotnet_diagnostic.REQ200.severity = error
# Missing class-level [ForRequirement]: warning (still works)
dotnet_diagnostic.REQ201.severity = warning
# Missing method-level [ForRequirement]: suggestion
dotnet_diagnostic.REQ202.severity = suggestion
# Fully implemented: informational
dotnet_diagnostic.REQ203.severity = suggestion

# ── REQ3xx: Implementation → Test ────────────────────────
# Feature with no tests: warning in dev, error in CI
dotnet_diagnostic.REQ300.severity = warning
# AC with no [Verifies] test: warning
dotnet_diagnostic.REQ301.severity = warning
# Stale [Verifies] reference: always an error (dead code)
dotnet_diagnostic.REQ302.severity = error
# Fully tested: informational
dotnet_diagnostic.REQ303.severity = suggestion

Directory.Build.props (for CI strict mode)

In CI, elevate warnings to errors so the build fails on any gap:

<!-- Directory.Build.props -->
<Project>
  <PropertyGroup Condition="'$(CI)' == 'true'">
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <WarningsAsErrors>
      REQ100;REQ101;REQ200;REQ300;REQ301;REQ302
    </WarningsAsErrors>
  </PropertyGroup>

  <!-- Quality gates thresholds -->
  <PropertyGroup>
    <RequirementMinPassRate>100</RequirementMinPassRate>
    <RequirementMinCoverage>80</RequirementMinCoverage>
    <RequirementMaxTestDuration>5000</RequirementMaxTestDuration>
    <RequirementFuzzInputs>500</RequirementFuzzInputs>
  </PropertyGroup>
</Project>

MSBuild Integration for Quality Gates

The quality gates tool is invoked as an MSBuild target that runs after VSTest:

<!-- MyApp.Tests.csproj -->
<Target Name="RequirementQualityGates" AfterTargets="VSTest">
  <Exec Command="dotnet quality-gates check
    --trx $(TestResultsDir)/*.trx
    --coverage $(CoverageResultsDir)/coverage.cobertura.xml
    --traceability $(IntermediateOutputPath)/TraceabilityMatrix.g.cs
    --min-pass-rate $(RequirementMinPassRate)
    --min-coverage $(RequirementMinCoverage)
    --max-duration $(RequirementMaxTestDuration)
    --fuzz-inputs $(RequirementFuzzInputs)"
    ConsoleToMsBuild="true" />
</Target>

The Full CI Pipeline

GitHub Actions Workflow

# .github/workflows/build.yml
name: Build & Quality Gates

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  DOTNET_VERSION: '10.0.x'
  CI: true  # Enables strict analyzer mode via Directory.Build.props

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}

      # Step 1: Build with analyzers (REQ1xx, REQ2xx, REQ3xx)
      # CI=true makes warnings into errors via Directory.Build.props
      - name: Build (Roslyn Analyzers)
        run: dotnet build --configuration Release --warnaserror

      # Step 2: Run tests with coverage collection
      - name: Test
        run: |
          dotnet test --configuration Release \
            --no-build \
            --collect:"XPlat Code Coverage" \
            --logger:trx \
            --results-directory TestResults

      # Step 3: Quality gates (REQ4xx)
      - name: Quality Gates
        run: |
          dotnet quality-gates check \
            --trx TestResults/*.trx \
            --coverage TestResults/**/coverage.cobertura.xml \
            --min-pass-rate 100 \
            --min-coverage 80 \
            --max-duration 5000 \
            --fuzz-inputs 500

What Each Step Catches

Diagram

Analyzer Implementation Architecture

The analyzers are implemented as a standard Roslyn DiagnosticAnalyzer in the Cmf.Requirements.Generators project. They are referenced by consumer projects via <ProjectReference ... OutputItemType="Analyzer">.

// Simplified structure of the REQ1xx analyzer
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class RequirementSpecificationAnalyzer : DiagnosticAnalyzer
{
    // Diagnostic descriptors
    private static readonly DiagnosticDescriptor REQ100 = new(
        id: "REQ100",
        title: "Feature has no specification",
        messageFormat: "{0} has {1} acceptance criteria but no specification " +
                       "interface references it via [ForRequirement(typeof({0}))]",
        category: "Requirements",
        defaultSeverity: DiagnosticSeverity.Error,
        isEnabledByDefault: true,
        helpLinkUri: "https://cmf.dev/analyzers/REQ100");

    private static readonly DiagnosticDescriptor REQ101 = new(
        id: "REQ101",
        title: "Acceptance criterion has no spec method",
        messageFormat: "{0}.{1} has no matching spec method with " +
                       "[ForRequirement(typeof({0}), nameof({0}.{1}))]",
        category: "Requirements",
        defaultSeverity: DiagnosticSeverity.Error,
        isEnabledByDefault: true,
        helpLinkUri: "https://cmf.dev/analyzers/REQ101");

    // ... REQ102, REQ103

    public override ImmutableArray<DiagnosticDescriptor>
        SupportedDiagnostics => ImmutableArray.Create(
            REQ100, REQ101, REQ102, REQ103);

    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(
            GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        context.RegisterCompilationAction(AnalyzeCompilation);
    }

    private void AnalyzeCompilation(CompilationAnalysisContext ctx)
    {
        var compilation = ctx.Compilation;

        // 1. Find all RequirementMetadata subtypes with abstract AC methods
        var requirements = FindRequirementTypes(compilation);

        // 2. Find all interfaces with [ForRequirement]
        var specifications = FindSpecInterfaces(compilation);

        // 3. Cross-reference and emit diagnostics
        foreach (var req in requirements)
        {
            var matchingSpec = specifications
                .FirstOrDefault(s => s.ForRequirementType == req.Type);

            if (matchingSpec is null)
            {
                ctx.ReportDiagnostic(Diagnostic.Create(
                    REQ100,
                    req.Location,
                    req.TypeName,
                    req.AcceptanceCriteria.Length));
                continue;
            }

            foreach (var ac in req.AcceptanceCriteria)
            {
                var hasSpecMethod = matchingSpec.Methods
                    .Any(m => m.ForRequirementAC == ac.Name);

                if (!hasSpecMethod)
                {
                    ctx.ReportDiagnostic(Diagnostic.Create(
                        REQ101,
                        ac.Location,
                        req.TypeName,
                        ac.Name));
                }
            }
        }
    }
}

The REQ2xx and REQ3xx analyzers follow the same pattern, scanning for different attribute combinations across different project layers.


Runtime Compliance Output

In addition to compile-time and CI enforcement, the production host can log compliance status at startup (as shown in Part IX). This provides runtime visibility into the state of the requirement chain:

info: RequirementComplianceCheck[0]
      ── Requirement Compliance Report ──
info: RequirementComplianceCheck[0]
      PlatformScalabilityEpic: 0 direct ACs (epic)
info: RequirementComplianceCheck[0]
      UserRolesFeature: 3/3 ACs covered
        AdminCanAssignRoles: 2 tests, 94% coverage
        ViewerHasReadOnlyAccess: 2 tests, 88% coverage
        RoleChangeTakesEffectImmediately: 1 test, 62% coverage [BELOW THRESHOLD]
warn: RequirementComplianceCheck[0]
      JwtRefreshStory: 0/2 ACs covered
        Missing: TokensExpireAfterOneHour, RefreshExtendsBySevenDays
info: RequirementComplianceCheck[0]
      ── Summary: 3/5 ACs covered across 2 requirements ──

This is informational -- the application still starts. The startup check uses the source-generated TraceabilityMatrix and RequirementRegistry, so it adds zero runtime overhead beyond a few log statements.


Summary

Three Enforcement Points

Point Tool Diagnostics When
Compile Roslyn Analyzers REQ1xx, REQ2xx, REQ3xx dotnet build
Test Test Runner Test execution dotnet test
Post-Test Quality Gates REQ4xx (pass rate, coverage, fuzz) dotnet quality-gates check

Diagnostic Reference

ID Family Severity What It Catches
REQ100 Req-to-Spec Error Feature has no specification interface
REQ101 Req-to-Spec Error AC has no matching spec method
REQ102 Req-to-Spec Warning Story has no specification
REQ103 Req-to-Spec Info Fully specified
REQ200 Spec-to-Impl Error Spec interface has no implementing class
REQ201 Spec-to-Impl Warning Impl missing class-level [ForRequirement]
REQ202 Spec-to-Impl Warning Impl missing method-level [ForRequirement]
REQ203 Spec-to-Impl Info Fully implemented
REQ300 Impl-to-Test Error Feature has no [TestsFor] test class
REQ301 Impl-to-Test Warning AC has no [Verifies] test
REQ302 Impl-to-Test Error Stale [Verifies] reference
REQ303 Impl-to-Test Info Fully tested
REQ400 Quality Gate Gate [Verifies] test failed
REQ401 Quality Gate Gate AC has no passing test
REQ402 Quality Gate Gate [ForRequirement] method below coverage threshold
REQ403 Quality Gate Gate Test exceeds duration budget
REQ404 Quality Gate Gate Flaky test detected
REQ405 Quality Gate Gate Implementation crashes on fuzz input

The analyzers turn the type-safe chain from Part IX into an enforced chain. Missing a specification? Compile error. Missing an implementation? Compile error. Missing a test? Compile warning (error in CI). Test fails? Quality gate fails. Implementation crashes on edge-case inputs? Fuzz gate fails.

The compiler is the project manager. The quality gates tool is the QA lead. Between them, the chain from requirement to verified production code is unbreakable.