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

Requirements as Code: Making the Compiler Enforce Your Development Lifecycle

The Problem with Requirement Silos

In most software projects, requirements live in Jira. Code lives in a repository. Tests live somewhere else again. When a developer asks "which code implements Feature-456?", the answer requires jumping between systems, reading comments, grepping for magic strings, and hoping someone documented things correctly.

In a wide solution -- or worse, a multi-solution mega mono-repo -- this problem is exponentially worse. A single Feature like "Order Processing" spans OrderService.dll (API server), PaymentGateway.dll (separate service on another machine), NotificationWorker.dll (background worker), and OrderTests.dll (verification). A developer touching one of these has no way to know which Feature it belongs to, which acceptance criteria are validated, which tests cover them, or what the Product Owner actually specified. The requirement silo isn't just an inconvenience -- it's a structural gap that makes large-scale development fragile.

Common "solutions" make it worse:

// String-based approach: looks safe, is not
[Feature(Id = "FEATURE-456", AcceptanceCriteria = new[] { "Admin can assign roles" })]
public class UserRoleFeature { }  // Empty marker class — dead code

[Implements("FEATURE-456")]       // String. Typo? Compiles fine.
public void AssignRole() { }

[FeatureTest("FEATURE-456", 0)]   // Index 0? What if someone reorders the array?
public void AdminCanAssign() { }

The problems are structural:

  1. Requirements are metadata on marker classes -- not real types with behavior
  2. Links are string-based -- "FEATURE-456" is not checked by the compiler. Typos, renames, and deletions are silent
  3. Acceptance criteria are string arrays -- "Admin can assign roles" says nothing the compiler can verify
  4. Tests reference requirements by index -- AC[0] breaks when someone reorders the acceptance criteria list
  5. No compile-time guarantee that an implementation actually satisfies a specification

What if requirements were types? What if acceptance criteria were abstract methods? What if the compiler refused to build until every AC was implemented and tested?

This approach applies the same principle as DDD -- model the problem domain first, let the compiler enforce the model -- and the same meta-metamodeling architecture: requirements become M2 concepts, and specific features become M1 instances that the source generator validates at build time.


The Chain: Six Projects, One Compiler

The design is six C# projects linked by <ProjectReference>. Each boundary is enforced by the C# type system. Each project compiles to a DLL artifact with a clear role:

MyApp.sln
├── src/
│   ├── MyApp.Requirements/          ← Features as types, ACs as abstract methods
│   ├── MyApp.SharedKernel/          ← Domain types (User, Role, Permission)
│   ├── MyApp.Specifications/        ← Interfaces the domain must implement
│   ├── MyApp.Domain/                ← Domain implementation (: ISpec)
│   └── MyApp.Api/                   ← Production host (DI, controllers, middleware)
├── test/
│   └── MyApp.Tests/                 ← Type-linked verification
└── tools/
    └── MyApp.Requirements.Analyzers/ ← Source generator (registry, matrix, diagnostics)

Project reference graph:

Requirements ──┐
               ├──> Specifications ──> Domain ← Api
SharedKernel ──┘         │                │
                         └────────────────┴──> Tests
Project DLL Artifact Role Depends On
Requirements MyApp.Requirements.dll Features as types, AC methods, generated attributes Nothing
SharedKernel MyApp.SharedKernel.dll Domain value types (User, Role, Permission, Result) Nothing
Specifications MyApp.Specifications.dll Interfaces per feature, validator bridges Requirements + SharedKernel
Domain MyApp.Domain.dll Implements spec interfaces -- compiler enforces Specifications
Api MyApp.Api.dll Production host: DI, controllers, middleware Domain
Tests MyApp.Tests.dll Verifies implementation via type refs Domain + Specifications

Every link between layers uses typeof() or nameof() -- Ctrl+Click navigable in the IDE, rename-safe, compiler-checked.


Layer 1: MyApp.Requirements -- Requirements ARE Types

The Fundamental Shift

A Feature is not an attribute on a marker class. A Feature is a class. Each acceptance criterion is an abstract method -- its signature defines the inputs, its return type enforces verifiability.

Base Types

namespace MyApp.Requirements;

public abstract record RequirementMetadata
{
    public abstract string Title { get; }
    public abstract RequirementPriority Priority { get; }
    public abstract string Owner { get; }
}

public enum RequirementPriority { Critical, High, Medium, Low, Backlog }
public enum BugSeverity { Critical, Major, Minor, Cosmetic }

Hierarchy Through Generics

The hierarchy (Epic > Feature > Story > Task, Bug) is enforced by generic constraints:

public abstract record Epic : RequirementMetadata;

public abstract record Feature<TParent> : RequirementMetadata
    where TParent : Epic;

public abstract record Feature : RequirementMetadata;  // root-level, no parent

public abstract record Story<TParent> : RequirementMetadata
    where TParent : RequirementMetadata;

public abstract record Task<TParent> : RequirementMetadata
    where TParent : RequirementMetadata
{
    public abstract int EstimatedHours { get; }
}

public abstract record Bug : RequirementMetadata
{
    public abstract BugSeverity Severity { get; }
}

You cannot write Feature<JwtRefreshStory> -- the constraint where TParent : Epic prevents it. The type system enforces the hierarchy.

The AC Result Type

Every acceptance criterion method returns this:

public readonly record struct AcceptanceCriterionResult
{
    public bool IsSatisfied { get; }
    public string? FailureReason { get; }

    private AcceptanceCriterionResult(bool satisfied, string? reason)
    {
        IsSatisfied = satisfied;
        FailureReason = reason;
    }

    public static AcceptanceCriterionResult Satisfied() => new(true, null);
    public static AcceptanceCriterionResult Failed(string reason) => new(false, reason);
    public static implicit operator bool(AcceptanceCriterionResult r) => r.IsSatisfied;
}

Lightweight Domain Concepts

AC method signatures use lightweight value types that represent domain concepts without importing domain implementation details:

public readonly record struct Email(string Value);
public readonly record struct UserId(Guid Value, string FirstName, string LastName, Email Email);
public readonly record struct RoleId(string Value);
public readonly record struct ResourceId(string Value);
public readonly record struct TokenId(Guid Value);

Concrete Requirements -- The "What" Without the "How"

namespace MyApp.Requirements.Epics;

public abstract record PlatformScalabilityEpic : Epic
{
    public override string Title => "Multi-tenant platform enablement";
    public override RequirementPriority Priority => RequirementPriority.High;
    public override string Owner => "platform-team";
}
namespace MyApp.Requirements.Features;

/// <summary>
/// Feature: User roles and permissions.
/// Each abstract method IS an acceptance criterion.
/// The method signature defines what must be true.
/// The parameters define the domain inputs.
/// The return type enforces that every AC produces a verifiable result.
/// </summary>
public abstract record UserRolesFeature : Feature<PlatformScalabilityEpic>
{
    public override string Title => "User roles and permissions";
    public override RequirementPriority Priority => RequirementPriority.High;
    public override string Owner => "auth-team";

