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

Overview

The PLM DSL is the topmost layer of the lifecycle stack. It declares the things that customers, product managers and account executives care about: what is the product, what versions of it exist, what is on the roadmap, what stage of its lifecycle is it in, and when does each version reach end-of-life.

It is the only DSL in the lifecycle trio that is strictly build-time. Nothing in AcmeStore.Plm ships in any deployable artifact. Its source generator emits documentation and metadata files: a roadmap.md, a CHANGELOG.g.md, an ICS calendar of EOL dates, a JSON blob for the customer-facing version page. All consumed by tools, never by services.

The source generator produces:

  • A PlmRegistry of const strings (Plm.PRODUCT_*, Plm.RELEASE_*, Plm.MILESTONE_*)
  • A per-product roadmap.md aggregating [RoadmapItem] declarations and the Requirements.FEATURE_* they back
  • CHANGELOG.g.md per release, listing every feature shipped, every flag retired, every SLO that changed
  • A generated lifecycle-state-machine.g.cs per Product (Concept → Alpha → Beta → GA → Maintenance → EOL)
  • An eol-calendar.ics from [DeprecationPolicy] declarations
  • Roslyn analyzers PLM001-PLM100 enforcing roadmap traceability and build-time-only isolation

Product

namespace Cmf.Plm.Lib;

/// <summary>
/// A customer-addressable product or component. Owns a set of ALM
/// services (which themselves ship SDLC build targets), passes
/// through lifecycle stages, and may declare a deprecation policy
/// that will eventually emit an EOL date.
/// </summary>
[MetaConcept("Product")]
[MetaConstraint("MustHaveAtLeastOneService",
    "Services.Any()",
    Message = "Product must own at least one ALM Service")]
public sealed class ProductAttribute : Attribute
{
    [MetaProperty("Name", "string", Required = true)]
    public string Name { get; }

    [MetaProperty("Services", "string[]", Required = true)]
    public string[] Services { get; set; }

    [MetaProperty("Description", "string")]
    public string? Description { get; set; }

    public ProductAttribute(string name, string[] services)
    {
        Name = name;
        Services = services;
    }
}

Vision

/// <summary>
/// A versioned strategic statement attached to a Product. Visions
/// are dated and append-only -- replacing one creates a new entry,
/// the old one survives in the changelog.
/// </summary>
[MetaConcept("Vision")]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class VisionAttribute : Attribute
{
    [MetaProperty("Statement", "string", Required = true)]
    public string Statement { get; }

    [MetaProperty("AsOf", "DateOnly", Required = true)]
    public string AsOf { get; set; }

    public VisionAttribute(string statement, string asOf)
    {
        Statement = statement;
        AsOf = asOf;
    }
}

RoadmapItem

/// <summary>
/// A planned chunk of work. Must reference a Requirements DSL
/// feature -- the compiler refuses dangling roadmap items.
/// Optional Milestone groups items into customer-visible buckets.
/// </summary>
[MetaConcept("RoadmapItem")]
[MetaReference("Feature", "Requirement", Multiplicity = "1")]
[MetaConstraint("MustBackAFeature",
    "Feature != null",
    Message = "RoadmapItem must reference a [Feature] from the Requirements DSL")]
public sealed class RoadmapItemAttribute : Attribute
{
    [MetaProperty("Feature", "string", Required = true)]
    public string Feature { get; set; }

    [MetaProperty("Milestone", "string")]
    public string? Milestone { get; set; }

    [MetaProperty("Status", "RoadmapStatus")]
    public RoadmapStatus Status { get; set; } = RoadmapStatus.Planned;

    public RoadmapItemAttribute(string feature) => Feature = feature;
}

public enum RoadmapStatus { Planned, InProgress, Shipped, Cancelled }

Release

/// <summary>
/// A semver-versioned release of a Product. Pipeline references
/// the SDLC pipeline that cuts it; Channel references the SDLC
/// release channel it ships to. The compiler verifies that the
/// channel is allowed by the active branch policy.
/// </summary>
[MetaConcept("Release")]
[MetaReference("Pipeline", "Pipeline", Multiplicity = "1")]
[MetaConstraint("VersionMustBeSemver",
    "Regex.IsMatch(Version, '^\\d+\\.\\d+\\.\\d+(-[A-Za-z0-9.-]+)?$')",
    Message = "Release Version must be valid semver")]
