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

Ops.ApiContract + Ops.EnvironmentParity + Ops.Lifecycle

Three DSLs that share a theme: managing change over time. APIs evolve, environments drift, and components age. These are the DSLs that govern how.


The Problem

The mobile team shipped a release on Tuesday. On Wednesday, the backend team renamed a JSON field from total_amount to totalAmount to match the C# naming convention. The mobile app crashed for 2.3 million users. The backend team did not know anyone consumed that field with the old name. The mobile team did not know the backend team was renaming fields.

This is a breaking change. It happened because:

  1. No API versioning policy. The API had no version header, no deprecation notice, no sunset date. Changes went to production immediately.
  2. No schema diffing. Nobody compared the before and after OpenAPI specs. The field rename was a one-line change in a DTO that passed code review because the reviewer looked at C# naming conventions, not API compatibility.
  3. No consumer contracts. The mobile team consumed 14 endpoints, but there was no record of which fields they depended on. The backend team assumed nobody used total_amount because the new field totalAmount was "the same thing."

The ApiContract DSL makes API evolution a compile-time concern.


ApiVersionPolicy

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class ApiVersionPolicyAttribute : Attribute
{
    public ApiVersionPolicyAttribute(string currentVersion,
        string minSupportedVersion) { }

    /// <summary>
    /// Deprecation notice for versions between min supported and current.
    /// Included in response headers: Deprecation: true, Sunset: [date].
    /// </summary>
    public string? DeprecationNotice { get; init; }

    /// <summary>
    /// Date after which deprecated versions will be removed.
    /// Format: "yyyy-MM-dd". Null = no sunset scheduled.
    /// </summary>
    public string? SunsetDate { get; init; }

    /// <summary>
    /// How the version is communicated: Header, UrlSegment, QueryParam.
    /// </summary>
    public ApiVersioningScheme Scheme { get; init; }
        = ApiVersioningScheme.Header;

    /// <summary>
    /// Header name when Scheme is Header.
    /// </summary>
    public string VersionHeader { get; init; } = "api-version";
}

public enum ApiVersioningScheme
{
    /// <summary>Version in a request header (e.g., api-version: 2).</summary>
    Header,

    /// <summary>Version in the URL path (e.g., /v2/orders).</summary>
    UrlSegment,

    /// <summary>Version as a query parameter (e.g., ?api-version=2).</summary>
    QueryParam
}

BreakingChangeGuard

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class BreakingChangeGuardAttribute : Attribute
{
    public BreakingChangeGuardAttribute(
        string baselineSchema) { }

    /// <summary>
    /// Changes that are allowed without a version bump.
    /// </summary>
    public SchemaChange[] AllowedChanges { get; init; }
        = new[] { SchemaChange.AddField, SchemaChange.AddEndpoint };

    /// <summary>
    /// Changes that require a version bump and deprecation period.
    /// These trigger analyzer API002 if detected without a version bump.
    /// </summary>
    public SchemaChange[] ProhibitedChanges { get; init; }
        = new[]
        {
            SchemaChange.RemoveField,
            SchemaChange.RenameField,
            SchemaChange.ChangeFieldType,
            SchemaChange.RemoveEndpoint,
            SchemaChange.ChangeStatusCode
        };

    /// <summary>
    /// Path to the baseline OpenAPI schema file.
    /// The generator diffs the current schema against this baseline.
    /// </summary>
    public string BaselinePath => baselineSchema;
}

public enum SchemaChange
{
    AddField,
    RemoveField,
    RenameField,
    ChangeFieldType,
    AddEndpoint,
    RemoveEndpoint,
    ChangeStatusCode,
    ChangeRequiredFields,
    ChangeAuthScheme,
    ChangeRateLimit
}