    // AC1: Admin users can assign roles to other users
    public abstract AcceptanceCriterionResult AdminCanAssignRoles(
        UserId actingUser, UserId targetUser, RoleId role);

    // AC2: Viewer users have read-only access
    public abstract AcceptanceCriterionResult ViewerHasReadOnlyAccess(
        UserId viewer, ResourceId resource);

    // AC3: Role changes take effect immediately (no restart required)
    public abstract AcceptanceCriterionResult RoleChangeTakesEffectImmediately(
        UserId user, RoleId previousRole, RoleId newRole);
}
namespace MyApp.Requirements.Stories;

public abstract record JwtRefreshStory : Story<UserRolesFeature>
{
    public override string Title => "Implement JWT refresh token flow";
    public override RequirementPriority Priority => RequirementPriority.Medium;
    public override string Owner => "alice@company.com";

    public abstract AcceptanceCriterionResult TokensExpireAfterOneHour(
        TokenId token, DateTimeOffset issuedAt, DateTimeOffset checkedAt);

    public abstract AcceptanceCriterionResult RefreshExtendsBySevenDays(
        TokenId token, DateTimeOffset refreshedAt);
}
namespace MyApp.Requirements.Bugs;

public abstract record OrderNegativeTotalBug : Bug
{
    public override string Title => "Orders crash when total is negative";
    public override RequirementPriority Priority => RequirementPriority.Critical;
    public override string Owner => "qa-team";
    public override BugSeverity Severity => BugSeverity.Critical;

    public abstract AcceptanceCriterionResult NegativeTotalIsRejected(
        decimal totalAmount);
}

What makes this work:

  1. UserRolesFeature is a type. typeof(UserRolesFeature) works everywhere -- in attributes, in generics, in reflection, in source generators.
  2. Each AC is an abstract method with a typed signature. The parameters document exactly what the AC needs. The return type enforces verifiability.
  3. The generic constraint Feature<PlatformScalabilityEpic> creates a compile-time hierarchy. You cannot accidentally parent a Feature under a Story.
  4. Metadata (Title, Priority, Owner) is overridden properties, not string attributes on marker classes.
  5. No string IDs anywhere. The type IS the identifier.

Layer 1 Generates Navigable Artifacts

Layer 1 includes a Roslyn source generator that scans all types inheriting RequirementMetadata and produces attributes used by every subsequent layer as clickable glue:

// Generated by Requirements source generator

/// <summary>
/// Links any type, interface, or method to a requirement.
/// Ctrl+Click on typeof(Feature) in IDE jumps to the requirement definition.
/// "Find All References" on Feature shows specs, implementations, and tests.
/// </summary>
[AttributeUsage(
    AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method,
    AllowMultiple = true)]
public sealed class ForRequirementAttribute : Attribute
{
    public Type RequirementType { get; }
    public string? AcceptanceCriterion { get; }

    public ForRequirementAttribute(Type requirementType, string? acceptanceCriterion = null)
    {
        RequirementType = requirementType;
        AcceptanceCriterion = acceptanceCriterion;
    }
}

/// <summary>
/// Links a test method to a specific acceptance criterion.
/// The AC is identified by nameof(Feature.ACMethod) -- compiler-checked.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public sealed class VerifiesAttribute : Attribute
{
    public Type RequirementType { get; }
    public string AcceptanceCriterionName { get; }

    public VerifiesAttribute(Type requirementType, string acceptanceCriterionName)
    {
        RequirementType = requirementType;
        AcceptanceCriterionName = acceptanceCriterionName;
    }
}

/// <summary>
/// Links a test class to a requirement type.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class TestsForAttribute : Attribute
{
    public Type RequirementType { get; }
    public TestsForAttribute(Type requirementType) => RequirementType = requirementType;
}

The generator also produces a requirement registry:

// Generated: RequirementRegistry.g.cs
public static class RequirementRegistry
{
    public static IReadOnlyDictionary<Type, RequirementInfo> All { get; } =
        new Dictionary<Type, RequirementInfo>
        {
            [typeof(PlatformScalabilityEpic)] = new(
                Type: typeof(PlatformScalabilityEpic),
                Kind: RequirementKind.Epic,
                Title: "Multi-tenant platform enablement",
                Parent: null,
                AcceptanceCriteria: Array.Empty<string>()),

            [typeof(UserRolesFeature)] = new(
                Type: typeof(UserRolesFeature),
                Kind: RequirementKind.Feature,
                Title: "User roles and permissions",
                Parent: typeof(PlatformScalabilityEpic),
                AcceptanceCriteria: new[]
                {
                    nameof(UserRolesFeature.AdminCanAssignRoles),
                    nameof(UserRolesFeature.ViewerHasReadOnlyAccess),
                    nameof(UserRolesFeature.RoleChangeTakesEffectImmediately)
                }),
        };
}

public record RequirementInfo(
    Type Type, RequirementKind Kind, string Title,
    Type? Parent, string[] AcceptanceCriteria);

public enum RequirementKind { Epic, Feature, Story, Task, Bug }

MyApp.SharedKernel -- Domain Types Shared Across Layers

Domain types live in their own project -- not in Specifications, not in Domain. Both reference it.

namespace MyApp.SharedKernel;

// Value types from Requirements are reused
using MyApp.Requirements;

public record User(UserId Id, string Name, IReadOnlySet<Role> Roles);
public record Role(RoleId Id, string Name, IReadOnlySet<Permission> Permissions);
public record Resource(ResourceId Id, string Type);
public record Permission(string Name);
public enum Operation { Read, Write, Delete, Admin }

/// <summary>
/// Strongly-typed result for domain operations.
/// Every specification method returns this -- not bool, not void.
/// </summary>
public record Result
{
    public bool IsSuccess { get; }
    public string? Reason { get; }

    private Result(bool success, string? reason) { IsSuccess = success; Reason = reason; }

    public static Result Success() => new(true, null);
    public static Result Failure(string reason) => new(false, reason);
    public static Result Aggregate(params Result[] results) =>
        new(results.All(r => r.IsSuccess), null);
}

SharedKernel references Requirements (for UserId, RoleId value types). Specifications and Domain both reference SharedKernel. This keeps interfaces and implementations using the same User, Role, Result types without duplication.


Layer 2: MyApp.Specifications -- Contracts the Domain Must Satisfy

This project references MyApp.Requirements and MyApp.SharedKernel. It defines only interfaces -- no entity definitions, no implementations. Every spec interface and method is decorated with [ForRequirement] for IDE navigability.

Specification Interface

namespace MyApp.Specifications;

using MyApp.Requirements.Features;
using MyApp.SharedKernel;

