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

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 }
⬇ Download