ConsumerContract

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class ConsumerContractAttribute : Attribute
{
    public ConsumerContractAttribute(string consumer,
        string contractFile) { }

    /// <summary>
    /// How often to verify the contract against the current implementation.
    /// "build" = every build, "daily" = nightly CI, "weekly" = weekly CI.
    /// </summary>
    public string VerifySchedule { get; init; } = "build";

    /// <summary>
    /// Whether contract verification failure blocks the build.
    /// </summary>
    public bool FailOnViolation { get; init; } = true;

    /// <summary>
    /// Contract format: Pact, OpenApiSubset, Custom.
    /// </summary>
    public ContractFormat Format { get; init; }
        = ContractFormat.Pact;
}

public enum ContractFormat
{
    /// <summary>Pact contract (JSON, consumer-driven).</summary>
    Pact,

    /// <summary>Subset of an OpenAPI spec listing consumed fields.</summary>
    OpenApiSubset,

    /// <summary>Custom contract format via IContractVerifier.</summary>
    Custom
}

Declaration Example

[ApiVersionPolicy("3", "2",
    DeprecationNotice = "API v2 is deprecated. Migrate to v3 by 2026-09-01.",
    SunsetDate = "2026-09-01",
    Scheme = ApiVersioningScheme.Header,
    VersionHeader = "api-version")]

[BreakingChangeGuard("schemas/order-api-v3-baseline.json",
    AllowedChanges = new[] { SchemaChange.AddField, SchemaChange.AddEndpoint },
    ProhibitedChanges = new[]
    {
        SchemaChange.RemoveField,
        SchemaChange.RenameField,
        SchemaChange.ChangeFieldType,
        SchemaChange.RemoveEndpoint
    })]

[ConsumerContract("mobile-ios", "contracts/mobile-ios-order-v3.pact.json",
    VerifySchedule = "build",
    FailOnViolation = true)]

[ConsumerContract("mobile-android", "contracts/mobile-android-order-v3.pact.json",
    VerifySchedule = "build",
    FailOnViolation = true)]

[ConsumerContract("partner-api", "contracts/partner-api-order-v3.pact.json",
    VerifySchedule = "daily",
    FailOnViolation = false)]

public partial class OrderServiceV3Ops { }

Generated Artifacts

OpenAPI Diff Configuration: The generator produces a config file for schema diffing tools (oasdiff, openapi-diff) that compares the current schema against the baseline. Prohibited changes fail the build.

Pact Verification Harness: A PactVerifier.g.cs file that runs consumer contract tests during dotnet test. Each [ConsumerContract] becomes a verification test:

// Auto-generated: PactVerifier.g.cs
[TestClass]
public class OrderServiceV3PactVerification
{
    [TestMethod]
    public async Task Verify_MobileIos_Contract()
    {
        var pact = PactFile.Load("contracts/mobile-ios-order-v3.pact.json");
        await PactVerifier.Verify(pact,
            baseUri: TestEnvironment.OrderServiceBaseUri);
    }

    [TestMethod]
    public async Task Verify_MobileAndroid_Contract()
    {
        var pact = PactFile.Load("contracts/mobile-android-order-v3.pact.json");
        await PactVerifier.Verify(pact,
            baseUri: TestEnvironment.OrderServiceBaseUri);
    }
}

Deprecation Headers Middleware: A DeprecationMiddleware.g.cs that injects Deprecation and Sunset headers for requests using deprecated API versions:

// Auto-generated: DeprecationMiddleware.g.cs
public class OrderServiceV3DeprecationMiddleware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var version = context.Request.Headers["api-version"].FirstOrDefault();
        if (version == "2")
        {
            context.Response.Headers["Deprecation"] = "true";
            context.Response.Headers["Sunset"] = "Tue, 01 Sep 2026 00:00:00 GMT";
            context.Response.Headers["Link"] =
                "</docs/migration-v2-to-v3>; rel=\"deprecation\"";
        }
        await next(context);
    }
}

Analyzers

API001: Endpoint Removed Without Deprecation. If the current schema removes an endpoint that exists in the baseline, and no SunsetDate is set, the build fails. Endpoints must be deprecated before removal.