/// <summary>
/// Specification for UserRolesFeature.
/// Any domain service claiming to implement user roles MUST implement this interface.
/// The compiler enforces it -- you cannot compile without satisfying every AC.
/// </summary>
[ForRequirement(typeof(UserRolesFeature))]
public interface IUserRolesSpec
{
    [ForRequirement(typeof(UserRolesFeature), nameof(UserRolesFeature.AdminCanAssignRoles))]
    Result AssignRole(User actingUser, User targetUser, Role role);

    [ForRequirement(typeof(UserRolesFeature), nameof(UserRolesFeature.ViewerHasReadOnlyAccess))]
    Result EnforceReadOnlyAccess(User viewer, Resource resource, Operation operation);

    [ForRequirement(typeof(UserRolesFeature), nameof(UserRolesFeature.RoleChangeTakesEffectImmediately))]
    Result VerifyImmediateRoleEffect(User user, Role previousRole, Role newRole);
}

In the IDE: right-click typeof(UserRolesFeature) in the attribute > "Go To Definition" > jumps to the abstract feature with its AC methods. Right-click nameof(UserRolesFeature.AdminCanAssignRoles) > jumps directly to that AC method signature.

Validator Bridge -- AC Evaluation at Runtime

The bridge connects abstract UserRolesFeature AC methods to the spec interface. In normal production flow, code calls IUserRolesSpec directly. For compliance-sensitive operations (audit, regulatory), the validator bridge evaluates ACs explicitly:

[ForRequirement(typeof(UserRolesFeature))]
public record UserRolesValidator : UserRolesFeature
{
    private readonly IUserRolesSpec _spec;

    public UserRolesValidator(IUserRolesSpec spec) => _spec = spec;

    public override AcceptanceCriterionResult AdminCanAssignRoles(
        UserId actingUser, UserId targetUser, RoleId role)
    {
        var result = _spec.AssignRole(
            new User(actingUser, "acting", new HashSet<Role>()),
            new User(targetUser, "target", new HashSet<Role>()),
            new Role(role, role.Value, new HashSet<Permission>()));

        return result.IsSuccess
            ? AcceptanceCriterionResult.Satisfied()
            : AcceptanceCriterionResult.Failed(result.Reason!);
    }

    public override AcceptanceCriterionResult ViewerHasReadOnlyAccess(
        UserId viewer, ResourceId resource)
    {
        var result = _spec.EnforceReadOnlyAccess(
            new User(viewer, "viewer", new HashSet<Role>()),
            new Resource(resource, "generic"),
            Operation.Write);

        return !result.IsSuccess
            ? AcceptanceCriterionResult.Satisfied()
            : AcceptanceCriterionResult.Failed("Viewer was allowed to write");
    }

    public override AcceptanceCriterionResult RoleChangeTakesEffectImmediately(
        UserId user, RoleId previousRole, RoleId newRole)
    {
        var result = _spec.VerifyImmediateRoleEffect(
            new User(user, "user", new HashSet<Role>()),
            new Role(previousRole, previousRole.Value, new HashSet<Permission>()),
            new Role(newRole, newRole.Value, new HashSet<Permission>()));

        return result.IsSuccess
            ? AcceptanceCriterionResult.Satisfied()
            : AcceptanceCriterionResult.Failed(result.Reason!);
    }
}

Two runtime modes:

// Mode 1: Normal production flow -- call ISpec directly
public class RoleController
{
    private readonly IUserRolesSpec _spec;

    [HttpPost("users/{targetId}/role")]
    public IActionResult AssignRole(Guid targetId, [FromBody] AssignRoleRequest req)
    {
        var result = _spec.AssignRole(actingUser, targetUser, role);
        return result.IsSuccess ? Ok() : BadRequest(result.Reason);
    }
}

// Mode 2: Compliance mode -- validate ACs explicitly (audit trail, regulatory)
public class ComplianceService
{
    private readonly UserRolesValidator _validator;

    public AcceptanceCriterionResult AuditRoleAssignment(
        UserId acting, UserId target, RoleId role)
    {
        var result = _validator.AdminCanAssignRoles(acting, target, role);
        _auditLog.Record("FEATURE:UserRoles", "AC:AdminCanAssignRoles", result);
        return result;
    }
}

What makes Layer 2 work:

  1. IUserRolesSpec is a compiler-enforced contract. If Domain claims to implement user roles, it must implement every method.
  2. Each method on the interface is linked to its AC via [ForRequirement(typeof(...), nameof(...))] -- clickable in IDE.
  3. The validator bridge supports both runtime modes: normal production flow (ISpec) and compliance evaluation (validator).
  4. Specifications defines only interfaces. Domain types come from SharedKernel. No duplication.

Layer 3: MyApp.Implementation -- Compiler-Enforced Domain Code

This project references MyApp.Specifications (and transitively, MyApp.Requirements). The domain implements spec interfaces. The colon operator IS the guarantee. Every class and method is decorated with [ForRequirement] for full IDE navigability.

namespace MyApp.Domain.Auth;

using MyApp.Specifications;
using MyApp.Requirements.Features;

/// <summary>
/// Domain service implementing user roles.
/// The compiler FORCES this class to implement all three IUserRolesSpec methods.
/// If a new AC is added to the specification interface, this class
/// will not compile until the new method is implemented.
/// </summary>
[ForRequirement(typeof(UserRolesFeature))]
public class AuthorizationService : IUserRolesSpec
{
    private readonly IUserRepository _users;
    private readonly IRoleRepository _roles;
    private readonly IPermissionCache _cache;

    public AuthorizationService(
        IUserRepository users, IRoleRepository roles, IPermissionCache cache)
    {
        _users = users;
        _roles = roles;
        _cache = cache;
    }

    [ForRequirement(typeof(UserRolesFeature), nameof(UserRolesFeature.AdminCanAssignRoles))]
    public Result AssignRole(User actingUser, User targetUser, Role role)
    {
        if (!actingUser.Roles.Any(r => r.Name == "Admin"))
            return Result.Failure("Only admins can assign roles");

        if (role is null)
            return Result.Failure("Role cannot be null");

        // Domain logic: persist the assignment
        _users.AssignRole(targetUser.Id, role.Id);
        return Result.Success();
    }

    [ForRequirement(typeof(UserRolesFeature), nameof(UserRolesFeature.ViewerHasReadOnlyAccess))]
    public Result EnforceReadOnlyAccess(User viewer, Resource resource, Operation operation)
    {
        var isViewer = viewer.Roles.Any(r => r.Name == "Viewer");
        if (isViewer && operation != Operation.Read)
            return Result.Failure($"Viewer cannot perform {operation} on {resource.Id}");

        return Result.Success();
    }

    [ForRequirement(typeof(UserRolesFeature), nameof(UserRolesFeature.RoleChangeTakesEffectImmediately))]
    public Result VerifyImmediateRoleEffect(User user, Role previousRole, Role newRole)
    {
        _cache.Invalidate(user.Id);

        var currentPermissions = _cache.GetPermissions(user.Id);
        var hasOldPermissions = currentPermissions.Intersect(previousRole.Permissions)
            .Except(newRole.Permissions).Any();

        if (hasOldPermissions)
            return Result.Failure("Old permissions still cached after role change");

        return Result.Success();
    }
}