public sealed class ReleaseAttribute : Attribute
{
    [MetaProperty("Product", "string", Required = true)]
    public string Product { get; }

    [MetaProperty("Version", "string", Required = true)]
    public string Version { get; set; }

    [MetaProperty("Date", "DateOnly", Required = true)]
    public string Date { get; set; }

    [MetaProperty("Pipeline", "string", Required = true)]
    public string Pipeline { get; set; }

    [MetaProperty("Channel", "ReleaseChannel", Required = true)]
    public ReleaseChannel Channel { get; set; }

    public ReleaseAttribute(string product, string version, string date,
                            string pipeline, ReleaseChannel channel)
    {
        Product = product;
        Version = version;
        Date = date;
        Pipeline = pipeline;
        Channel = channel;
    }
}

Milestone

/// <summary>
/// A dated bucket that aggregates RoadmapItems and Releases. The
/// generator emits one section per milestone in the per-product
/// roadmap.md.
/// </summary>
[MetaConcept("Milestone")]
public sealed class MilestoneAttribute : Attribute
{
    [MetaProperty("Name", "string", Required = true)]
    public string Name { get; }

    [MetaProperty("TargetDate", "DateOnly", Required = true)]
    public string TargetDate { get; set; }

    public MilestoneAttribute(string name, string targetDate)
    {
        Name = name;
        TargetDate = targetDate;
    }
}

LifecycleStage

/// <summary>
/// Where the Product currently sits on its lifecycle curve. The
/// compiler enforces forward-only transitions: Concept -> Alpha -> Beta
/// -> GA -> Maintenance -> EOL. Going backwards is a [Deprecation],
/// not a [LifecycleStage].
/// </summary>
[MetaConcept("LifecycleStage")]
[MetaConstraint("StageOrderingIsMonotonic",
    "this.Stage >= product.PreviousStage",
    Message = "LifecycleStage cannot go backwards")]
public sealed class LifecycleStageAttribute : Attribute
{
    [MetaProperty("Stage", "Stage", Required = true)]
    public Stage Stage { get; set; }

    public LifecycleStageAttribute(Stage stage) => Stage = stage;
}

public enum Stage { Concept, Alpha, Beta, GA, Maintenance, EOL }

DeprecationPolicy

/// <summary>
/// Declares the notice period customers receive before a Product
/// hits EOL. The generator computes the EOL date and emits it into
/// the eol-calendar.ics so that downstream tooling can publish
/// reminders.
/// </summary>
[MetaConcept("DeprecationPolicy")]
[MetaInherits("MetaConstraint")]
public sealed class DeprecationPolicyAttribute : Attribute
{
    [MetaProperty("NoticePeriodDays", "int", Required = true)]
    public int NoticePeriodDays { get; set; }

    [MetaProperty("PolicyUrl", "string")]
    public string? PolicyUrl { get; set; }

    public DeprecationPolicyAttribute(int noticePeriodDays)
        => NoticePeriodDays = noticePeriodDays;
}

What the developer writes

namespace AcmeStore.Plm;

[Product("Store",
    Services = new[] { Alm.SERVICE_API, Alm.SERVICE_WORKER, Alm.SERVICE_WEB },
    Description = "Public e-commerce storefront")]
[Vision("Become the easiest checkout experience on the web", AsOf = "2026-01-15")]
[LifecycleStage(Stage.GA)]
[DeprecationPolicy(NoticePeriodDays = 180,
    PolicyUrl = "https://acmestore.example/legal/deprecation")]
public partial class StoreProduct { }

// ── Milestones ──
[Milestone("Q2-2026", TargetDate = "2026-06-30")]
public partial class Q2_2026_Milestone { }

[Milestone("Q3-2026", TargetDate = "2026-09-30")]
public partial class Q3_2026_Milestone { }