API002: Response Field Removed (Breaking Change). If a response field in the baseline schema is absent in the current schema, and SchemaChange.RemoveField is in the prohibited list, the build fails. The field rename that crashed 2.3 million mobile users would have been caught here.

API003: Consumer Contract Without Baseline. A [ConsumerContract] references a Pact file that does not exist. The contract verification would silently pass because there is nothing to verify. The analyzer catches it.



The Problem

"It works on my machine." "It works in staging." "It does not work in production."

The investigation revealed three differences between staging and production:

  1. Database schema. Staging was two migrations behind production because the deployment pipeline ran migrations in a different order. A column that existed in production did not exist in staging.
  2. Feature flags. The "new-checkout-flow" feature flag was enabled in staging and disabled in production. The staging tests validated the new flow. Production served the old flow. The test results were irrelevant.
  3. Service stubs. The staging environment used a stub for the payment gateway that always returned success. The production payment gateway had a rate limit that triggered after 100 requests per second. The load test in staging showed no issues. Production fell over during Black Friday.

Environment parity is not about making environments identical. It is about knowing where they differ, why they differ, and whether those differences are intentional.


ParityRule

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class ParityRuleAttribute : Attribute
{
    public ParityRuleAttribute(string name,
        string[] environments) { }

    /// <summary>
    /// The dimension of parity to enforce.
    /// </summary>
    public ParityDimension Dimension { get; init; }

    /// <summary>
    /// Whether violations are errors or warnings.
    /// </summary>
    public bool StrictEnforcement { get; init; } = true;

    /// <summary>
    /// Specific items to exclude from the parity check.
    /// e.g., exclude a config key that is intentionally different.
    /// </summary>
    public string[]? Exclusions { get; init; }
}

public enum ParityDimension
{
    /// <summary>Database schema must match across environments.</summary>
    Schema,

    /// <summary>Application configuration keys must exist in all envs.</summary>
    Config,

    /// <summary>Service versions must match (or be within N versions).</summary>
    ServiceVersions,

    /// <summary>Feature flag states must be documented if different.</summary>
    FeatureFlags,

    /// <summary>Data shape (table structures, index definitions).</summary>
    DataShape
}

FeatureFlag

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class FeatureFlagAttribute : Attribute
{
    public FeatureFlagAttribute(string name,
        Type ownerFeature) { }

    /// <summary>
    /// Default value when the flag provider is unavailable.
    /// </summary>
    public bool DefaultValue { get; init; } = false;

    /// <summary>
    /// Environments where this flag is enabled.
    /// Null = controlled by external flag provider.
    /// </summary>
    public string[]? EnabledEnvironments { get; init; }

    /// <summary>
    /// Date after which this flag should be removed.
    /// Permanent flags are tech debt. Every flag needs a sunset.
    /// </summary>
    public string? SunsetDate { get; init; }

    /// <summary>
    /// Percentage rollout (0-100). Null = binary on/off.
    /// </summary>
    public int? RolloutPercentage { get; init; }

    /// <summary>
    /// Description of what this flag controls. Required for documentation.
    /// </summary>
    public string Description { get; init; } = "";
}

The Type ownerFeature parameter takes a typeof() reference. This links the feature flag to a feature in the Requirements DSL. The flag is not a free-floating boolean -- it belongs to a feature, has an owner, and has a lifecycle.

ServiceStub

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class ServiceStubAttribute : Attribute
{
    public ServiceStubAttribute(string name,
        string[] environments) { }

    /// <summary>
    /// How the stub behaves: AlwaysSuccess, AlwaysFailure,
    /// RecordedResponses, Configurable.
    /// </summary>
    public StubBehavior Behavior { get; init; }
        = StubBehavior.RecordedResponses;

    /// <summary>
    /// Path to the recorded responses file.
    /// Used when Behavior is RecordedResponses.
    /// </summary>
    public string? ResponsesFile { get; init; }

    /// <summary>
    /// Simulated latency range (e.g., "50ms-200ms").
    /// Stubs should not be instant -- that hides timing issues.
    /// </summary>
    public string? SimulatedLatency { get; init; }

    /// <summary>
    /// Simulated rate limit (requests per second).
    /// Null = no rate limit (dangerous -- hides production behavior).
    /// </summary>
    public int? SimulatedRateLimit { get; init; }
}