What makes Layer 3 work:

  1. AuthorizationService : IUserRolesSpec -- the compiler forces all three AC methods to exist with the correct signatures.
  2. [ForRequirement(typeof(UserRolesFeature))] -- type reference, not string. Ctrl+Click jumps to the feature. Refactoring tools track it.
  3. Each method is linked to its specific AC via nameof(UserRolesFeature.AdminCanAssignRoles) -- compiler-checked.
  4. If someone adds AC4 to IUserRolesSpec, AuthorizationService fails to compile until AC4 is implemented. This is the entire point.

Layer 4: MyApp.Tests -- Type-Linked Verification

This project references MyApp.Implementation and MyApp.Specifications (and transitively, MyApp.Requirements). Test classes link to requirements through type references.

namespace MyApp.Tests.Auth;

using MyApp.Requirements.Features;
using MyApp.Specifications;
using MyApp.Domain.Auth;

[TestFixture]
[TestsFor(typeof(UserRolesFeature))]
public class UserRolesTests
{
    private AuthorizationService _authService;
    private User _admin;
    private User _viewer;
    private Role _editorRole;

    [SetUp]
    public void Setup()
    {
        _authService = new AuthorizationService(
            new InMemoryUserRepository(),
            new InMemoryRoleRepository(),
            new InMemoryPermissionCache());

        var adminRole = new Role(new RoleId("admin"), "Admin",
            new HashSet<Permission> { new("manage-roles") });
        var viewerRole = new Role(new RoleId("viewer"), "Viewer",
            new HashSet<Permission> { new("read") });

        _admin = new User(new UserId(Guid.NewGuid()), "Alice",
            new HashSet<Role> { adminRole });
        _viewer = new User(new UserId(Guid.NewGuid()), "Bob",
            new HashSet<Role> { viewerRole });
        _editorRole = new Role(new RoleId("editor"), "Editor",
            new HashSet<Permission> { new("read"), new("write") });
    }

    [Test]
    [Verifies(typeof(UserRolesFeature), nameof(UserRolesFeature.AdminCanAssignRoles))]
    public void Admin_can_assign_role_to_another_user()
    {
        var target = new User(new UserId(Guid.NewGuid()), "Charlie", new HashSet<Role>());
        var result = _authService.AssignRole(_admin, target, _editorRole);
        Assert.That(result.IsSuccess, Is.True);
    }

    [Test]
    [Verifies(typeof(UserRolesFeature), nameof(UserRolesFeature.AdminCanAssignRoles))]
    public void Non_admin_cannot_assign_roles()
    {
        var target = new User(new UserId(Guid.NewGuid()), "Charlie", new HashSet<Role>());
        var result = _authService.AssignRole(_viewer, target, _editorRole);
        Assert.That(result.IsSuccess, Is.False);
        Assert.That(result.Reason, Does.Contain("Only admins"));
    }

    [Test]
    [Verifies(typeof(UserRolesFeature), nameof(UserRolesFeature.ViewerHasReadOnlyAccess))]
    public void Viewer_cannot_write()
    {
        var resource = new Resource(new ResourceId("order-123"), "Order");
        var result = _authService.EnforceReadOnlyAccess(_viewer, resource, Operation.Write);
        Assert.That(result.IsSuccess, Is.False);
    }

    [Test]
    [Verifies(typeof(UserRolesFeature), nameof(UserRolesFeature.ViewerHasReadOnlyAccess))]
    public void Viewer_can_read()
    {
        var resource = new Resource(new ResourceId("order-123"), "Order");
        var result = _authService.EnforceReadOnlyAccess(_viewer, resource, Operation.Read);
        Assert.That(result.IsSuccess, Is.True);
    }

    [Test]
    [Verifies(typeof(UserRolesFeature), nameof(UserRolesFeature.RoleChangeTakesEffectImmediately))]
    public void Role_change_invalidates_cache_immediately()
    {
        var previousRole = new Role(new RoleId("viewer"), "Viewer",
            new HashSet<Permission> { new("read") });
        var result = _authService.VerifyImmediateRoleEffect(_admin, previousRole, _editorRole);
        Assert.That(result.IsSuccess, Is.True);
    }
}

What makes Layer 4 work:

  1. [TestsFor(typeof(UserRolesFeature))] -- type reference to the requirement. Source generators enumerate all tests for any feature.
  2. [Verifies(typeof(UserRolesFeature), nameof(UserRolesFeature.AdminCanAssignRoles))] -- the nameof() is compiler-checked. Rename the AC method: this updates. Delete the AC method: compile error.
  3. Multiple tests can verify the same AC (positive and negative cases). The coverage report knows AdminCanAssignRoles has 2 tests.
  4. Tests call domain code through the specification interface -- the same interface the domain was forced to implement.

Layer 5: MyApp.Api -- The Production Host

This is where the chain becomes a deployable application. The API project references MyApp.Domain (and transitively, everything else). It wires the DI container, exposes controllers, and optionally enables compliance validation.

DI Registration

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Wire the requirement chain
builder.Services.AddScoped<IUserRolesSpec, AuthorizationService>();
builder.Services.AddScoped<IJwtRefreshSpec, JwtRefreshService>();

// Optional: enable compliance mode for audit-sensitive operations
builder.Services.AddScoped<UserRolesValidator>(sp =>
    new UserRolesValidator(sp.GetRequiredService<IUserRolesSpec>()));

// Optional: enable requirement compliance checking at startup
builder.Services.AddHostedService<RequirementComplianceCheck>();

var app = builder.Build();
app.MapControllers();
app.Run();

Controller -- Normal Production Flow

namespace MyApp.Api.Controllers;

using MyApp.Specifications;
using MyApp.SharedKernel;
using MyApp.Requirements.Features;

[ApiController]
[Route("api/users")]
[ForRequirement(typeof(UserRolesFeature))]
public class UsersController : ControllerBase
{
    private readonly IUserRolesSpec _spec;

    public UsersController(IUserRolesSpec spec) => _spec = spec;

    [HttpPost("{targetId}/role")]
    [ForRequirement(typeof(UserRolesFeature), nameof(UserRolesFeature.AdminCanAssignRoles))]
    public IActionResult AssignRole(Guid targetId, [FromBody] AssignRoleRequest request)
    {
        var actingUser = HttpContext.GetCurrentUser();
        var targetUser = _users.GetById(new UserId(targetId));
        var role = _roles.GetById(new RoleId(request.RoleId));

        var result = _spec.AssignRole(actingUser, targetUser, role);

        return result.IsSuccess
            ? Ok(new { message = "Role assigned" })
            : BadRequest(new { error = result.Reason });
    }
}