// ── Roadmap ──
[RoadmapItem(Requirements.FEATURE_USER_ROLES,
    Milestone = "Q2-2026", Status = RoadmapStatus.Shipped)]
public partial class RM_UserRoles { }

[RoadmapItem(Requirements.FEATURE_NEW_CHECKOUT,
    Milestone = "Q2-2026", Status = RoadmapStatus.InProgress)]
public partial class RM_NewCheckout { }

[RoadmapItem(Requirements.FEATURE_LOYALTY_PROGRAM,
    Milestone = "Q3-2026", Status = RoadmapStatus.Planned)]
public partial class RM_Loyalty { }

// ── Releases ──
[Release("Store",
    Version = "2.3.0",
    Date = "2026-03-15",
    Pipeline = Sdlc.PIPELINE_MAIN,
    Channel = ReleaseChannel.Stable)]
public partial class Store_2_3_0 { }

[Release("Store",
    Version = "2.4.0-preview.1",
    Date = "2026-04-01",
    Pipeline = Sdlc.PIPELINE_MAIN,
    Channel = ReleaseChannel.Preview)]
public partial class Store_2_4_0_preview { }

Two cross-DSL guarantees the compiler enforces here:

  1. Alm.SERVICE_API exists and points at a valid [Service]. Removing the service in AcmeStore.Alm breaks StoreProduct at compile time.
  2. Requirements.FEATURE_NEW_CHECKOUT exists and points at a valid [Feature]. Removing the feature in AcmeStore.Requirements breaks the roadmap item at compile time. Roadmaps cannot lie.

1. PlmRegistry

// Generated: PlmRegistry.g.cs
namespace AcmeStore.Plm;

public static class Plm
{
    public const string PRODUCT_STORE = "AcmeStore.Plm.StoreProduct";

    public const string RELEASE_STORE_2_3_0         = "Store::2.3.0";
    public const string RELEASE_STORE_2_4_0_PREVIEW = "Store::2.4.0-preview.1";

    public const string MILESTONE_Q2_2026 = "Q2-2026";
    public const string MILESTONE_Q3_2026 = "Q3-2026";
}

2. roadmap.md (per product)

<!-- Generated: docs/products/store/roadmap.md -->
# Store -- Roadmap

> Become the easiest checkout experience on the web (vision as of 2026-01-15)

## Q2-2026 (target: 2026-06-30)

| Status      | Feature                              | Requirement ID    |
|-------------|--------------------------------------|-------------------|
| ✓ Shipped   | User roles & permissions             | FEATURE_USER_ROLES |
| ◐ In progress | New checkout flow                  | FEATURE_NEW_CHECKOUT |

## Q3-2026 (target: 2026-09-30)

| Status      | Feature              | Requirement ID         |
|-------------|----------------------|------------------------|
| · Planned   | Loyalty program      | FEATURE_LOYALTY_PROGRAM |

3. CHANGELOG.g.md (per release)

<!-- Generated: docs/products/store/CHANGELOG.md -->
# Store CHANGELOG

## 2.4.0-preview.1 -- 2026-04-01 (Preview)

Pipeline: `MainBuild`

### In progress
- New checkout flow (FEATURE_NEW_CHECKOUT)
  - Behind feature flag `NewCheckout` (canary 10%)

## 2.3.0 -- 2026-03-15 (Stable)

Pipeline: `MainBuild`

### Shipped features
- User roles & permissions (FEATURE_USER_ROLES)

### Retired flags
- `LegacyAuth` -- removed after migration complete

4. Lifecycle state machine

// Generated: StoreProductLifecycle.g.cs
public enum StoreProductLifecycleStage
{
    Concept, Alpha, Beta, GA, Maintenance, EOL
}

public static class StoreProductLifecycle
{
    public static StoreProductLifecycleStage Current
        => StoreProductLifecycleStage.GA;