public enum StubBehavior
{
    AlwaysSuccess,
    AlwaysFailure,
    RecordedResponses,
    Configurable
}

Declaration Example

[ParityRule("schema-parity",
    new[] { "dev", "staging", "production" },
    Dimension = ParityDimension.Schema,
    StrictEnforcement = true)]

[ParityRule("config-parity",
    new[] { "dev", "staging", "production" },
    Dimension = ParityDimension.Config,
    Exclusions = new[] { "ConnectionStrings:*", "ApiKeys:*" })]

[ParityRule("feature-flag-parity",
    new[] { "staging", "production" },
    Dimension = ParityDimension.FeatureFlags,
    StrictEnforcement = false)]

[FeatureFlag("new-checkout-flow",
    typeof(NewCheckoutFeature),
    DefaultValue = false,
    EnabledEnvironments = new[] { "dev", "staging" },
    SunsetDate = "2026-07-01",
    Description = "New checkout flow with single-page payment")]

[FeatureFlag("payment-v2-api",
    typeof(PaymentV2Feature),
    DefaultValue = false,
    RolloutPercentage = 25,
    SunsetDate = "2026-08-15",
    Description = "Payment v2 API with idempotency keys")]

[ServiceStub("payment-gateway",
    new[] { "dev", "test" },
    Behavior = StubBehavior.RecordedResponses,
    ResponsesFile = "stubs/payment-gateway-responses.json",
    SimulatedLatency = "100ms-500ms",
    SimulatedRateLimit = 100)]

public partial class OrderServiceV3Ops { }

Generated Artifacts

FeatureFlags.g.cs: Strongly-typed feature flag accessors. No magic strings.

// Auto-generated: FeatureFlags.g.cs
public static class OrderServiceV3FeatureFlags
{
    public static class NewCheckoutFlow
    {
        public const string Name = "new-checkout-flow";
        public const bool DefaultValue = false;
        public static readonly DateOnly SunsetDate = new(2026, 7, 1);
        public static readonly Type OwnerFeature = typeof(NewCheckoutFeature);

        public static bool IsEnabled(IFeatureFlagProvider provider)
            => provider.IsEnabled(Name, DefaultValue);
    }

    public static class PaymentV2Api
    {
        public const string Name = "payment-v2-api";
        public const bool DefaultValue = false;
        public static readonly DateOnly SunsetDate = new(2026, 8, 15);
        public static readonly Type OwnerFeature = typeof(PaymentV2Feature);

        public static bool IsEnabled(IFeatureFlagProvider provider)
            => provider.IsEnabled(Name, DefaultValue);

        public static bool IsEnabledForUser(
            IFeatureFlagProvider provider, string userId)
            => provider.IsEnabledPercentage(Name, userId, 25);
    }
}

docker-compose.override.dev.yaml: Stub service definitions for development:

# Auto-generated: docker-compose.override.dev.yaml
services:
  payment-gateway-stub:
    image: wiremock/wiremock:3.3.1
    volumes:
      - ./stubs/payment-gateway-responses.json:/home/wiremock/mappings/responses.json
    environment:
      - WIREMOCK_OPTIONS=--global-response-templating
    ports:
      - "8081:8080"
    labels:
      ops.stub: "payment-gateway"
      ops.behavior: "recorded-responses"
      ops.simulated-latency: "100ms-500ms"
      ops.simulated-rate-limit: "100"

Environment Diff Report: A Markdown report showing intentional and unintentional differences across environments:

# Environment Parity Report -- OrderServiceV3

## Schema Parity: dev / staging / production
Status: PASS (all environments at migration 47)