The [ForRequirement] on the controller and action method means the generated OpenAPI schema includes requirement metadata:

{
  "paths": {
    "/api/users/{targetId}/role": {
      "post": {
        "x-requirement": "UserRolesFeature",
        "x-acceptance-criterion": "AdminCanAssignRoles",
        "summary": "Assign a role to a user"
      }
    }
  }
}

Startup Compliance Check

public class RequirementComplianceCheck : IHostedService
{
    private readonly ILogger<RequirementComplianceCheck> _logger;

    public Task StartAsync(CancellationToken ct)
    {
        // TraceabilityMatrix is source-generated
        foreach (var (type, entry) in TraceabilityMatrix.Entries)
        {
            var info = RequirementRegistry.All[type];
            var totalACs = info.AcceptanceCriteria.Length;
            var testedACs = entry.AcceptanceCriteriaCoverage.Count;

            if (testedACs < totalACs)
                _logger.LogWarning("{Feature}: {Tested}/{Total} ACs tested ({Missing} missing)",
                    info.Title, testedACs, totalACs,
                    string.Join(", ", info.AcceptanceCriteria
                        .Except(entry.AcceptanceCriteriaCoverage.Keys)));
            else
                _logger.LogInformation("{Feature}: all {Total} ACs covered",
                    info.Title, totalACs);
        }

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}

DLL Artifacts at Build Time

After dotnet build, the solution produces:

bin/
├── MyApp.Requirements.dll           ← Feature types, AC methods, generated attributes
├── MyApp.SharedKernel.dll           ← User, Role, Permission, Result
├── MyApp.Specifications.dll         ← IUserRolesSpec, IJwtRefreshSpec, validator bridges
├── MyApp.Domain.dll                 ← AuthorizationService : IUserRolesSpec
├── MyApp.Api.dll                    ← Controllers, DI wiring, startup compliance
└── MyApp.Tests.dll                  ← Type-linked tests (not deployed)

Generated at compile time (embedded in respective DLLs):
├── RequirementRegistry.g.cs         ← Type catalog + hierarchy
├── TraceabilityMatrix.g.cs          ← Requirement → Implementation → Test mapping
└── Compiler diagnostics             ← IDE warnings for untested ACs

IDE Navigation Chain

From any layer, "Find All References" on UserRolesFeature shows the entire traceability chain:

UserRolesFeature                          <-- Layer 1: requirement definition
|-- IUserRolesSpec                        <-- Layer 2: [ForRequirement(typeof(UserRolesFeature))]
|   |-- .AssignRole                       <-- Layer 2: [ForRequirement(..., nameof(...AdminCanAssignRoles))]
|   |-- .EnforceReadOnlyAccess            <-- Layer 2: [ForRequirement(..., nameof(...ViewerHasReadOnlyAccess))]
|   '-- .VerifyImmediateRoleEffect        <-- Layer 2: [ForRequirement(..., nameof(...RoleChangeTakesEffectImmediately))]
|-- AuthorizationService                  <-- Layer 3: [ForRequirement(typeof(UserRolesFeature))]
|   |-- .AssignRole                       <-- Layer 3: [ForRequirement(..., nameof(...AdminCanAssignRoles))]
|   '-- ...
'-- UserRolesTests                        <-- Layer 4: [TestsFor(typeof(UserRolesFeature))]
    |-- .Admin_can_assign_role            <-- Layer 4: [Verifies(..., nameof(...AdminCanAssignRoles))]
    |-- .Non_admin_cannot_assign_roles    <-- Layer 4: [Verifies(..., nameof(...AdminCanAssignRoles))]
    '-- ...

Every link is a typeof() or nameof() -- Ctrl+Click navigable, refactor-safe, compiler-checked.


Source-Generated Traceability

The Roslyn source generator, referenced by all projects as an analyzer, cross-references [ForRequirement], [TestsFor], and [Verifies] attributes to produce:

Traceability Matrix

// Generated: TraceabilityMatrix.g.cs
public static class TraceabilityMatrix
{
    public static IReadOnlyDictionary<Type, TraceabilityEntry> Entries { get; } =
        new Dictionary<Type, TraceabilityEntry>
        {
            [typeof(UserRolesFeature)] = new TraceabilityEntry(
                RequirementType: typeof(UserRolesFeature),
                Implementations: new[]
                {
                    new ImplementationRef(typeof(AuthorizationService), typeof(IUserRolesSpec))
                },
                Tests: new[]
                {
                    new TestRef(typeof(UserRolesTests), "Admin_can_assign_role_to_another_user",
                        nameof(UserRolesFeature.AdminCanAssignRoles)),
                    new TestRef(typeof(UserRolesTests), "Non_admin_cannot_assign_roles",
                        nameof(UserRolesFeature.AdminCanAssignRoles)),
                    new TestRef(typeof(UserRolesTests), "Viewer_cannot_write",
                        nameof(UserRolesFeature.ViewerHasReadOnlyAccess)),
                    new TestRef(typeof(UserRolesTests), "Viewer_can_read",
                        nameof(UserRolesFeature.ViewerHasReadOnlyAccess)),
                    new TestRef(typeof(UserRolesTests), "Role_change_invalidates_cache_immediately",
                        nameof(UserRolesFeature.RoleChangeTakesEffectImmediately)),
                },
                AcceptanceCriteriaCoverage: new Dictionary<string, int>
                {
                    [nameof(UserRolesFeature.AdminCanAssignRoles)] = 2,
                    [nameof(UserRolesFeature.ViewerHasReadOnlyAccess)] = 2,
                    [nameof(UserRolesFeature.RoleChangeTakesEffectImmediately)] = 1
                }),
        };
}

Compiler Diagnostics

The generator emits Roslyn diagnostics -- not runtime checks, but IDE warnings and CI build output:

warning REQ001: UserRolesFeature -- all 3 acceptance criteria have tests (100% coverage)
warning REQ002: JwtRefreshStory -- 0/2 acceptance criteria have tests (0% coverage)
  -> Missing: TokensExpireAfterOneHour, RefreshExtendsBySevenDays
warning REQ003: No class found implementing IJwtRefreshSpec
  -> JwtRefreshStory has a specification but no domain implementation

These appear in the IDE Error List and in CI build output. No runtime overhead. No reflection.


Roslyn Analyzers: Enforcing the Chain at Compile Time

The MyApp.Requirements.Analyzers project is a Roslyn analyzer + source generator that enforces three invariants: every requirement is specified, every specification is implemented, and every implementation is tested. Violations are compiler errors or warnings -- not runtime failures.

Analyzer 1: Requirement Coverage (REQ1xx)

Scans Layer 1 (MyApp.Requirements) for all types inheriting RequirementMetadata with abstract AC methods. Scans Layer 2 (MyApp.Specifications) for [ForRequirement] attributes. Reports gaps.

Diagnostic Severity Trigger
REQ100 Error Feature type has no [ForRequirement(typeof(Feature))] interface in Specifications
REQ101 Error Feature AC method has no matching [ForRequirement(..., nameof(AC))] on any spec interface method
REQ102 Warning Story type has no specification (acceptable for small stories, error in strict mode)
REQ103 Info Requirement fully specified -- all ACs have spec methods
// Build output:
error REQ100: UserRolesFeature has 3 acceptance criteria but no ISpec interface
              references it via [ForRequirement(typeof(UserRolesFeature))]
              → Create IUserRolesSpec with [ForRequirement(typeof(UserRolesFeature))]

error REQ101: UserRolesFeature.RoleChangeTakesEffectImmediately has no matching
              spec method with [ForRequirement(typeof(UserRolesFeature),
              nameof(UserRolesFeature.RoleChangeTakesEffectImmediately))]
              → Add a method to IUserRolesSpec with the matching [ForRequirement]

Analyzer 2: Specification Implementation (REQ2xx)

Scans Layer 2 interfaces decorated with [ForRequirement]. Scans Layer 3 (MyApp.Domain) for classes implementing those interfaces. Reports unimplemented specs.

Diagnostic Severity Trigger
REQ200 Error Spec interface has no implementing class in Domain
REQ201 Warning Implementing class does not have [ForRequirement] attribute (works but loses traceability)
REQ202 Warning Implementing class has [ForRequirement] but method-level attributes are missing
REQ203 Info Spec fully implemented -- all methods have [ForRequirement] on impl
// Build output:
error REQ200: IUserRolesSpec is not implemented by any class in MyApp.Domain
              → Create a class that implements IUserRolesSpec

warning REQ201: AuthorizationService implements IUserRolesSpec but is missing
                [ForRequirement(typeof(UserRolesFeature))] on the classAdd [ForRequirement(typeof(UserRolesFeature))] for traceability

warning REQ202: AuthorizationService.VerifyImmediateRoleEffect implements
                IUserRolesSpec but is missing [ForRequirement(typeof(UserRolesFeature),
                nameof(UserRolesFeature.RoleChangeTakesEffectImmediately))]
                → Add method-level [ForRequirement] for IDE navigation

Analyzer 3: Test Coverage (REQ3xx)

Scans Layer 4 (MyApp.Tests) for [TestsFor] and [Verifies] attributes. Cross-references with Layer 1 AC methods. Reports untested ACs.

Diagnostic Severity Trigger
REQ300 Error Feature has zero [TestsFor] test classes
REQ301 Warning AC method has no [Verifies(..., nameof(AC))] test
REQ302 Warning [Verifies] references an AC method that doesn't exist (stale test)
REQ303 Info Feature fully tested -- all ACs have at least one [Verifies] test
// Build output:
error REQ300: JwtRefreshStory has 2 acceptance criteria but no test class
              with [TestsFor(typeof(JwtRefreshStory))]

warning REQ301: UserRolesFeature.RoleChangeTakesEffectImmediately has no test
                with [Verifies(typeof(UserRolesFeature),
                nameof(UserRolesFeature.RoleChangeTakesEffectImmediately))]
                → Add a test method with [Verifies] for this AC

warning REQ302: UserRolesTests.OldTest references nameof(UserRolesFeature.DeletedAC)
                which no longer exists on UserRolesFeature
                → Remove or update the stale [Verifies] attribute

Analyzer 4: Quality Gates (REQ4xx)

Integrates with a dotnet quality-gates tool (invoked as an MSBuild target after test execution) to enforce that tests don't just exist -- they pass, meet coverage thresholds, and satisfy performance budgets.

MSBuild integration:

<!-- MyApp.Tests.csproj -->
<PropertyGroup>
  <RequirementMinCoverage>80</RequirementMinCoverage>
  <RequirementMinPassRate>100</RequirementMinPassRate>
  <RequirementMaxTestDuration>5000</RequirementMaxTestDuration> <!-- ms per test -->
</PropertyGroup>

<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)"
    ConsoleToMsBuild="true" />