    private static readonly (StoreProductLifecycleStage From, StoreProductLifecycleStage To)[] Transitions =
    {
        (StoreProductLifecycleStage.Concept,     StoreProductLifecycleStage.Alpha),
        (StoreProductLifecycleStage.Alpha,       StoreProductLifecycleStage.Beta),
        (StoreProductLifecycleStage.Beta,        StoreProductLifecycleStage.GA),
        (StoreProductLifecycleStage.GA,          StoreProductLifecycleStage.Maintenance),
        (StoreProductLifecycleStage.Maintenance, StoreProductLifecycleStage.EOL),
    };

    public static bool IsValidTransition(
        StoreProductLifecycleStage from, StoreProductLifecycleStage to)
        => Transitions.Any(t => t.From == from && t.To == to);
}
Diagram
The monotonic product lifecycle enforced by PLM005: a product can only move forward through Concept, Alpha, Beta, GA, Maintenance and EOL, never skip stages or regress.

5. EOL calendar

BEGIN:VCALENDAR
PRODID:-//AcmeStore//PLM DSL//EN
VERSION:2.0
BEGIN:VEVENT
UID:store-eol@acmestore
SUMMARY:Store -- EOL notice window opens
DTSTART;VALUE=DATE:20261001
DTEND;VALUE=DATE:20261001
DESCRIPTION:NoticePeriod=180 days. Policy: https://acmestore.example/legal/deprecation
END:VEVENT
END:VCALENDAR

The EOL date is computed from LifecycleStage.EOL-bearing releases plus DeprecationPolicy.NoticePeriodDays. As long as the product is in GA or Maintenance, the calendar event slides forward automatically; cutting an explicit [LifecycleStage(Stage.EOL)] release pins it.

6. Generation pipeline

Diagram
The PLM generation pipeline: a single pass of cross-DSL link checking feeds registry, roadmap, changelogs, state machine, EOL calendar and the PLM100 build-time guard.

Cross-DSL references

PLM is the apex of the lifecycle stack and references everything below it:

  • [Product(Services = ...)] → ALM Alm.SERVICE_*
  • [RoadmapItem(Feature = ...)] → Requirements DSL Requirements.FEATURE_*
  • [Release(Pipeline = ...)] → SDLC Sdlc.PIPELINE_*
  • [Release(Channel = ...)] → SDLC ReleaseChannel enum

Reverse references are forbidden. SDLC, ALM, Requirements and the domain do not reference PLM. The compiler enforces this with analyzer PLM100.


Analyzers, including the build-time-only guard

ID Severity Message
PLM001 Error RoadmapItem {R} references unknown Feature {F}
PLM002 Error Release {R} declares GA without a prior Beta release
PLM003 Error Release {R} Channel {C} is not allowed by active branch policy
PLM004 Warning Product {P} has Vision older than 12 months -- consider a refresh
PLM005 Error LifecycleStage transition violates monotonic ordering
PLM100 Error Project {X} (any non-tooling layer) must not reference AcmeStore.Plm. PLM is build-time only. Use the generated artifacts (roadmap.md, CHANGELOG.g.md) instead

PLM100 is even stricter than its ALM cousin: it forbids any runtime project from referencing the PLM assembly, including the AppHost. The only legal consumers are the PLM tooling CLI and other PLM projects. This is what allows PLM declarations to carry forward-looking, customer-sensitive information (vision statements, internal roadmap items) without that information ever ending up in a deployed binary that a curious user could decompile.


Why a separate DSL instead of PLM-as-comments

The naive approach is to put product information in /// doc comments and harvest them with a script. Three problems with that approach:

  1. No type safety. Comments cannot reference Sdlc.PIPELINE_MAIN. They reference free text. They drift.
  2. No constraint enforcement. Comments cannot fail the build when a release ships GA without a prior Beta. They are inert.
  3. Mixed concerns. Putting customer-facing roadmap items in source comments leaks them into generated documentation pages by accident.

The PLM DSL gives PMs and product owners a first-class surface in the codebase that is checked by the same compiler that checks the production code, while remaining strictly out of the runtime dependency graph. The next chapter (Part XVIII -- AcmeStore Walkthrough) shows the three lifecycle DSLs working together end-to-end with a full dependency-inversion diagram and feature-flag walkthrough.

⬇ Download