## Config Parity: dev / staging / production
Status: PASS (3 excluded keys: ConnectionStrings:*)
Excluded: ConnectionStrings:Default, ConnectionStrings:ReadReplica, ApiKeys:PaymentGateway

## Feature Flag Parity: staging / production
Status: WARNING (2 flags differ)
| Flag | Staging | Production | Intentional |
|------|---------|------------|-------------|
| new-checkout-flow | Enabled | Disabled | Yes (rollout plan) |
| payment-v2-api | Enabled (100%) | Enabled (25%) | Yes (gradual rollout) |

Analyzers

ENV001: Config Differs Without Exemption. A configuration key exists in production but not in staging (or vice versa), and it is not in the Exclusions list. This catches the "it works in staging" problem.

ENV002: Feature Flag Past Sunset Date. The SunsetDate on a feature flag has passed. The flag is still in the code. It should have been removed -- permanent feature flags are unmanaged complexity.

ENV003: Stub Used in Production. A [ServiceStub] includes "production" in its environments list. Stubs in production mean fake data in production. The analyzer blocks this unconditionally.



The Problem

The OrderService depends on Newtonsoft.Json 9.0.1, released in 2016. Nobody knows why it has not been updated. The NuGet restore warning has been there so long that everyone ignores it. The package has three known CVEs. The security scanner flags it every week. The team marks the finding as "accepted risk" every week. This has been happening for two years.

Meanwhile, the OrderService.V1Controller has a [Obsolete] attribute that was added 18 months ago. The attribute message says "Use V2Controller instead." The V2Controller was deployed 18 months ago. The V1Controller still receives 12% of traffic because three partner integrations have not migrated. Nobody tracks which partners are on V1. Nobody has a deadline for removing it. The V1 code path has diverged from V2 and now has its own bugs that only V1 users experience.

The Lifecycle DSL turns component aging into a compile-time concern.


SunsetSchedule

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
    AllowMultiple = true)]
public sealed class SunsetScheduleAttribute : Attribute
{
    public SunsetScheduleAttribute(string component,
        string deprecationDate, string sunsetDate) { }

    /// <summary>
    /// The replacement component. Null = no replacement (feature removal).
    /// </summary>
    public string? Replacement { get; init; }

    /// <summary>
    /// Link to the migration guide. Required if Replacement is set.
    /// </summary>
    public string? MigrationGuide { get; init; }

    /// <summary>
    /// Consumers that must migrate before sunset.
    /// Used for tracking and notification.
    /// </summary>
    public string[]? AffectedConsumers { get; init; }

    /// <summary>
    /// Whether to generate [Obsolete] attributes on the sunset component.
    /// </summary>
    public bool GenerateObsolete { get; init; } = true;
}

SupportWindow

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class SupportWindowAttribute : Attribute
{
    public SupportWindowAttribute(string version,
        string endOfLife) { }

    /// <summary>
    /// Current support level for this version.
    /// </summary>
    public SupportLevel Level { get; init; }
        = SupportLevel.Active;
}

public enum SupportLevel
{
    /// <summary>Full support. Bug fixes, features, security patches.</summary>
    Active,

    /// <summary>Bug fixes and security patches only. No new features.</summary>
    Maintenance,

    /// <summary>Security patches only. No bug fixes.</summary>
    SecurityOnly,

    /// <summary>No support. Use at your own risk. Migrate immediately.</summary>
    EndOfLife
}

TechDebtItem

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
    AllowMultiple = true)]
public sealed class TechDebtItemAttribute : Attribute
{
    public TechDebtItemAttribute(string description) { }

    /// <summary>
    /// Estimated effort to resolve. Format: "2d", "1w", "4h".
    /// </summary>
    public string? EstimatedEffort { get; init; }

    /// <summary>
    /// Deadline for resolution. Null = no deadline (tracked but not enforced).
    /// </summary>
    public string? Deadline { get; init; }

    /// <summary>
    /// Priority: Critical, High, Medium, Low.
    /// Critical tech debt past its deadline fails the build.
    /// </summary>
    public TechDebtPriority Priority { get; init; }
        = TechDebtPriority.Medium;