</Target>

What dotnet quality-gates check validates:

Gate What it checks Fails when
Pass rate All [Verifies] tests passed in .trx results Any test marked [Verifies] failed
AC coverage Every AC in TraceabilityMatrix has a passing [Verifies] test AC exists but its test failed or is missing
Code coverage Lines touched by [Verifies] tests cover the [ForRequirement] methods Implementation method has <N% line coverage
Duration Individual test execution time Any [Verifies] test exceeds max duration
Flakiness Test result consistency across runs A [Verifies] test has inconsistent pass/fail across retries
Fuzz testing Auto-generated random/boundary inputs for AC methods Implementation crashes or returns unexpected results on edge cases

The fuzz testing gate deserves special attention. Because every AC is an abstract method with a typed signature, the tool knows exactly what inputs to generate. For AdminCanAssignRoles(UserId actingUser, UserId targetUser, RoleId role), it can automatically produce: null values, empty GUIDs, duplicate user/target IDs, nonexistent role IDs, extremely long strings, Unicode edge cases, and concurrent calls. The spec method's Result return type means the tool knows what "not crashing" looks like -- a Result.Failure with a reason is acceptable; an unhandled exception is not. This is property-based testing derived from the requirement signature itself.

Output:

$ dotnet quality-gates check --trx results.trx --coverage coverage.xml ...

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

FEATURE: UserRolesFeature (3 ACs, 5 tests)
  ✓ AdminCanAssignRoles      2 tests, 100% pass, 94% coverage, avg 12ms
    fuzz: 500 inputs, 0 crashes, 12 Result.Failure (expected)
  ✓ ViewerHasReadOnlyAccess  2 tests, 100% pass, 88% coverage, avg 8ms
    fuzz: 500 inputs, 0 crashes, 8 Result.Failure (expected)
  ✗ RoleChangeTakesEffect    1 test,  100% pass, 62% coverage ← BELOW 80% THRESHOLD
    fuzz: 500 inputs, 1 CRASH ← NullReferenceException with empty RoleId
    → AuthorizationService.VerifyImmediateRoleEffect:L45-L52 not covered

STORY: JwtRefreshStory (2 ACs, 0 tests)
  ✗ TokensExpireAfterOneHour   NO TESTS
  ✗ RefreshExtendsBySevenDays  NO TESTS

Summary: 2/3 quality gates passed for UserRolesFeature (fuzz found 1 crash)
         0/2 quality gates passed for JwtRefreshStory
         Overall: FAIL (exit code 1)

Severity Configuration

The analyzer severity is configurable per-project via .editorconfig:

# .editorconfig
[*.cs]

# Requirements → Specifications: treat missing specs as errors
dotnet_diagnostic.REQ100.severity = error
dotnet_diagnostic.REQ101.severity = error
dotnet_diagnostic.REQ102.severity = warning

# Specifications → Implementation: treat missing impl as errors
dotnet_diagnostic.REQ200.severity = error
dotnet_diagnostic.REQ201.severity = warning
dotnet_diagnostic.REQ202.severity = suggestion

