Part II: CMF Feature Catalog -- Requirements as Code
About This Chapter
This chapter expresses the CMF's own requirements using the Requirements DSL notation from Part IX. Every epic, feature, story, and task is a typed C# record with abstract acceptance criterion methods. This serves two purposes:
- Specification: It defines exactly what the CMF must do -- a complete product backlog
- Validation: It stress-tests the Requirements DSL design against a non-trivial, real-world project
The Bootstrap Question
This chapter uses the Requirements DSL notation, but the DSL does not exist yet. We are writing abstract record UserRolesFeature : Feature<PlatformScalabilityEpic> before the Feature<T> base type is implemented. This is not self-hosting -- it is design validation. True self-hosting (the CMF using its own tooling to track its own requirements) is a future milestone that becomes possible once the CMF is built.
Catalog Overview
Epic 1: DomainModelingEpic
namespace Cmf.Requirements.Epics;
public abstract record DomainModelingEpic : Epic
{
public override string Title => "Domain Modeling & Persistence Generation";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "framework-team";
}namespace Cmf.Requirements.Epics;
public abstract record DomainModelingEpic : Epic
{
public override string Title => "Domain Modeling & Persistence Generation";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "framework-team";
}Feature 1.1: AggregateDefinition
namespace Cmf.Requirements.Features;
/// <summary>
/// Define aggregate roots, entities, and value objects using DDD attributes.
/// Source generators produce complete domain implementations.
/// </summary>
public abstract record AggregateDefinitionFeature : Feature<DomainModelingEpic>
{
public override string Title => "Aggregate root, entity, and value object definition";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "framework-team";
/// <summary>
/// A developer can declare a class with [AggregateRoot] and [EntityId],
/// and the compiler produces a complete entity implementation with backing fields.
/// </summary>
public abstract AcceptanceCriterionResult DeveloperCanDefineAggregateRoot(
string className, string idPropertyName);
/// <summary>
/// A developer can add [Entity] classes with [Composition] references,
/// and the compiler validates that every entity is reachable from its aggregate root.
/// </summary>
public abstract AcceptanceCriterionResult ComposedEntitiesAreValidated(
string aggregateRootClass, string[] entityClasses);
/// <summary>
/// A developer can add [ValueObject] types with [ValueComponent] properties,
/// and the compiler generates immutable value objects with structural equality.
/// </summary>
public abstract AcceptanceCriterionResult ValueObjectsAreImmutable(
string valueObjectClass, string[] componentNames);
/// <summary>
/// [Association] references across aggregate boundaries are validated:
/// no cascade delete, eventual consistency enforced.
/// </summary>
public abstract AcceptanceCriterionResult CrossAggregateAssociationsAreEventuallyConsistent(
string sourceAggregate, string targetAggregate);
}namespace Cmf.Requirements.Features;
/// <summary>
/// Define aggregate roots, entities, and value objects using DDD attributes.
/// Source generators produce complete domain implementations.
/// </summary>
public abstract record AggregateDefinitionFeature : Feature<DomainModelingEpic>
{
public override string Title => "Aggregate root, entity, and value object definition";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "framework-team";
/// <summary>
/// A developer can declare a class with [AggregateRoot] and [EntityId],
/// and the compiler produces a complete entity implementation with backing fields.
/// </summary>
public abstract AcceptanceCriterionResult DeveloperCanDefineAggregateRoot(
string className, string idPropertyName);
/// <summary>
/// A developer can add [Entity] classes with [Composition] references,
/// and the compiler validates that every entity is reachable from its aggregate root.
/// </summary>
public abstract AcceptanceCriterionResult ComposedEntitiesAreValidated(
string aggregateRootClass, string[] entityClasses);
/// <summary>
/// A developer can add [ValueObject] types with [ValueComponent] properties,
/// and the compiler generates immutable value objects with structural equality.
/// </summary>
public abstract AcceptanceCriterionResult ValueObjectsAreImmutable(
string valueObjectClass, string[] componentNames);
/// <summary>
/// [Association] references across aggregate boundaries are validated:
/// no cascade delete, eventual consistency enforced.
/// </summary>
public abstract AcceptanceCriterionResult CrossAggregateAssociationsAreEventuallyConsistent(
string sourceAggregate, string targetAggregate);
}Story 1.1.1: DefineAggregateRoot
public abstract record DefineAggregateRootStory : Story<AggregateDefinitionFeature>
{
public override string Title => "Define an aggregate root with EntityId";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult PartialClassWithAggregateRootAttribute(
string className);
public abstract AcceptanceCriterionResult EntityIdPropertyIsRequired(
string className, string idType);
public abstract AcceptanceCriterionResult CompilerErrorIfNoEntityId(
string className);
}public abstract record DefineAggregateRootStory : Story<AggregateDefinitionFeature>
{
public override string Title => "Define an aggregate root with EntityId";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult PartialClassWithAggregateRootAttribute(
string className);
public abstract AcceptanceCriterionResult EntityIdPropertyIsRequired(
string className, string idType);
public abstract AcceptanceCriterionResult CompilerErrorIfNoEntityId(
string className);
}Tasks:
- Task 1.1.1.1: Implement
[AggregateRoot]attribute with MetaConcept registration (4h) - Task 1.1.1.2: Implement
[EntityId]attribute with strongly-typed ID generation (3h) - Task 1.1.1.3: Stage 1 validator: reject aggregate without EntityId (2h)
- Task 1.1.1.4: Stage 2 generator: emit backing fields and property implementations (6h)
Story 1.1.2: AddComposition
public abstract record AddCompositionStory : Story<AggregateDefinitionFeature>
{
public override string Title => "Add composition relationships between entities";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult SingleEntityComposition(
string parentClass, string childClass);
public abstract AcceptanceCriterionResult CollectionEntityComposition(
string parentClass, string childCollectionType);
public abstract AcceptanceCriterionResult ValueObjectCompositionEmbedsColumns(
string parentClass, string valueObjectClass);
public abstract AcceptanceCriterionResult CascadeDeleteOnComposition(
string parentClass, string childClass);
}public abstract record AddCompositionStory : Story<AggregateDefinitionFeature>
{
public override string Title => "Add composition relationships between entities";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult SingleEntityComposition(
string parentClass, string childClass);
public abstract AcceptanceCriterionResult CollectionEntityComposition(
string parentClass, string childCollectionType);
public abstract AcceptanceCriterionResult ValueObjectCompositionEmbedsColumns(
string parentClass, string valueObjectClass);
public abstract AcceptanceCriterionResult CascadeDeleteOnComposition(
string parentClass, string childClass);
}Tasks:
- Task 1.1.2.1: Implement
[Composition]attribute for single entity references (3h) - Task 1.1.2.2: Implement
[Composition]forIReadOnlyList<T>collections (4h) - Task 1.1.2.3: Implement
[Composition]for value objects (OwnsOne/OwnsMany) (4h) - Task 1.1.2.4: Stage 1 validator: verify all entities reachable via composition (3h)
- Task 1.1.2.5: Stage 2 generator: emit EF Core
HasOne/HasManywith cascade delete (5h)
Story 1.1.3: GenerateEfCore
public abstract record GenerateEfCoreStory : Story<AggregateDefinitionFeature>
{
public override string Title => "Generate EF Core persistence from aggregate model";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult DbContextIncludesAllAggregates(
string[] aggregateNames);
public abstract AcceptanceCriterionResult RepositoryInterfaceGenerated(
string aggregateName);
public abstract AcceptanceCriterionResult RepositoryImplementationGenerated(
string aggregateName);
public abstract AcceptanceCriterionResult MigrationScriptGenerable(
string aggregateName);
}public abstract record GenerateEfCoreStory : Story<AggregateDefinitionFeature>
{
public override string Title => "Generate EF Core persistence from aggregate model";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult DbContextIncludesAllAggregates(
string[] aggregateNames);
public abstract AcceptanceCriterionResult RepositoryInterfaceGenerated(
string aggregateName);
public abstract AcceptanceCriterionResult RepositoryImplementationGenerated(
string aggregateName);
public abstract AcceptanceCriterionResult MigrationScriptGenerable(
string aggregateName);
}Tasks:
- Task 1.1.3.1: Stage 2 generator: emit
DbContextwithDbSet<T>per aggregate (4h) - Task 1.1.3.2: Stage 2 generator: emit
IEntityTypeConfiguration<T>per entity (6h) - Task 1.1.3.3: Stage 2 generator: emit
IRepository<T>interface (2h) - Task 1.1.3.4: Stage 2 generator: emit repository implementation with
SaveChangesAsync(4h) - Task 1.1.3.5: Integration with
dotnet ef migrations add(3h)
Feature 1.2: CqrsGeneration
public abstract record CqrsGenerationFeature : Feature<DomainModelingEpic>
{
public override string Title => "CQRS command, event, query, and saga generation";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult CommandTargetsAggregate(
string commandName, string aggregateName);
public abstract AcceptanceCriterionResult EventSourcedFromAggregate(
string eventName, string aggregateName);
public abstract AcceptanceCriterionResult HandlerGeneratedPerCommand(
string commandName);
public abstract AcceptanceCriterionResult SagaOrchestratesMultipleAggregates(
string sagaName, string[] aggregateNames);
public abstract AcceptanceCriterionResult InvariantsCalledAfterCommand(
string commandName, string aggregateName);
}public abstract record CqrsGenerationFeature : Feature<DomainModelingEpic>
{
public override string Title => "CQRS command, event, query, and saga generation";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult CommandTargetsAggregate(
string commandName, string aggregateName);
public abstract AcceptanceCriterionResult EventSourcedFromAggregate(
string eventName, string aggregateName);
public abstract AcceptanceCriterionResult HandlerGeneratedPerCommand(
string commandName);
public abstract AcceptanceCriterionResult SagaOrchestratesMultipleAggregates(
string sagaName, string[] aggregateNames);
public abstract AcceptanceCriterionResult InvariantsCalledAfterCommand(
string commandName, string aggregateName);
}Stories for CqrsGeneration:
- Story 1.2.1: DefineCommand --
[Command]attribute with target aggregate, generated handler that callsEnsureInvariants()after mutation - Story 1.2.2: DefineDomainEvent --
[DomainEvent]attribute with source aggregate, published after successful command - Story 1.2.3: DefineQuery --
[Query]attribute with projection from aggregate - Story 1.2.4: DefineSaga --
[Saga]attribute orchestrating multi-aggregate processes with compensation
Feature 1.3: BuilderGeneration
public abstract record BuilderGenerationFeature : Feature<DomainModelingEpic>
{
public override string Title => "Fluent builder generation for aggregates and entities";
public override RequirementPriority Priority => RequirementPriority.Medium;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult FluentBuilderGeneratedPerAggregate(
string aggregateName);
public abstract AcceptanceCriterionResult BuilderEnforcesRequiredProperties(
string aggregateName, string[] requiredProperties);
public abstract AcceptanceCriterionResult BuilderProducesImmutableInstance(
string aggregateName);
public abstract AcceptanceCriterionResult BuilderCallsEnsureInvariants(
string aggregateName);
}public abstract record BuilderGenerationFeature : Feature<DomainModelingEpic>
{
public override string Title => "Fluent builder generation for aggregates and entities";
public override RequirementPriority Priority => RequirementPriority.Medium;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult FluentBuilderGeneratedPerAggregate(
string aggregateName);
public abstract AcceptanceCriterionResult BuilderEnforcesRequiredProperties(
string aggregateName, string[] requiredProperties);
public abstract AcceptanceCriterionResult BuilderProducesImmutableInstance(
string aggregateName);
public abstract AcceptanceCriterionResult BuilderCallsEnsureInvariants(
string aggregateName);
}Feature 1.4: InvariantEnforcement
public abstract record InvariantEnforcementFeature : Feature<DomainModelingEpic>
{
public override string Title => "Aggregate invariant enforcement via [Invariant] methods";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "framework-team";
/// <summary>
/// Methods marked [Invariant] return Result and are discovered by the generator.
/// </summary>
public abstract AcceptanceCriterionResult InvariantMethodsReturnResult(
string aggregateName, string[] invariantMethodNames);
/// <summary>
/// Generator emits EnsureInvariants() that calls all [Invariant] methods
/// and aggregates their Results.
/// </summary>
public abstract AcceptanceCriterionResult EnsureInvariantsAggregatesResults(
string aggregateName, int expectedInvariantCount);
/// <summary>
/// EnsureInvariants() is injected into every generated command handler
/// between state mutation and SaveChanges.
/// </summary>
public abstract AcceptanceCriterionResult InvariantsCalledInCommandHandlers(
string commandName);
/// <summary>
/// Analyzer DDD100 warns if an aggregate has zero [Invariant] methods.
/// </summary>
public abstract AcceptanceCriterionResult AnalyzerWarnsOnMissingInvariants(
string aggregateName);
}public abstract record InvariantEnforcementFeature : Feature<DomainModelingEpic>
{
public override string Title => "Aggregate invariant enforcement via [Invariant] methods";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "framework-team";
/// <summary>
/// Methods marked [Invariant] return Result and are discovered by the generator.
/// </summary>
public abstract AcceptanceCriterionResult InvariantMethodsReturnResult(
string aggregateName, string[] invariantMethodNames);
/// <summary>
/// Generator emits EnsureInvariants() that calls all [Invariant] methods
/// and aggregates their Results.
/// </summary>
public abstract AcceptanceCriterionResult EnsureInvariantsAggregatesResults(
string aggregateName, int expectedInvariantCount);
/// <summary>
/// EnsureInvariants() is injected into every generated command handler
/// between state mutation and SaveChanges.
/// </summary>
public abstract AcceptanceCriterionResult InvariantsCalledInCommandHandlers(
string commandName);
/// <summary>
/// Analyzer DDD100 warns if an aggregate has zero [Invariant] methods.
/// </summary>
public abstract AcceptanceCriterionResult AnalyzerWarnsOnMissingInvariants(
string aggregateName);
}Stories for InvariantEnforcement:
- Story 1.4.1: DefineInvariant --
[Invariant("description")]onprivate Result Method()(2h) - Story 1.4.2: GenerateEnsureInvariants -- Source generator discovers
[Invariant]methods, emitsEnsureInvariants()(4h) - Story 1.4.3: InjectIntoHandlers -- Generated command handlers call
EnsureInvariants()after mutation (3h) - Story 1.4.4: DDD100Analyzer -- Roslyn analyzer warns on aggregates without invariants (2h)
Feature 1.5: ValidationGeneration
public abstract record ValidationGenerationFeature : Feature<DomainModelingEpic>
{
public override string Title => "Property validation and cross-entity rule generation";
public override RequirementPriority Priority => RequirementPriority.Medium;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult RequiredPropertiesValidated(
string entityName, string[] requiredPropertyNames);
public abstract AcceptanceCriterionResult StringLengthConstraintsEnforced(
string entityName, string propertyName, int maxLength);
public abstract AcceptanceCriterionResult CustomValidationRulesSupported(
string entityName, string validationExpression);
}public abstract record ValidationGenerationFeature : Feature<DomainModelingEpic>
{
public override string Title => "Property validation and cross-entity rule generation";
public override RequirementPriority Priority => RequirementPriority.Medium;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult RequiredPropertiesValidated(
string entityName, string[] requiredPropertyNames);
public abstract AcceptanceCriterionResult StringLengthConstraintsEnforced(
string entityName, string propertyName, int maxLength);
public abstract AcceptanceCriterionResult CustomValidationRulesSupported(
string entityName, string validationExpression);
}Epic 2: ContentManagementEpic
public abstract record ContentManagementEpic : Epic
{
public override string Title => "Content Parts, Blocks, and StreamFields";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
}public abstract record ContentManagementEpic : Epic
{
public override string Title => "Content Parts, Blocks, and StreamFields";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
}Feature 2.1: ContentParts
public abstract record ContentPartsFeature : Feature<ContentManagementEpic>
{
public override string Title => "Composable content parts (horizontal composition)";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult PartCanBeDefinedWithAttributes(
string partName, string[] fieldNames);
public abstract AcceptanceCriterionResult PartCanBeAttachedToEntity(
string entityName, string partName);
public abstract AcceptanceCriterionResult MultiplePartsOnSameEntity(
string entityName, string[] partNames);
public abstract AcceptanceCriterionResult PartFieldsIncludedInAdminForm(
string partName, string[] fieldNames);
public abstract AcceptanceCriterionResult PartVersioningSupported(
string partName);
}public abstract record ContentPartsFeature : Feature<ContentManagementEpic>
{
public override string Title => "Composable content parts (horizontal composition)";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult PartCanBeDefinedWithAttributes(
string partName, string[] fieldNames);
public abstract AcceptanceCriterionResult PartCanBeAttachedToEntity(
string entityName, string partName);
public abstract AcceptanceCriterionResult MultiplePartsOnSameEntity(
string entityName, string[] partNames);
public abstract AcceptanceCriterionResult PartFieldsIncludedInAdminForm(
string partName, string[] fieldNames);
public abstract AcceptanceCriterionResult PartVersioningSupported(
string partName);
}Stories:
- Story 2.1.1: DefineContentPart --
[ContentPart]with[PartField]attributes - Story 2.1.2: AttachPartToEntity --
[HasPart(typeof(Routable))]on aggregate - Story 2.1.3: BuiltInParts -- Routable, Seoable, Taggable, Auditable, Versionable
- Story 2.1.4: PartPersistence -- EF Core owned types or JSON columns for part data
Feature 2.2: ContentBlocks
public abstract record ContentBlocksFeature : Feature<ContentManagementEpic>
{
public override string Title => "Structured content blocks (vertical composition)";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult StructBlockCanBeDefined(
string blockName, string[] fieldNames);
public abstract AcceptanceCriterionResult ListBlockSupportsMultipleItems(
string blockName, string childBlockType);
public abstract AcceptanceCriterionResult BlocksCanBeNested(
string parentBlockName, string childBlockName);
public abstract AcceptanceCriterionResult BlocksSerializeToJson(
string blockName);
public abstract AcceptanceCriterionResult BlocksRenderInBlazer(
string blockName);
}public abstract record ContentBlocksFeature : Feature<ContentManagementEpic>
{
public override string Title => "Structured content blocks (vertical composition)";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult StructBlockCanBeDefined(
string blockName, string[] fieldNames);
public abstract AcceptanceCriterionResult ListBlockSupportsMultipleItems(
string blockName, string childBlockType);
public abstract AcceptanceCriterionResult BlocksCanBeNested(
string parentBlockName, string childBlockName);
public abstract AcceptanceCriterionResult BlocksSerializeToJson(
string blockName);
public abstract AcceptanceCriterionResult BlocksRenderInBlazer(
string blockName);
}Feature 2.3: StreamFields
public abstract record StreamFieldsFeature : Feature<ContentManagementEpic>
{
public override string Title => "StreamField composable block sequences";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult StreamFieldAcceptsMultipleBlockTypes(
string fieldName, string[] allowedBlockTypes);
public abstract AcceptanceCriterionResult BlockOrderIsPreserved(
string fieldName, string[] blockTypesInOrder);
public abstract AcceptanceCriterionResult StreamFieldSerializesToJsonArray(
string fieldName);
public abstract AcceptanceCriterionResult StreamFieldRenderableInBlazor(
string fieldName);
}public abstract record StreamFieldsFeature : Feature<ContentManagementEpic>
{
public override string Title => "StreamField composable block sequences";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult StreamFieldAcceptsMultipleBlockTypes(
string fieldName, string[] allowedBlockTypes);
public abstract AcceptanceCriterionResult BlockOrderIsPreserved(
string fieldName, string[] blockTypesInOrder);
public abstract AcceptanceCriterionResult StreamFieldSerializesToJsonArray(
string fieldName);
public abstract AcceptanceCriterionResult StreamFieldRenderableInBlazor(
string fieldName);
}Epic 3: AdminGenerationEpic
public abstract record AdminGenerationEpic : Epic
{
public override string Title => "Auto-Generated Admin Interfaces";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
}public abstract record AdminGenerationEpic : Epic
{
public override string Title => "Auto-Generated Admin Interfaces";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
}Feature 3.1: ListGeneration
public abstract record ListGenerationFeature : Feature<AdminGenerationEpic>
{
public override string Title => "Paginated list views with filtering and sorting";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult ListViewGeneratedPerAdminModule(
string moduleName);
public abstract AcceptanceCriterionResult PaginationWorks(
string moduleName, int pageSize);
public abstract AcceptanceCriterionResult FiltersApplyToColumns(
string moduleName, string[] filterableColumns);
public abstract AcceptanceCriterionResult SortingWorksOnAllColumns(
string moduleName);
}public abstract record ListGenerationFeature : Feature<AdminGenerationEpic>
{
public override string Title => "Paginated list views with filtering and sorting";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult ListViewGeneratedPerAdminModule(
string moduleName);
public abstract AcceptanceCriterionResult PaginationWorks(
string moduleName, int pageSize);
public abstract AcceptanceCriterionResult FiltersApplyToColumns(
string moduleName, string[] filterableColumns);
public abstract AcceptanceCriterionResult SortingWorksOnAllColumns(
string moduleName);
}Feature 3.2: FormGeneration
public abstract record FormGenerationFeature : Feature<AdminGenerationEpic>
{
public override string Title => "Create/edit forms with validation and nested entities";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult FormIncludesAllProperties(
string aggregateName, string[] propertyNames);
public abstract AcceptanceCriterionResult ValidationErrorsDisplayed(
string aggregateName, string invalidPropertyName);
public abstract AcceptanceCriterionResult NestedEntityEditing(
string aggregateName, string nestedEntityName);
public abstract AcceptanceCriterionResult ContentPartFieldsInForm(
string aggregateName, string[] attachedPartNames);
}public abstract record FormGenerationFeature : Feature<AdminGenerationEpic>
{
public override string Title => "Create/edit forms with validation and nested entities";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult FormIncludesAllProperties(
string aggregateName, string[] propertyNames);
public abstract AcceptanceCriterionResult ValidationErrorsDisplayed(
string aggregateName, string invalidPropertyName);
public abstract AcceptanceCriterionResult NestedEntityEditing(
string aggregateName, string nestedEntityName);
public abstract AcceptanceCriterionResult ContentPartFieldsInForm(
string aggregateName, string[] attachedPartNames);
}Feature 3.3: BatchActions
public abstract record BatchActionsFeature : Feature<AdminGenerationEpic>
{
public override string Title => "Bulk operations with confirmation dialogs";
public override RequirementPriority Priority => RequirementPriority.Medium;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult BulkDeleteWithConfirmation(
string moduleName, int selectedCount);
public abstract AcceptanceCriterionResult BulkPublishAction(
string moduleName);
public abstract AcceptanceCriterionResult CustomBulkActions(
string moduleName, string actionName);
}public abstract record BatchActionsFeature : Feature<AdminGenerationEpic>
{
public override string Title => "Bulk operations with confirmation dialogs";
public override RequirementPriority Priority => RequirementPriority.Medium;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult BulkDeleteWithConfirmation(
string moduleName, int selectedCount);
public abstract AcceptanceCriterionResult BulkPublishAction(
string moduleName);
public abstract AcceptanceCriterionResult CustomBulkActions(
string moduleName, string actionName);
}Epic 4: PageCompositionEpic
public abstract record PageCompositionEpic : Epic
{
public override string Title => "Dynamic Page Composition & Routing";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
}public abstract record PageCompositionEpic : Epic
{
public override string Title => "Dynamic Page Composition & Routing";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
}Feature 4.1: PageTree
public abstract record PageTreeFeature : Feature<PageCompositionEpic>
{
public override string Title => "Hierarchical page tree with slugs and materialized paths";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult PagesFormHierarchy(
string[] pagePaths);
public abstract AcceptanceCriterionResult SlugsAutoGenerated(
string pageTitle, string expectedSlug);
public abstract AcceptanceCriterionResult MaterializedPathsComputed(
string pagePath, string expectedMaterializedPath);
public abstract AcceptanceCriterionResult PageMovingUpdatesDescendants(
string movedPagePath, string newParentPath);
}public abstract record PageTreeFeature : Feature<PageCompositionEpic>
{
public override string Title => "Hierarchical page tree with slugs and materialized paths";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult PagesFormHierarchy(
string[] pagePaths);
public abstract AcceptanceCriterionResult SlugsAutoGenerated(
string pageTitle, string expectedSlug);
public abstract AcceptanceCriterionResult MaterializedPathsComputed(
string pagePath, string expectedMaterializedPath);
public abstract AcceptanceCriterionResult PageMovingUpdatesDescendants(
string movedPagePath, string newParentPath);
}Feature 4.2: WidgetSystem
public abstract record WidgetSystemFeature : Feature<PageCompositionEpic>
{
public override string Title => "Composable widgets placed in zones on layouts";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult WidgetDefinedWithAttribute(
string widgetName, string[] configProperties);
public abstract AcceptanceCriterionResult WidgetPlaceableInZone(
string widgetName, string zoneName);
public abstract AcceptanceCriterionResult WidgetRendersInBlazorWasm(
string widgetName);
public abstract AcceptanceCriterionResult WidgetConfigEditableInAdmin(
string widgetName);
}public abstract record WidgetSystemFeature : Feature<PageCompositionEpic>
{
public override string Title => "Composable widgets placed in zones on layouts";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult WidgetDefinedWithAttribute(
string widgetName, string[] configProperties);
public abstract AcceptanceCriterionResult WidgetPlaceableInZone(
string widgetName, string zoneName);
public abstract AcceptanceCriterionResult WidgetRendersInBlazorWasm(
string widgetName);
public abstract AcceptanceCriterionResult WidgetConfigEditableInAdmin(
string widgetName);
}Feature 4.3: DynamicRouting
public abstract record DynamicRoutingFeature : Feature<PageCompositionEpic>
{
public override string Title => "URL resolution from page tree with SEO support";
public override RequirementPriority Priority => RequirementPriority.Medium;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult UrlResolvesToPage(
string url, string expectedPageTitle);
public abstract AcceptanceCriterionResult BoundEntityUrlsWork(
string entityType, string entitySlug, string expectedUrl);
public abstract AcceptanceCriterionResult SeoMetadataGenerated(
string pageTitle, string expectedCanonicalUrl);
}public abstract record DynamicRoutingFeature : Feature<PageCompositionEpic>
{
public override string Title => "URL resolution from page tree with SEO support";
public override RequirementPriority Priority => RequirementPriority.Medium;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult UrlResolvesToPage(
string url, string expectedPageTitle);
public abstract AcceptanceCriterionResult BoundEntityUrlsWork(
string entityType, string entitySlug, string expectedUrl);
public abstract AcceptanceCriterionResult SeoMetadataGenerated(
string pageTitle, string expectedCanonicalUrl);
}Epic 5: WorkflowEpic
public abstract record WorkflowEpic : Epic
{
public override string Title => "Editorial Workflow Pipelines";
public override RequirementPriority Priority => RequirementPriority.Medium;
public override string Owner => "framework-team";
}public abstract record WorkflowEpic : Epic
{
public override string Title => "Editorial Workflow Pipelines";
public override RequirementPriority Priority => RequirementPriority.Medium;
public override string Owner => "framework-team";
}Feature 5.1: StateMachine
public abstract record StateMachineFeature : Feature<WorkflowEpic>
{
public override string Title => "State machine with stages, transitions, and guards";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult StagesDefinedViaAttributes(
string workflowName, string[] stageNames);
public abstract AcceptanceCriterionResult TransitionsValidated(
string fromStage, string toStage, bool isValid);
public abstract AcceptanceCriterionResult GuardsPreventInvalidTransitions(
string transitionName, string guardCondition);
public abstract AcceptanceCriterionResult DomainEventsOnTransition(
string transitionName, string expectedEventType);
}public abstract record StateMachineFeature : Feature<WorkflowEpic>
{
public override string Title => "State machine with stages, transitions, and guards";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult StagesDefinedViaAttributes(
string workflowName, string[] stageNames);
public abstract AcceptanceCriterionResult TransitionsValidated(
string fromStage, string toStage, bool isValid);
public abstract AcceptanceCriterionResult GuardsPreventInvalidTransitions(
string transitionName, string guardCondition);
public abstract AcceptanceCriterionResult DomainEventsOnTransition(
string transitionName, string expectedEventType);
}Feature 5.2: LocaleTracking
public abstract record LocaleTrackingFeature : Feature<WorkflowEpic>
{
public override string Title => "Per-locale progress tracking";
public override RequirementPriority Priority => RequirementPriority.Medium;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult EachLocaleTrackedIndependently(
string entityId, string[] locales);
public abstract AcceptanceCriterionResult TranslationGateBlocksUntilAllLocalesDone(
string entityId, string[] completedLocales, string[] pendingLocales);
}public abstract record LocaleTrackingFeature : Feature<WorkflowEpic>
{
public override string Title => "Per-locale progress tracking";
public override RequirementPriority Priority => RequirementPriority.Medium;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult EachLocaleTrackedIndependently(
string entityId, string[] locales);
public abstract AcceptanceCriterionResult TranslationGateBlocksUntilAllLocalesDone(
string entityId, string[] completedLocales, string[] pendingLocales);
}Feature 5.3: ScheduledPublishing
public abstract record ScheduledPublishingFeature : Feature<WorkflowEpic>
{
public override string Title => "Timed transitions (scheduled publish/unpublish)";
public override RequirementPriority Priority => RequirementPriority.Low;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult ScheduledTransitionExecutesAtTime(
string entityId, string targetStage, DateTimeOffset scheduledTime);
public abstract AcceptanceCriterionResult ScheduledTransitionCancellable(
string entityId);
}public abstract record ScheduledPublishingFeature : Feature<WorkflowEpic>
{
public override string Title => "Timed transitions (scheduled publish/unpublish)";
public override RequirementPriority Priority => RequirementPriority.Low;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult ScheduledTransitionExecutesAtTime(
string entityId, string targetStage, DateTimeOffset scheduledTime);
public abstract AcceptanceCriterionResult ScheduledTransitionCancellable(
string entityId);
}Epic 6: RequirementsTrackingEpic
public abstract record RequirementsTrackingEpic : Epic
{
public override string Title => "Type-Safe Requirements Chain & Quality Enforcement";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "framework-team";
}public abstract record RequirementsTrackingEpic : Epic
{
public override string Title => "Type-Safe Requirements Chain & Quality Enforcement";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "framework-team";
}Feature 6.1: TypeSafeChain
public abstract record TypeSafeChainFeature : Feature<RequirementsTrackingEpic>
{
public override string Title => "Requirements as types with compiler-enforced chain";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult RequirementsAreAbstractRecords(
string featureTypeName);
public abstract AcceptanceCriterionResult AcceptanceCriteriaAreAbstractMethods(
string featureTypeName, string[] acMethodNames);
public abstract AcceptanceCriterionResult SpecificationsAreInterfaces(
string specInterfaceName, string linkedFeatureTypeName);
public abstract AcceptanceCriterionResult ImplementationMustSatisfySpec(
string implClassName, string specInterfaceName);
public abstract AcceptanceCriterionResult TestsLinkViaTypeOfAndNameOf(
string testClassName, string featureTypeName, string acMethodName);
public abstract AcceptanceCriterionResult ForRequirementAttributeNavigable(
string targetTypeName, string featureTypeName);
}public abstract record TypeSafeChainFeature : Feature<RequirementsTrackingEpic>
{
public override string Title => "Requirements as types with compiler-enforced chain";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult RequirementsAreAbstractRecords(
string featureTypeName);
public abstract AcceptanceCriterionResult AcceptanceCriteriaAreAbstractMethods(
string featureTypeName, string[] acMethodNames);
public abstract AcceptanceCriterionResult SpecificationsAreInterfaces(
string specInterfaceName, string linkedFeatureTypeName);
public abstract AcceptanceCriterionResult ImplementationMustSatisfySpec(
string implClassName, string specInterfaceName);
public abstract AcceptanceCriterionResult TestsLinkViaTypeOfAndNameOf(
string testClassName, string featureTypeName, string acMethodName);
public abstract AcceptanceCriterionResult ForRequirementAttributeNavigable(
string targetTypeName, string featureTypeName);
}Feature 6.2: RoslynAnalyzers
public abstract record RoslynAnalyzersFeature : Feature<RequirementsTrackingEpic>
{
public override string Title => "Compile-time analyzers enforcing the chain (REQ1xx-REQ3xx)";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult REQ1xxDetectsMissingSpecifications(
string featureTypeName);
public abstract AcceptanceCriterionResult REQ2xxDetectsMissingImplementations(
string specInterfaceName);
public abstract AcceptanceCriterionResult REQ3xxDetectsMissingTests(
string featureTypeName, string untestedAcMethod);
public abstract AcceptanceCriterionResult SeverityConfigurableViaEditorconfig(
string diagnosticId, string severityLevel);
}public abstract record RoslynAnalyzersFeature : Feature<RequirementsTrackingEpic>
{
public override string Title => "Compile-time analyzers enforcing the chain (REQ1xx-REQ3xx)";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult REQ1xxDetectsMissingSpecifications(
string featureTypeName);
public abstract AcceptanceCriterionResult REQ2xxDetectsMissingImplementations(
string specInterfaceName);
public abstract AcceptanceCriterionResult REQ3xxDetectsMissingTests(
string featureTypeName, string untestedAcMethod);
public abstract AcceptanceCriterionResult SeverityConfigurableViaEditorconfig(
string diagnosticId, string severityLevel);
}Feature 6.3: QualityGatesIntegration
/// <summary>
/// The CMF build uses the pre-existing `dotnet quality-gates` tool
/// to enforce pass rate, coverage, and fuzz testing thresholds.
/// This feature is about wiring the tool into the CMF's CI pipeline,
/// not building the tool itself.
/// </summary>
public abstract record QualityGatesIntegrationFeature : Feature<RequirementsTrackingEpic>
{
public override string Title => "Quality gates integration (dotnet quality-gates)";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult PassRateEnforced(
decimal minimumPassRate);
public abstract AcceptanceCriterionResult CodeCoverageEnforced(
decimal minimumCoveragePercent);
public abstract AcceptanceCriterionResult FuzzTestingEnabled(
string featureTypeName, int inputCount);
public abstract AcceptanceCriterionResult CiPipelineIntegrated(
string pipelineStage);
public abstract AcceptanceCriterionResult FlakynessDetected(
string testMethodName, int retryCount);
}/// <summary>
/// The CMF build uses the pre-existing `dotnet quality-gates` tool
/// to enforce pass rate, coverage, and fuzz testing thresholds.
/// This feature is about wiring the tool into the CMF's CI pipeline,
/// not building the tool itself.
/// </summary>
public abstract record QualityGatesIntegrationFeature : Feature<RequirementsTrackingEpic>
{
public override string Title => "Quality gates integration (dotnet quality-gates)";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "framework-team";
public abstract AcceptanceCriterionResult PassRateEnforced(
decimal minimumPassRate);
public abstract AcceptanceCriterionResult CodeCoverageEnforced(
decimal minimumCoveragePercent);
public abstract AcceptanceCriterionResult FuzzTestingEnabled(
string featureTypeName, int inputCount);
public abstract AcceptanceCriterionResult CiPipelineIntegrated(
string pipelineStage);
public abstract AcceptanceCriterionResult FlakynessDetected(
string testMethodName, int retryCount);
}Catalog Summary
| Epic | Features | Stories | Tasks (est.) | Priority |
|---|---|---|---|---|
| DomainModelingEpic | 5 | 15 | 45 | Critical |
| ContentManagementEpic | 3 | 10 | 30 | High |
| AdminGenerationEpic | 3 | 8 | 20 | High |
| PageCompositionEpic | 3 | 9 | 25 | High |
| WorkflowEpic | 3 | 7 | 15 | Medium |
| RequirementsTrackingEpic | 3 | 8 | 20 | Critical |
| Total | 20 | 57 | 155 |
What This Catalog Validates
By expressing 20 features with ~100 acceptance criteria methods across 6 epics, we validate that:
- The hierarchy works:
Feature<TParent>with generic constraints correctly models the Epic → Feature → Story relationship - AC methods are expressive: Every acceptance criterion has typed parameters that document what it checks
- The notation scales: 155 tasks across 6 epics is a realistic product backlog, and the DSL handles it without becoming unwieldy
- Requirements are navigable: In an IDE,
typeof(InvariantEnforcementFeature)→ Ctrl+Click → jumps to the feature.nameof(InvariantEnforcementFeature.InvariantsCalledInCommandHandlers)→ jumps to the specific AC - The chain is enforceable: Each feature type in this catalog will eventually need a specification interface, a domain implementation, and tests -- and the analyzers will track that
What It Does NOT Validate
- Runtime behavior: The AC methods are abstract -- nobody calls them yet
- Self-hosting: This catalog is written before the DSL exists. It's design validation, not bootstrapping
- Completeness: A real product would have more features (search, media, i18n, permissions). This catalog covers the core