    /// <summary>
    /// Category for grouping in reports.
    /// </summary>
    public TechDebtCategory Category { get; init; }
        = TechDebtCategory.CodeQuality;

    /// <summary>
    /// Link to the tracking issue. Null = tracked only via this attribute.
    /// </summary>
    public string? TrackingIssue { get; init; }
}

public enum TechDebtPriority
{
    Critical,
    High,
    Medium,
    Low
}

public enum TechDebtCategory
{
    CodeQuality,
    DependencyUpdate,
    SecurityVulnerability,
    PerformanceOptimization,
    ArchitecturalRefactor,
    TestCoverage,
    DocumentationGap,
    DeprecatedApiUsage
}

Declaration Example

[SunsetSchedule("OrderController.V1",
    deprecationDate: "2025-06-01",
    sunsetDate: "2026-06-01",
    Replacement = "OrderController.V2",
    MigrationGuide = "docs/migration-v1-to-v2.md",
    AffectedConsumers = new[] { "partner-acme", "partner-globex",
                                 "partner-initech" },
    GenerateObsolete = true)]

[SupportWindow("v1", "2026-06-01",
    Level = SupportLevel.SecurityOnly)]

[SupportWindow("v2", "2027-06-01",
    Level = SupportLevel.Active)]

[SupportWindow("v3", "2028-06-01",
    Level = SupportLevel.Active)]

[TechDebtItem("Upgrade Newtonsoft.Json from 9.0.1 to 13.x",
    EstimatedEffort = "2d",
    Deadline = "2026-05-01",
    Priority = TechDebtPriority.Critical,
    Category = TechDebtCategory.SecurityVulnerability,
    TrackingIssue = "JIRA-4521")]

[TechDebtItem("Replace raw SQL queries with EF Core in OrderRepository",
    EstimatedEffort = "1w",
    Priority = TechDebtPriority.Medium,
    Category = TechDebtCategory.CodeQuality)]

[TechDebtItem("Add integration tests for payment retry logic",
    EstimatedEffort = "3d",
    Priority = TechDebtPriority.High,
    Category = TechDebtCategory.TestCoverage,
    Deadline = "2026-06-15")]

public partial class OrderServiceV3Ops { }

Generated Artifacts

Obsolete Attributes: The generator emits [Obsolete] attributes on sunset components:

// Auto-generated: SunsetObsolete.g.cs
#pragma warning disable CS0618

[Obsolete("OrderController.V1 is deprecated (2025-06-01) and will be "
    + "removed on 2026-06-01. Use OrderController.V2 instead. "
    + "Migration guide: docs/migration-v1-to-v2.md")]
public partial class OrderControllerV1 { }

The [Obsolete] attribute is not manually written and forgotten. It is generated from the [SunsetSchedule] attribute. When the sunset date passes, the analyzer upgrades from warning to error.

Lifecycle Dashboard: A generated Markdown report showing the lifecycle state of all components:

# Lifecycle Dashboard -- OrderServiceV3

## Component Lifecycle

| Component | Status | Deprecation | Sunset | Replacement |
|-----------|--------|-------------|--------|-------------|
| OrderController.V1 | SECURITY ONLY | 2025-06-01 | 2026-06-01 | V2 |
| OrderController.V2 | ACTIVE | - | 2027-06-01 | - |
| OrderController.V3 | ACTIVE | - | 2028-06-01 | - |

## Tech Debt Inventory

| Description | Priority | Effort | Deadline | Category | Status |
|-------------|----------|--------|----------|----------|--------|
| Upgrade Newtonsoft.Json 9.0.1 -> 13.x | CRITICAL | 2d | 2026-05-01 | Security | OVERDUE |
| Replace raw SQL in OrderRepository | Medium | 1w | - | Code Quality | Open |
| Add integration tests for payment retry | High | 3d | 2026-06-15 | Test Coverage | Open |