# Implementation → Tests: treat missing tests as warnings (errors in CI)
dotnet_diagnostic.REQ300.severity = warning
dotnet_diagnostic.REQ301.severity = warning
dotnet_diagnostic.REQ302.severity = error    # stale tests are always errors

For CI, override to strict:

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

The Full CI Pipeline

# .github/workflows/build.yml
jobs:
  build:
    steps:
      - run: dotnet build --warnaserror     # REQ1xx, REQ2xx, REQ3xx analyzers
      - run: dotnet test --collect:"XPlat Code Coverage" --logger:trx
      - run: dotnet quality-gates check     # REQ4xx quality gates
        --trx TestResults/*.trx
        --coverage TestResults/**/coverage.cobertura.xml
        --min-pass-rate 100
        --min-coverage 80

Three enforcement points, one build:

  1. dotnet build -- Roslyn analyzers catch structural gaps (missing specs, missing implementations, missing tests)
  2. dotnet test -- tests execute against the implementation
  3. dotnet quality-gates -- post-test analysis ensures tests are passing, coverage is sufficient, and performance is acceptable

If any gate fails, the build fails. The chain is enforced from requirement to quality-verified production code.


Five-Stage Generation Pipeline

The generator follows the CMF's existing multi-stage Roslyn pipeline:

Stage 0: Metamodel Registration

Register requirement base types (Epic, Feature<T>, Story<T>, Task<T>, Bug) as M3 MetaConcepts.

Stage 1: Requirement Collection

Walk compilation for types inheriting RequirementMetadata. Build the hierarchy graph. Validate constraints (no cycles, valid parent-child types).

Stage 2: Registry Generation

Generate RequirementRegistry.g.cs -- the type catalog with hierarchy, AC method names, and metadata.

Stage 3: Bridge Validators

Generate specification bridge classes (the UserRolesValidator that connects abstract Feature AC methods to ISpec methods).

Stage 4: Traceability Matrix

Cross-reference [ForRequirement], [TestsFor], [Verifies] attributes across all referenced assemblies. Generate TraceabilityMatrix.g.cs. Emit compiler diagnostics for coverage gaps.

Stage 5: Reports

Generate markdown hierarchy reports, JSON export for external tool integration (Jira, Linear, Azure DevOps), and CSV traceability matrices.


Requirement Lifecycle State Machine

All requirements support lifecycle tracking, automatically generated as a state machine:

Draft --> Proposed --> Approved --> InProgress --> Quality --> Translation --> Review --> Done
public enum RequirementLifecycleState : byte
{
    Draft = 0,
    Proposed = 1,
    Approved = 2,
    InProgress = 3,
    Quality = 4,
    Translation = 5,
    Review = 6,
    Done = 7
}

public class RequirementLifecycleStateMachine
{
    public static bool CanTransition(RequirementLifecycleState from, RequirementLifecycleState to)
    {
        if (to > from) return to == from + 1;   // Forward: one step at a time
        if (to < from) return to == from - 1;   // Backward: one step back allowed
        return false;
    }
}

The state machine is a by-product of the meta-metamodel -- the M3 layer understands "requirements have a lifecycle," and generation creates the SM automatically.


Integration With Existing CMF DSLs

M3/M2/M1/M0 Mapping

Level In CMF In Requirements Chain
M3 MetaConcept, MetaProperty RequirementMetadata, Feature<T>, Story<T> base types
M2 [AggregateRoot], [Entity] UserRolesFeature, JwtRefreshStory -- concrete requirement types
M1 Generated entities, repos, CQRS RequirementRegistry.g.cs, TraceabilityMatrix.g.cs, diagnostics
M0 Runtime instances AuthorizationService processing actual User objects

With DDD DSL

[ForRequirement(typeof(UserRolesFeature))]
[AggregateRoot]
public partial class AuthAggregate : IUserRolesSpec
{
    [ForRequirement(typeof(UserRolesFeature), nameof(UserRolesFeature.AdminCanAssignRoles))]
    public Result AssignRole(User actingUser, User targetUser, Role role)
    {
        // DDD generator produces Entity + Repository + CQRS handlers
        // ALL tagged with [ForRequirement(typeof(UserRolesFeature))]
    }
}

With Workflow DSL

[ForRequirement(typeof(UserRolesFeature))]
[Workflow(Name = "FeatureApproval", Stages = new[] { "Design", "Impl", "Test", "Review" })]
public partial class FeatureWorkflow { }

// Workflow rules can reference requirement compliance:
[WorkflowRule(Stage = "Test", Condition = "All ACs have [Verifies] tests")]
[WorkflowRule(Stage = "Review", Condition = "Test coverage >= 80%")]

With Admin DSL

[ForRequirement(typeof(UserRolesFeature))]
[AdminModule(Name = "FeatureTracking", Path = "/admin/features")]
public partial class FeatureAdminPanel { }
// Generates: dashboard with compliance status, implementations per feature, test coverage

Before and After

Before -- stringly-typed:

[Feature(Id = "FEATURE-456", AcceptanceCriteria = new[] { "Admin can assign roles" })]
public class UserRoleFeature { }  // Empty marker class -- dead code

[Implements(Requirements.FEATURE_USER_ROLES)]  // const string = "FEATURE-456"
public void AssignRole(User u, Role r) { }

[FeatureTest(Requirements.FEATURE_USER_ROLES, 0)]  // AC[0] by fragile index
public void AdminCanAssign() { }

Problems: strings can be wrong, AC indices are fragile, no compiler check that implementation satisfies ACs, marker class is dead code.

After -- type-safe:

// Requirement IS a type with abstract AC methods
public abstract record UserRolesFeature : Feature<PlatformScalabilityEpic>
{
    public abstract AcceptanceCriterionResult AdminCanAssignRoles(
        UserId actingUser, UserId targetUser, RoleId role);
}

// Specification IS an interface the domain must implement
[ForRequirement(typeof(UserRolesFeature))]
public interface IUserRolesSpec
{
    [ForRequirement(typeof(UserRolesFeature), nameof(UserRolesFeature.AdminCanAssignRoles))]
    Result AssignRole(User actingUser, User targetUser, Role role);
}

// Implementation MUST implement the interface -- compiler enforces it
[ForRequirement(typeof(UserRolesFeature))]
public class AuthorizationService : IUserRolesSpec
{
    public Result AssignRole(User actingUser, User targetUser, Role role) { ... }
    // Remove this method --> compile error
}