## Summary
- Components: 3 (1 security-only, 2 active)
- Tech debt items: 3 (1 critical/overdue, 1 high, 1 medium)
- Total estimated effort: 12 days
- Next sunset: OrderController.V1 on 2026-06-01 (56 days remaining)

Tech Debt Burndown Data: A JSON file that tracks tech debt over time, suitable for graphing:

{
  "snapshot_date": "2026-04-06",
  "total_items": 3,
  "total_effort_days": 12,
  "by_priority": {
    "critical": { "count": 1, "effort_days": 2 },
    "high": { "count": 1, "effort_days": 3 },
    "medium": { "count": 1, "effort_days": 7 }
  },
  "overdue": [
    {
      "description": "Upgrade Newtonsoft.Json from 9.0.1 to 13.x",
      "deadline": "2026-05-01",
      "days_overdue": 0,
      "priority": "critical"
    }
  ]
}

Analyzers

LFC001: Component Past Sunset Still Referenced. If the current date is past the sunset date and the component is still referenced in code, the analyzer fires an error. The component should have been removed. This is the "18-month-old [Obsolete] attribute" problem, enforced by the compiler.

LFC002: Dependency on End-of-Life Version. Cross-references [SupportWindow] with EndOfLife against actual dependencies. If a component depends on a version whose support level is EndOfLife, the build fails.

LFC003: Tech Debt Past Deadline. A [TechDebtItem] with Priority = Critical and a Deadline that has passed fails the build. Medium and Low priority items past deadline generate warnings. The team cannot ignore critical tech debt forever -- the compiler enforces the deadline they set for themselves.

LFC004: Sunset Without Migration Guide. A [SunsetSchedule] has a Replacement but no MigrationGuide. If you are telling consumers to migrate, you must tell them how.


ApiContract to Lifecycle

The ApiContract DSL's [ApiVersionPolicy] has a SunsetDate. The Lifecycle DSL's [SunsetSchedule] also has a SunsetDate. The analyzer validates consistency: if the API version 2 sunset date is September 2026, the component sunset date for V2Controller must be the same. Mismatched sunset dates mean someone will remove the code before the API is sunset (or sunset the API and leave dead code in the repo).

EnvironmentParity to Requirements

The [FeatureFlag] attribute takes a typeof() reference to a Requirements DSL feature. This creates a typed link: the feature flag exists because the feature exists. When the feature moves to "Done" in the Requirements DSL workflow, the feature flag should be removed (it has served its purpose). The analyzer cross-references: feature in "Done" state + feature flag still in code = ENV002 warning.

Lifecycle to Quality

The Quality DSL declares complexity budgets and coverage targets. The Lifecycle DSL declares tech debt. Cross-DSL integration: tech debt items in the CodeQuality category contribute to the quality budget. A method with [TechDebtItem(Category = CodeQuality)] gets a relaxed complexity threshold (because it is known debt being tracked) but a stricter deadline (because it is known debt that must be paid).


Three DSLs, One Theme

ApiContract governs how services talk to their consumers over time. EnvironmentParity governs how services behave across deployment targets. Lifecycle governs how services age and eventually die.

Together they answer three questions that every long-lived service must answer:

  1. Can I change this API? The ApiContract DSL tells you which consumers depend on which fields, whether a version bump is required, and when the old version sunsets.

  2. Will this work in production? The EnvironmentParity DSL tells you where your environments differ, whether those differences are intentional, and whether your feature flags have overstayed their welcome.

  3. When does this go away? The Lifecycle DSL tells you when components sunset, when support windows close, and how much tech debt is accumulating. It turns "we should really upgrade that library" into a compile-time deadline.

No wiki page answers these questions reliably. A wiki page about API versioning is stale the moment the next endpoint is added. A wiki page about feature flags is stale the moment a flag is toggled. A wiki page about tech debt is stale the moment someone says "we'll do it next sprint."

The attributes are the source of truth. The compiler enforces them. The generators produce the artifacts. The analyzers catch the violations. The wiki stays retired.

⬇ Download