// Test linked by type + nameof -- compiler-checked
[Verifies(typeof(UserRolesFeature), nameof(UserRolesFeature.AdminCanAssignRoles))]
public void Admin_can_assign_role() { ... }
// Rename AC method --> compile error. Delete AC method --> compile error.
Aspect Before (strings) After (types)
Requirement "FEATURE-456" const string typeof(UserRolesFeature)
Acceptance criterion new[] { "Admin can..." } abstract AcceptanceCriterionResult AdminCanAssignRoles(...)
Implementation link [Implements("FEATURE-456")] : IUserRolesSpec -- compiler enforces
Test link [FeatureTest("FEATURE-456", 0)] [Verifies(typeof(...), nameof(...AdminCanAssignRoles))]
Add new AC Add string to array, hope Add abstract method --> compile error everywhere
Rename Find-replace, pray Refactoring tools update all references

Requirements Driven Development

Features Span DLLs, Processes, and Machines

In a real system, a Feature doesn't live in one project. "Order Processing" touches the API, the payment gateway, the notification worker, the event bus, and the test suite -- each compiled to a separate DLL, potentially running on different machines. The .Requirements project is the cross-cutting source of truth that all of these reference via <ProjectReference>:

MyCompany.sln
├── src/
│   ├── MyCompany.Requirements/          ← Every project references this
│   ├── MyCompany.OrderService/          ← [Implements] OrderProcessingSpec
│   ├── MyCompany.PaymentGateway/        ← [Implements] PaymentValidationSpec
│   ├── MyCompany.NotificationWorker/    ← [Implements] OrderNotificationSpec
│   └── MyCompany.SharedKernel/          ← Domain types shared across services
└── test/
    ├── MyCompany.OrderService.Tests/    ← [Verifies] OrderProcessing ACs
    └── MyCompany.PaymentGateway.Tests/  ← [Verifies] PaymentValidation ACs

Every DLL knows which Features it implements. Every test knows which AC it verifies. The .Requirements project is the single artifact that ties them together -- across solution boundaries, across machines, across teams.

Acceptance Criteria Are Live Code

ACs are not documentation. They are static methods that validate business rules -- and they run in two places:

// In the AC definition (MyCompany.Requirements)
public abstract partial record OrderProcessingFeature : Feature
{
    // This AC is an abstract method -- but its generated validation logic is concrete
    public static bool OrderTotalMustBePositive(Order order)
        => order.Total > 0 && order.Lines.All(l => l.Quantity > 0);
}

// In production code (MyCompany.OrderService)
public class OrderService : IOrderProcessingSpec
{
    public Result<Order, IDomainException> CreateOrder(CreateOrderCommand cmd)
    {
        var order = Order.CreateInstance(cmd);

        // AC validation called in production -- same method the tests use
        if (!OrderProcessingFeature.OrderTotalMustBePositive(order))
            throw new InvalidOrderException("Order total must be positive");

        return Result<Order, IDomainException>.Success(order);
    }
}

// In unit tests (MyCompany.OrderService.Tests)
[Verifies(typeof(OrderProcessingFeature), nameof(OrderProcessingFeature.OrderTotalMustBePositive))]
public void Order_with_positive_total_passes_AC()
{
    var order = CreateValidOrder();
    Assert.True(OrderProcessingFeature.OrderTotalMustBePositive(order));
}

The same AC method is called in production to enforce the rule and in tests to verify it. One definition, two uses. This is what gives value to the entire process: ACs are compiled, tested, and running in production. They are not a checklist in Jira that someone forgets to update.

The Developer Sees the Full Scope

In a 50-project mono-repo, a developer working on OrderLine can Ctrl+Click through the entire chain:

  1. Test[Verifies(typeof(OrderProcessingFeature), nameof(OrderTotalMustBePositive))] → click → lands on the AC method
  2. AC method → defined on OrderProcessingFeature → click → lands on the Feature definition
  3. Feature[Epic(typeof(ECommerceEpic))] → click → lands on the Epic
  4. Epic → see all Features, all ACs, all implementations, all tests

No Jira lookup. No "ask the PO." No context switching. The developer understands the full scope of what they're building, what's already implemented, and what's missing -- because it's all in the type system.

The .Requirements Project Is a Company Asset

This is the intersection of TDD (write the failing spec first), BDD (specs are behavior descriptions), and industrial software practice (traceability is mandatory in DO-178C, IEC 62304, ISO 26262). The .Requirements project is the masterpiece that holds the system together:

  • It's compiled -- invalid requirements produce compiler errors
  • It's tested -- every AC has a [Verifies] test or the build warns
  • It's running in production -- AC methods enforce business rules at runtime
  • It's versioned -- lifecycle states (Draft → Approved → InProgress → Done) are a generated state machine
  • It's traceable -- the source generator produces compliance reports, traceability matrices, and coverage metrics

It deserves the same care as the domain model. It deserves code review, refactoring, and architectural attention. Because it IS the system specification -- not a document about the system, but the system describing itself.


Why This Matters

For developers: Requirements are types with IDE support. Ctrl+Click from a test to the AC it verifies, from the AC to its Feature, from the Feature to its Epic. In a solution with 20+ projects spanning multiple services, every developer can trace from their code to the business requirement it satisfies -- and back down to every test that proves it. ACs are not documentation: they are static methods called in unit tests AND in production code. When you write a test, you're proving an AC. When you read production code, you see which AC it enforces. No context switching. No Jira lookups. No "ask the PO."

For the compiler: Adding an AC to a Feature is adding an abstract method. The build breaks until the specification interface has the method, the domain implements it, and tests verify it. The compiler IS the project manager.

For QA: The traceability matrix is source-generated. Coverage analysis is type-based, not string-matching. Every untested AC is a compiler warning.

For ops: The production host validates compliance at startup. API endpoints are annotated with requirement metadata in the OpenAPI schema. Compliance mode enables runtime AC evaluation for audit trails.

For architects: Requirements are first-class modeling primitives (M2 on M3). They integrate with DDD, Workflow, Admin, Content, and Pages DSLs. The hierarchy is generic-constrained. The lifecycle is a generated state machine. Each layer compiles to a distinct DLL artifact with clear dependencies.


What's Next

This blog post describes the design of a type-safe requirements chain. The implementation follows the CMF's existing multi-stage pipeline:

  1. Requirements (MyApp.Requirements.dll): Features as abstract records with AC methods, plus generated [ForRequirement]/[Verifies] attributes and RequirementRegistry
  2. SharedKernel (MyApp.SharedKernel.dll): Domain value types shared across all layers
  3. Specifications (MyApp.Specifications.dll): Interfaces decorated with [ForRequirement], plus validator bridges for compliance mode
  4. Domain (MyApp.Domain.dll): Implements spec interfaces -- compiler-enforced
  5. Api (MyApp.Api.dll): Production host with DI wiring, controllers with [ForRequirement], startup compliance check, OpenAPI enrichment
  6. Tests (MyApp.Tests.dll): Type-linked verification with [TestsFor] and [Verifies]
  7. Analyzers: Traceability matrix, compiler diagnostics, markdown/JSON/CSV reports

Six DLLs. One compiler. Every link is a type reference. Requirements are not documents -- they are types, and the compiler enforces the chain from definition to production deployment to end-to-end verification. No strings. No indices. No prayer.