The Document DSL — A DSL That Documents DSLs
"If you have N DSLs and each needs a doc generator, that is N generators to maintain. Or: one Document DSL with pluggable strategies."
The Meta-Problem
The DDD DSL knows about aggregates. The Requirements DSL knows about features. The Testing DSL knows about coverage. Each could have its own documentation generator — but that is N Source Generators for N DSLs, each maintained separately, each with its own output format, each duplicating cross-referencing logic.
Instead: one Document DSL with pluggable strategies. Each DSL registers how it should be documented. The Document DSL's Source Generator reads all registrations and generates all documentation in one pass.
┌──────────────┐
│ Document DSL │
│ (one SG) │
└──────┬───────┘
┌───────────────┼───────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ DDD │ │ Require- │ │ Ops │
│ Strategy │ │ ments │ │ Strategy │
│ │ │ Strategy │ │ │
└────────────┘ └────────────┘ └────────────┘
│ │ │
▼ ▼ ▼
architecture.md traceability.md runbook.md
Order.mermaid coverage.md dag.mermaid
grafana.json ┌──────────────┐
│ Document DSL │
│ (one SG) │
└──────┬───────┘
┌───────────────┼───────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ DDD │ │ Require- │ │ Ops │
│ Strategy │ │ ments │ │ Strategy │
│ │ │ Strategy │ │ │
└────────────┘ └────────────┘ └────────────┘
│ │ │
▼ ▼ ▼
architecture.md traceability.md runbook.md
Order.mermaid coverage.md dag.mermaid
grafana.jsonAttribute Definitions
// ═══════════════════════════════════════════════════════════════
// Cmf.Document.Lib — The Document DSL
// ═══════════════════════════════════════════════════════════════
/// Declare that a specific DSL concept should be documented.
/// Used at assembly level — one declaration per concept to document.
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class DocumentAttribute<TDsl> : Attribute
where TDsl : class
{
public DocumentFormat Format { get; init; } = DocumentFormat.Markdown;
public string OutputPath { get; init; } = "docs/generated";
public string[] IncludeSections { get; init; } = [];
public string[] ExcludeSections { get; init; } = [];
}
/// Document EVERYTHING for a target type across ALL DSLs.
/// Composes all available strategies and generates cross-referenced output.
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class DocumentSuiteAttribute<TTarget> : Attribute
where TTarget : class
{
public string Title { get; init; } = "";
public DocumentFormat[] Formats { get; init; } = [DocumentFormat.Markdown];
public bool CrossReference { get; init; } = true;
}
public enum DocumentFormat
{
Markdown, // .md — human-readable
Html, // .html — web publishing
Json, // .json — machine-readable, CI/CD integration
Mermaid, // .mermaid — diagrams
OpenApi, // .yaml — API documentation
Yaml, // .yaml — Prometheus, K8s, Helm
Csv // .csv — spreadsheet export
}// ═══════════════════════════════════════════════════════════════
// Cmf.Document.Lib — The Document DSL
// ═══════════════════════════════════════════════════════════════
/// Declare that a specific DSL concept should be documented.
/// Used at assembly level — one declaration per concept to document.
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class DocumentAttribute<TDsl> : Attribute
where TDsl : class
{
public DocumentFormat Format { get; init; } = DocumentFormat.Markdown;
public string OutputPath { get; init; } = "docs/generated";
public string[] IncludeSections { get; init; } = [];
public string[] ExcludeSections { get; init; } = [];
}
/// Document EVERYTHING for a target type across ALL DSLs.
/// Composes all available strategies and generates cross-referenced output.
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class DocumentSuiteAttribute<TTarget> : Attribute
where TTarget : class
{
public string Title { get; init; } = "";
public DocumentFormat[] Formats { get; init; } = [DocumentFormat.Markdown];
public bool CrossReference { get; init; } = true;
}
public enum DocumentFormat
{
Markdown, // .md — human-readable
Html, // .html — web publishing
Json, // .json — machine-readable, CI/CD integration
Mermaid, // .mermaid — diagrams
OpenApi, // .yaml — API documentation
Yaml, // .yaml — Prometheus, K8s, Helm
Csv // .csv — spreadsheet export
}Usage — Declaring What to Document
// ── In your project's AssemblyInfo.cs (or anywhere) ───────────
// Document a specific feature → traceability matrix
[assembly: Document<Feature<OrderProcessingFeature>>(
Format = DocumentFormat.Markdown,
OutputPath = "docs/features")]
// → Generates: docs/features/OrderProcessingFeature.md
// Contains: description, priority, AC table with impl/test status
// Document an aggregate → architecture diagram
[assembly: Document<AggregateRoot<Order>>(
Format = DocumentFormat.Mermaid,
OutputPath = "docs/architecture")]
// → Generates: docs/architecture/Order.mermaid
// Contains: class diagram with compositions, events, value objects
// Document a deployment → runbook
[assembly: Document<DeploymentOrchestrator<OrderServiceV24Deployment>>(
Format = DocumentFormat.Markdown,
OutputPath = "docs/runbooks")]
// → Generates: docs/runbooks/OrderServiceV24Deployment.md
// Contains: DAG, step-by-step procedure, rollback, health checks
// Document EVERYTHING for Order across ALL DSLs
[assembly: DocumentSuite<Order>(
Title = "Order Aggregate — Complete Documentation",
Formats = [DocumentFormat.Markdown, DocumentFormat.Json],
CrossReference = true)]
// → Generates:
// docs/Order/architecture.md (from DDD DSL)
// docs/Order/requirements.md (from Requirements DSL)
// docs/Order/api.md (from API DSL)
// docs/Order/tests.md (from Testing DSL)
// docs/Order/deployment.md (from Ops DSL)
// docs/Order/observability.md (from Observability DSL)
// docs/Order/index.md (cross-referenced index)
// docs/Order/Order.json (machine-readable export)// ── In your project's AssemblyInfo.cs (or anywhere) ───────────
// Document a specific feature → traceability matrix
[assembly: Document<Feature<OrderProcessingFeature>>(
Format = DocumentFormat.Markdown,
OutputPath = "docs/features")]
// → Generates: docs/features/OrderProcessingFeature.md
// Contains: description, priority, AC table with impl/test status
// Document an aggregate → architecture diagram
[assembly: Document<AggregateRoot<Order>>(
Format = DocumentFormat.Mermaid,
OutputPath = "docs/architecture")]
// → Generates: docs/architecture/Order.mermaid
// Contains: class diagram with compositions, events, value objects
// Document a deployment → runbook
[assembly: Document<DeploymentOrchestrator<OrderServiceV24Deployment>>(
Format = DocumentFormat.Markdown,
OutputPath = "docs/runbooks")]
// → Generates: docs/runbooks/OrderServiceV24Deployment.md
// Contains: DAG, step-by-step procedure, rollback, health checks
// Document EVERYTHING for Order across ALL DSLs
[assembly: DocumentSuite<Order>(
Title = "Order Aggregate — Complete Documentation",
Formats = [DocumentFormat.Markdown, DocumentFormat.Json],
CrossReference = true)]
// → Generates:
// docs/Order/architecture.md (from DDD DSL)
// docs/Order/requirements.md (from Requirements DSL)
// docs/Order/api.md (from API DSL)
// docs/Order/tests.md (from Testing DSL)
// docs/Order/deployment.md (from Ops DSL)
// docs/Order/observability.md (from Observability DSL)
// docs/Order/index.md (cross-referenced index)
// docs/Order/Order.json (machine-readable export)Document Strategies — How Each DSL Gets Documented
Each DSL registers a strategy — a class that knows how to read that DSL's attributes and produce documentation.
Strategy Interface
/// Register a documentation strategy for a DSL concept.
[AttributeUsage(AttributeTargets.Class)]
public sealed class DocumentStrategyAttribute<TDslConcept> : Attribute
{
public Type StrategyType { get; }
public DocumentStrategyAttribute(Type strategyType) => StrategyType = strategyType;
}
/// The interface every strategy implements.
public interface IDocumentStrategy<TDslConcept>
{
DocumentOutput Generate(DslConceptInfo<TDslConcept> concept, DocumentContext context);
}
/// Metadata about a DSL concept extracted from Roslyn.
public sealed record DslConceptInfo<T>(
string TypeName,
string Namespace,
string FilePath,
int Line,
ImmutableDictionary<string, object?> AttributeProperties,
ImmutableArray<MemberInfo> Members);
/// Injected context — provides cross-DSL queries via the Artifact Graph.
public sealed class DocumentContext
{
public ArtifactGraph Graph { get; }
public IReadOnlyList<AcceptanceCriteriaInfo> GetAcceptanceCriteria(Type featureType) => ...;
public IReadOnlyList<TestInfo> FindTests(Type featureType, string acName) => ...;
public IReadOnlyList<ImplementationInfo> FindImplementations(Type featureType) => ...;
public IReadOnlyList<EndpointInfo> FindEndpoints(Type featureType) => ...;
public IReadOnlyList<CrossReference> BuildCrossReferences(string sourceType) => ...;
}
/// Output of a documentation strategy.
public sealed record DocumentOutput(
string FileName,
string Content,
DocumentFormat Format,
IReadOnlyList<CrossReference> References);
public sealed record CrossReference(
string TargetType,
string TargetName,
string RelativePath,
CrossReferenceKind Kind);
public enum CrossReferenceKind
{
Implements, TestedBy, DeployedBy, ConfiguredBy,
MonitoredBy, AlertsOn, RollsBackTo, DependsOn
}/// Register a documentation strategy for a DSL concept.
[AttributeUsage(AttributeTargets.Class)]
public sealed class DocumentStrategyAttribute<TDslConcept> : Attribute
{
public Type StrategyType { get; }
public DocumentStrategyAttribute(Type strategyType) => StrategyType = strategyType;
}
/// The interface every strategy implements.
public interface IDocumentStrategy<TDslConcept>
{
DocumentOutput Generate(DslConceptInfo<TDslConcept> concept, DocumentContext context);
}
/// Metadata about a DSL concept extracted from Roslyn.
public sealed record DslConceptInfo<T>(
string TypeName,
string Namespace,
string FilePath,
int Line,
ImmutableDictionary<string, object?> AttributeProperties,
ImmutableArray<MemberInfo> Members);
/// Injected context — provides cross-DSL queries via the Artifact Graph.
public sealed class DocumentContext
{
public ArtifactGraph Graph { get; }
public IReadOnlyList<AcceptanceCriteriaInfo> GetAcceptanceCriteria(Type featureType) => ...;
public IReadOnlyList<TestInfo> FindTests(Type featureType, string acName) => ...;
public IReadOnlyList<ImplementationInfo> FindImplementations(Type featureType) => ...;
public IReadOnlyList<EndpointInfo> FindEndpoints(Type featureType) => ...;
public IReadOnlyList<CrossReference> BuildCrossReferences(string sourceType) => ...;
}
/// Output of a documentation strategy.
public sealed record DocumentOutput(
string FileName,
string Content,
DocumentFormat Format,
IReadOnlyList<CrossReference> References);
public sealed record CrossReference(
string TargetType,
string TargetName,
string RelativePath,
CrossReferenceKind Kind);
public enum CrossReferenceKind
{
Implements, TestedBy, DeployedBy, ConfiguredBy,
MonitoredBy, AlertsOn, RollsBackTo, DependsOn
}Strategy: Documenting a Feature
[DocumentStrategy<FeatureBase>(typeof(FeatureDocumentStrategy))]
public sealed class FeatureDocumentStrategy : IDocumentStrategy<FeatureBase>
{
public DocumentOutput Generate(DslConceptInfo<FeatureBase> concept, DocumentContext ctx)
{
var featureType = concept.TypeName;
var description = concept.AttributeProperties.GetValueOrDefault("Description") as string ?? "";
var priority = concept.AttributeProperties.GetValueOrDefault("Priority")?.ToString() ?? "Medium";
var criteria = ctx.GetAcceptanceCriteria(concept.ResolvedType);
var sb = new StringBuilder();
sb.AppendLine($"# {featureType}");
sb.AppendLine();
sb.AppendLine($"> {description}");
sb.AppendLine($"> Priority: **{priority}**");
sb.AppendLine();
// ── Acceptance Criteria Table ──
sb.AppendLine("## Acceptance Criteria");
sb.AppendLine();
sb.AppendLine("| # | Criteria | Implemented | Tests | Status |");
sb.AppendLine("|---|---------|-------------|-------|--------|");
foreach (var ac in criteria)
{
var impls = ctx.FindImplementations(concept.ResolvedType)
.Where(i => i.ReferencesAC(ac.Name));
var tests = ctx.FindTests(concept.ResolvedType, ac.Name);
var implemented = impls.Any() ? "yes" : "**NO**";
var testCount = tests.Any() ? $"{tests.Count} tests" : "**NO TESTS**";
var status = impls.Any() && tests.Any() ? "Covered" : "Gap";
sb.AppendLine($"| {ac.Index} | {ac.Description} | {implemented} | {testCount} | {status} |");
}
// ── Traceability ──
sb.AppendLine();
sb.AppendLine("## Traceability");
sb.AppendLine();
sb.AppendLine("### Implementations");
foreach (var impl in ctx.FindImplementations(concept.ResolvedType))
sb.AppendLine($"- `{impl.TypeName}` — {impl.FilePath}:{impl.Line}");
sb.AppendLine();
sb.AppendLine("### API Endpoints");
foreach (var ep in ctx.FindEndpoints(concept.ResolvedType))
sb.AppendLine($"- `{ep.Method} {ep.Route}` — {ep.TypeName} ({ep.Version})");
sb.AppendLine();
sb.AppendLine("### Tests");
foreach (var test in ctx.FindTests(concept.ResolvedType))
sb.AppendLine($"- `{test.MethodName}` verifies AC#{test.ACIndex} — {test.FilePath}:{test.Line}");
return new DocumentOutput(
$"{featureType}.md",
sb.ToString(),
DocumentFormat.Markdown,
ctx.BuildCrossReferences(featureType));
}
}[DocumentStrategy<FeatureBase>(typeof(FeatureDocumentStrategy))]
public sealed class FeatureDocumentStrategy : IDocumentStrategy<FeatureBase>
{
public DocumentOutput Generate(DslConceptInfo<FeatureBase> concept, DocumentContext ctx)
{
var featureType = concept.TypeName;
var description = concept.AttributeProperties.GetValueOrDefault("Description") as string ?? "";
var priority = concept.AttributeProperties.GetValueOrDefault("Priority")?.ToString() ?? "Medium";
var criteria = ctx.GetAcceptanceCriteria(concept.ResolvedType);
var sb = new StringBuilder();
sb.AppendLine($"# {featureType}");
sb.AppendLine();
sb.AppendLine($"> {description}");
sb.AppendLine($"> Priority: **{priority}**");
sb.AppendLine();
// ── Acceptance Criteria Table ──
sb.AppendLine("## Acceptance Criteria");
sb.AppendLine();
sb.AppendLine("| # | Criteria | Implemented | Tests | Status |");
sb.AppendLine("|---|---------|-------------|-------|--------|");
foreach (var ac in criteria)
{
var impls = ctx.FindImplementations(concept.ResolvedType)
.Where(i => i.ReferencesAC(ac.Name));
var tests = ctx.FindTests(concept.ResolvedType, ac.Name);
var implemented = impls.Any() ? "yes" : "**NO**";
var testCount = tests.Any() ? $"{tests.Count} tests" : "**NO TESTS**";
var status = impls.Any() && tests.Any() ? "Covered" : "Gap";
sb.AppendLine($"| {ac.Index} | {ac.Description} | {implemented} | {testCount} | {status} |");
}
// ── Traceability ──
sb.AppendLine();
sb.AppendLine("## Traceability");
sb.AppendLine();
sb.AppendLine("### Implementations");
foreach (var impl in ctx.FindImplementations(concept.ResolvedType))
sb.AppendLine($"- `{impl.TypeName}` — {impl.FilePath}:{impl.Line}");
sb.AppendLine();
sb.AppendLine("### API Endpoints");
foreach (var ep in ctx.FindEndpoints(concept.ResolvedType))
sb.AppendLine($"- `{ep.Method} {ep.Route}` — {ep.TypeName} ({ep.Version})");
sb.AppendLine();
sb.AppendLine("### Tests");
foreach (var test in ctx.FindTests(concept.ResolvedType))
sb.AppendLine($"- `{test.MethodName}` verifies AC#{test.ACIndex} — {test.FilePath}:{test.Line}");
return new DocumentOutput(
$"{featureType}.md",
sb.ToString(),
DocumentFormat.Markdown,
ctx.BuildCrossReferences(featureType));
}
}Generated output (docs/features/OrderProcessingFeature.md):
# OrderProcessingFeature
> Process customer orders with payment and fulfillment
> Priority: **Critical**
## Acceptance Criteria
| # | Criteria | Implemented | Tests | Status |
|---|---------|-------------|-------|--------|
| 0 | Order can be created with at least one line item | yes | 2 tests | Covered |
| 1 | Order total is calculated from line quantities × unit prices | yes | 1 test | Covered |
| 2 | Order can be marked as paid with a valid payment reference | yes | 2 tests | Covered |
| 3 | Paid order triggers fulfillment workflow | **NO** | **NO TESTS** | Gap |
## Traceability
### Implementations
- `Order` — src/Domain/Order.cs:15
- `OrderLine` — src/Domain/OrderLine.cs:8
### API Endpoints
- `POST /api/orders` — CreateOrderEndpoint (v2)
- `POST /api/orders/{id}/pay` — PayOrderEndpoint (v2)
### Tests
- `Order_can_be_created_with_valid_lines` verifies AC#0 — tests/OrderTests.cs:22
- `Order_creation_fails_with_empty_lines` verifies AC#0 — tests/OrderTests.cs:31
- `Order_total_equals_sum_of_line_totals` verifies AC#1 — tests/OrderTests.cs:40
- `Order_can_be_paid_with_valid_reference` verifies AC#2 — tests/OrderTests.cs:49
- `Order_payment_fails_without_reference` verifies AC#2 — tests/OrderTests.cs:58# OrderProcessingFeature
> Process customer orders with payment and fulfillment
> Priority: **Critical**
## Acceptance Criteria
| # | Criteria | Implemented | Tests | Status |
|---|---------|-------------|-------|--------|
| 0 | Order can be created with at least one line item | yes | 2 tests | Covered |
| 1 | Order total is calculated from line quantities × unit prices | yes | 1 test | Covered |
| 2 | Order can be marked as paid with a valid payment reference | yes | 2 tests | Covered |
| 3 | Paid order triggers fulfillment workflow | **NO** | **NO TESTS** | Gap |
## Traceability
### Implementations
- `Order` — src/Domain/Order.cs:15
- `OrderLine` — src/Domain/OrderLine.cs:8
### API Endpoints
- `POST /api/orders` — CreateOrderEndpoint (v2)
- `POST /api/orders/{id}/pay` — PayOrderEndpoint (v2)
### Tests
- `Order_can_be_created_with_valid_lines` verifies AC#0 — tests/OrderTests.cs:22
- `Order_creation_fails_with_empty_lines` verifies AC#0 — tests/OrderTests.cs:31
- `Order_total_equals_sum_of_line_totals` verifies AC#1 — tests/OrderTests.cs:40
- `Order_can_be_paid_with_valid_reference` verifies AC#2 — tests/OrderTests.cs:49
- `Order_payment_fails_without_reference` verifies AC#2 — tests/OrderTests.cs:58Strategy: Documenting an Aggregate Root
[DocumentStrategy<AggregateRootAttribute>(typeof(AggregateDocumentStrategy))]
public sealed class AggregateDocumentStrategy : IDocumentStrategy<AggregateRootAttribute>
{
public DocumentOutput Generate(
DslConceptInfo<AggregateRootAttribute> concept, DocumentContext ctx)
{
var name = concept.TypeName;
var bc = concept.AttributeProperties.GetValueOrDefault("BoundedContext") as string ?? "";
var desc = concept.AttributeProperties.GetValueOrDefault("Description") as string ?? "";
var compositions = concept.Members
.Where(m => m.HasAttribute("CompositionAttribute"))
.ToList();
var events = concept.Members
.Where(m => m.HasAttribute("DomainEventAttribute"))
.ToList();
var sb = new StringBuilder();
// ── Mermaid class diagram ──
sb.AppendLine($"# {name}");
sb.AppendLine($"> Bounded Context: **{bc}**");
sb.AppendLine($"> {desc}");
sb.AppendLine();
sb.AppendLine("## Entity Diagram");
sb.AppendLine();
sb.AppendLine("```mermaid");
sb.AppendLine("classDiagram");
sb.AppendLine($" class {name} {{");
sb.AppendLine($" +{name}Id Id");
foreach (var comp in compositions)
sb.AppendLine($" +{comp.TypeName} {comp.Name}");
sb.AppendLine(" }");
foreach (var comp in compositions)
{
sb.AppendLine($" class {comp.TypeName}");
sb.AppendLine($" {name} *-- {comp.TypeName} : composition");
}
sb.AppendLine("```");
// ── Domain Events ──
sb.AppendLine();
sb.AppendLine("## Domain Events");
sb.AppendLine();
foreach (var evt in events)
{
var eventType = evt.GetAttributeProperty("EventType") as string ?? "";
sb.AppendLine($"- `{evt.Name}()` publishes `{eventType}`");
}
// ── Linked Requirements ──
var linkedFeatures = ctx.Graph
.GetIncomingEdges(name, EdgeKind.Implements)
.Select(e => e.Source);
sb.AppendLine();
sb.AppendLine("## Linked Requirements");
foreach (var f in linkedFeatures)
sb.AppendLine($"- [{f.TypeName}](../features/{f.TypeName}.md)");
return new DocumentOutput(
$"{name}.md", sb.ToString(),
DocumentFormat.Markdown,
ctx.BuildCrossReferences(name));
}
}[DocumentStrategy<AggregateRootAttribute>(typeof(AggregateDocumentStrategy))]
public sealed class AggregateDocumentStrategy : IDocumentStrategy<AggregateRootAttribute>
{
public DocumentOutput Generate(
DslConceptInfo<AggregateRootAttribute> concept, DocumentContext ctx)
{
var name = concept.TypeName;
var bc = concept.AttributeProperties.GetValueOrDefault("BoundedContext") as string ?? "";
var desc = concept.AttributeProperties.GetValueOrDefault("Description") as string ?? "";
var compositions = concept.Members
.Where(m => m.HasAttribute("CompositionAttribute"))
.ToList();
var events = concept.Members
.Where(m => m.HasAttribute("DomainEventAttribute"))
.ToList();
var sb = new StringBuilder();
// ── Mermaid class diagram ──
sb.AppendLine($"# {name}");
sb.AppendLine($"> Bounded Context: **{bc}**");
sb.AppendLine($"> {desc}");
sb.AppendLine();
sb.AppendLine("## Entity Diagram");
sb.AppendLine();
sb.AppendLine("```mermaid");
sb.AppendLine("classDiagram");
sb.AppendLine($" class {name} {{");
sb.AppendLine($" +{name}Id Id");
foreach (var comp in compositions)
sb.AppendLine($" +{comp.TypeName} {comp.Name}");
sb.AppendLine(" }");
foreach (var comp in compositions)
{
sb.AppendLine($" class {comp.TypeName}");
sb.AppendLine($" {name} *-- {comp.TypeName} : composition");
}
sb.AppendLine("```");
// ── Domain Events ──
sb.AppendLine();
sb.AppendLine("## Domain Events");
sb.AppendLine();
foreach (var evt in events)
{
var eventType = evt.GetAttributeProperty("EventType") as string ?? "";
sb.AppendLine($"- `{evt.Name}()` publishes `{eventType}`");
}
// ── Linked Requirements ──
var linkedFeatures = ctx.Graph
.GetIncomingEdges(name, EdgeKind.Implements)
.Select(e => e.Source);
sb.AppendLine();
sb.AppendLine("## Linked Requirements");
foreach (var f in linkedFeatures)
sb.AppendLine($"- [{f.TypeName}](../features/{f.TypeName}.md)");
return new DocumentOutput(
$"{name}.md", sb.ToString(),
DocumentFormat.Markdown,
ctx.BuildCrossReferences(name));
}
}DocumentSuite<T> — Compose All Docs for One Target
When you declare [assembly: DocumentSuite<Order>], the Document DSL finds every strategy that can document Order and composes them:
// The Document SG discovers:
// - Order has [AggregateRoot] → AggregateDocumentStrategy applies
// - Order has [ForRequirement(typeof(OrderProcessingFeature))] → FeatureDocumentStrategy applies
// - CreateOrderEndpoint has [ForRequirement(typeof(OrderProcessingFeature))]
// and references Order → EndpointDocumentStrategy applies
// - OrderProcessingTests has [Verifies(...OrderProcessingFeature...)]
// → TestCoverageDocumentStrategy applies
// Generated file tree:
// docs/Order/
// ├── index.md ← cross-referenced hub
// ├── architecture.md ← from AggregateDocumentStrategy
// ├── architecture.mermaid ← entity diagram
// ├── requirements.md ← from FeatureDocumentStrategy
// ├── api.md ← from EndpointDocumentStrategy
// ├── tests.md ← from TestCoverageDocumentStrategy
// └── Order.json ← machine-readable export (all data combined)// The Document SG discovers:
// - Order has [AggregateRoot] → AggregateDocumentStrategy applies
// - Order has [ForRequirement(typeof(OrderProcessingFeature))] → FeatureDocumentStrategy applies
// - CreateOrderEndpoint has [ForRequirement(typeof(OrderProcessingFeature))]
// and references Order → EndpointDocumentStrategy applies
// - OrderProcessingTests has [Verifies(...OrderProcessingFeature...)]
// → TestCoverageDocumentStrategy applies
// Generated file tree:
// docs/Order/
// ├── index.md ← cross-referenced hub
// ├── architecture.md ← from AggregateDocumentStrategy
// ├── architecture.mermaid ← entity diagram
// ├── requirements.md ← from FeatureDocumentStrategy
// ├── api.md ← from EndpointDocumentStrategy
// ├── tests.md ← from TestCoverageDocumentStrategy
// └── Order.json ← machine-readable export (all data combined)The generated index.md cross-references everything:
# Order — Complete Documentation
> Bounded Context: Ordering
> Represents a customer order with line items and payment
## Documentation Index
| Document | Source DSL | Format | Generated From |
|---|---|---|---|
| [Architecture](architecture.md) | DDD | Markdown + Mermaid | `[AggregateRoot]` |
| [Requirements](requirements.md) | Requirements | Markdown | `[ForRequirement]` → `OrderProcessingFeature` |
| [API](api.md) | API | Markdown | `[TypedEndpoint]` on 2 endpoints |
| [Tests](tests.md) | Testing | Markdown | `[Verifies]` on 5 test methods |
## Quick Stats
- **Acceptance Criteria**: 4 (3 covered, 1 gap)
- **Test Methods**: 5
- **API Endpoints**: 2 (POST /api/orders, POST /api/orders/{id}/pay)
- **Domain Events**: 2 (OrderCreated, OrderPaid)
- **Compositions**: 2 (OrderLine, PaymentInfo)# Order — Complete Documentation
> Bounded Context: Ordering
> Represents a customer order with line items and payment
## Documentation Index
| Document | Source DSL | Format | Generated From |
|---|---|---|---|
| [Architecture](architecture.md) | DDD | Markdown + Mermaid | `[AggregateRoot]` |
| [Requirements](requirements.md) | Requirements | Markdown | `[ForRequirement]` → `OrderProcessingFeature` |
| [API](api.md) | API | Markdown | `[TypedEndpoint]` on 2 endpoints |
| [Tests](tests.md) | Testing | Markdown | `[Verifies]` on 5 test methods |
## Quick Stats
- **Acceptance Criteria**: 4 (3 covered, 1 gap)
- **Test Methods**: 5
- **API Endpoints**: 2 (POST /api/orders, POST /api/orders/{id}/pay)
- **Domain Events**: 2 (OrderCreated, OrderPaid)
- **Compositions**: 2 (OrderLine, PaymentInfo)The Self-Documenting DSL — Document<Document<>>
Here is the recursive proof that the pattern works.
The Document DSL itself is a DSL — it has attributes (DocumentAttribute<T>, DocumentStrategyAttribute<T>), a Source Generator, and analyzers. Therefore, it can document itself:
// The Document DSL documents its own strategy registry
[assembly: Document<Document<DocumentAttribute<object>>>(
Format = DocumentFormat.Markdown,
OutputPath = "docs/document-dsl")]// The Document DSL documents its own strategy registry
[assembly: Document<Document<DocumentAttribute<object>>>(
Format = DocumentFormat.Markdown,
OutputPath = "docs/document-dsl")]The DocumentMetaStrategy knows how to introspect the Document DSL:
[DocumentStrategy<DocumentAttribute<object>>(typeof(DocumentMetaStrategy))]
public sealed class DocumentMetaStrategy : IDocumentStrategy<DocumentAttribute<object>>
{
public DocumentOutput Generate(
DslConceptInfo<DocumentAttribute<object>> concept, DocumentContext ctx)
{
var sb = new StringBuilder();
sb.AppendLine("# Document DSL — Reference Documentation");
sb.AppendLine();
sb.AppendLine("> Auto-generated by `Document<Document<>>`");
sb.AppendLine();
// List all registered strategies
sb.AppendLine("## Registered Strategies");
sb.AppendLine();
sb.AppendLine("| DSL Concept | Strategy Class | Output Format | Description |");
sb.AppendLine("|---|---|---|---|");
foreach (var strategy in ctx.GetAllRegisteredStrategies())
{
sb.AppendLine(
$"| `{strategy.DslConcept}` | `{strategy.StrategyType}` " +
$"| {strategy.DefaultFormat} | {strategy.Description} |");
}
// List all Document<> declarations in the solution
sb.AppendLine();
sb.AppendLine("## Active Document Declarations");
sb.AppendLine();
foreach (var decl in ctx.GetAllDocumentDeclarations())
{
sb.AppendLine($"- `Document<{decl.DslType}>` → `{decl.OutputPath}/{decl.FileName}`");
}
// List all analyzers
sb.AppendLine();
sb.AppendLine("## Analyzer Diagnostics");
sb.AppendLine();
sb.AppendLine("| ID | Severity | Description |");
sb.AppendLine("|---|---|---|");
sb.AppendLine("| DOC001 | Warning | Orphaned artifact — DSL attribute without [ForRequirement] |");
sb.AppendLine("| DOC002 | Warning | Empty DocumentSuite section — DSL not referenced |");
sb.AppendLine("| DOC003 | Error | Broken composition reference in diagram |");
sb.AppendLine("| DOC004 | Warning | Stale cross-reference — target removed |");
sb.AppendLine("| DOC005 | Error | Strategy not found — [DocumentStrategy] references missing type |");
return new DocumentOutput(
"document-dsl-reference.md", sb.ToString(),
DocumentFormat.Markdown,
[]);
}
}[DocumentStrategy<DocumentAttribute<object>>(typeof(DocumentMetaStrategy))]
public sealed class DocumentMetaStrategy : IDocumentStrategy<DocumentAttribute<object>>
{
public DocumentOutput Generate(
DslConceptInfo<DocumentAttribute<object>> concept, DocumentContext ctx)
{
var sb = new StringBuilder();
sb.AppendLine("# Document DSL — Reference Documentation");
sb.AppendLine();
sb.AppendLine("> Auto-generated by `Document<Document<>>`");
sb.AppendLine();
// List all registered strategies
sb.AppendLine("## Registered Strategies");
sb.AppendLine();
sb.AppendLine("| DSL Concept | Strategy Class | Output Format | Description |");
sb.AppendLine("|---|---|---|---|");
foreach (var strategy in ctx.GetAllRegisteredStrategies())
{
sb.AppendLine(
$"| `{strategy.DslConcept}` | `{strategy.StrategyType}` " +
$"| {strategy.DefaultFormat} | {strategy.Description} |");
}
// List all Document<> declarations in the solution
sb.AppendLine();
sb.AppendLine("## Active Document Declarations");
sb.AppendLine();
foreach (var decl in ctx.GetAllDocumentDeclarations())
{
sb.AppendLine($"- `Document<{decl.DslType}>` → `{decl.OutputPath}/{decl.FileName}`");
}
// List all analyzers
sb.AppendLine();
sb.AppendLine("## Analyzer Diagnostics");
sb.AppendLine();
sb.AppendLine("| ID | Severity | Description |");
sb.AppendLine("|---|---|---|");
sb.AppendLine("| DOC001 | Warning | Orphaned artifact — DSL attribute without [ForRequirement] |");
sb.AppendLine("| DOC002 | Warning | Empty DocumentSuite section — DSL not referenced |");
sb.AppendLine("| DOC003 | Error | Broken composition reference in diagram |");
sb.AppendLine("| DOC004 | Warning | Stale cross-reference — target removed |");
sb.AppendLine("| DOC005 | Error | Strategy not found — [DocumentStrategy] references missing type |");
return new DocumentOutput(
"document-dsl-reference.md", sb.ToString(),
DocumentFormat.Markdown,
[]);
}
}Generated output (docs/document-dsl/document-dsl-reference.md):
# Document DSL — Reference Documentation
> Auto-generated by `Document<Document<>>`
## Registered Strategies
| DSL Concept | Strategy Class | Output Format | Description |
|---|---|---|---|
| `FeatureBase` | `FeatureDocumentStrategy` | Markdown | Traceability matrix, AC table, test coverage |
| `AggregateRootAttribute` | `AggregateDocumentStrategy` | Markdown + Mermaid | Entity diagram, compositions, events |
| `TypedEndpointAttribute` | `EndpointDocumentStrategy` | Markdown + OpenAPI | API docs with requirement links |
| `DeploymentOrchestratorAttribute` | `DeploymentDocumentStrategy` | Markdown + Mermaid + YAML | Runbook, DAG, Grafana, Prometheus |
| `DocumentAttribute<>` | `DocumentMetaStrategy` | Markdown | This document (self-referential) |
## Active Document Declarations
- `Document<Feature<OrderProcessingFeature>>` → `docs/features/OrderProcessingFeature.md`
- `Document<AggregateRoot<Order>>` → `docs/architecture/Order.md`
- `Document<DeploymentOrchestrator<OrderServiceV24Deployment>>` → `docs/runbooks/OrderServiceV24Deployment.md`
- `Document<Document<DocumentAttribute<object>>>` → `docs/document-dsl/document-dsl-reference.md`
## Analyzer Diagnostics
| ID | Severity | Description |
|---|---|---|
| DOC001 | Warning | Orphaned artifact — DSL attribute without [ForRequirement] |
| DOC002 | Warning | Empty DocumentSuite section — DSL not referenced |
| DOC003 | Error | Broken composition reference in diagram |
| DOC004 | Warning | Stale cross-reference — target removed |
| DOC005 | Error | Strategy not found — [DocumentStrategy] references missing type |# Document DSL — Reference Documentation
> Auto-generated by `Document<Document<>>`
## Registered Strategies
| DSL Concept | Strategy Class | Output Format | Description |
|---|---|---|---|
| `FeatureBase` | `FeatureDocumentStrategy` | Markdown | Traceability matrix, AC table, test coverage |
| `AggregateRootAttribute` | `AggregateDocumentStrategy` | Markdown + Mermaid | Entity diagram, compositions, events |
| `TypedEndpointAttribute` | `EndpointDocumentStrategy` | Markdown + OpenAPI | API docs with requirement links |
| `DeploymentOrchestratorAttribute` | `DeploymentDocumentStrategy` | Markdown + Mermaid + YAML | Runbook, DAG, Grafana, Prometheus |
| `DocumentAttribute<>` | `DocumentMetaStrategy` | Markdown | This document (self-referential) |
## Active Document Declarations
- `Document<Feature<OrderProcessingFeature>>` → `docs/features/OrderProcessingFeature.md`
- `Document<AggregateRoot<Order>>` → `docs/architecture/Order.md`
- `Document<DeploymentOrchestrator<OrderServiceV24Deployment>>` → `docs/runbooks/OrderServiceV24Deployment.md`
- `Document<Document<DocumentAttribute<object>>>` → `docs/document-dsl/document-dsl-reference.md`
## Analyzer Diagnostics
| ID | Severity | Description |
|---|---|---|
| DOC001 | Warning | Orphaned artifact — DSL attribute without [ForRequirement] |
| DOC002 | Warning | Empty DocumentSuite section — DSL not referenced |
| DOC003 | Error | Broken composition reference in diagram |
| DOC004 | Warning | Stale cross-reference — target removed |
| DOC005 | Error | Strategy not found — [DocumentStrategy] references missing type |If you can document the documenter, the pattern is self-sustaining.
DOC001–DOC005 Analyzers
The Document DSL includes its own analyzers to catch documentation issues at compile time:
// ── DOC001: Orphaned Artifact ─────────────────────────────────
// A class has a DSL attribute but no [ForRequirement] link
warning DOC001: 'PaymentGatewayClient' has [Injectable] but is not linked
to any requirement via [ForRequirement]
// ── DOC002: Empty DocumentSuite Section ───────────────────────
// DocumentSuite<Order> includes deployment docs but no Ops DSL references Order
warning DOC002: [DocumentSuite<Order>] includes deployment docs but
no [DeploymentOrchestrator] references Order
— deployment section will be empty
// ── DOC003: Broken Composition Reference ──────────────────────
// [Composition] points to a type without [ValueObject] or [Entity]
error DOC003: [Document<AggregateRoot<Order>>] references [Composition]
OrderLine but OrderLine has no [ValueObject] or [Entity]
attribute — cannot generate relationship diagram
// ── DOC004: Stale Cross-Reference ─────────────────────────────
// A cross-reference target was removed
warning DOC004: Cross-reference from Feature 'OrderProcessing' to
Endpoint 'CreateOrder' — endpoint removed, link stale
// ── DOC005: Strategy Not Found ────────────────────────────────
// [DocumentStrategy] references a type that doesn't implement the interface
error DOC005: [DocumentStrategy<FeatureBase>] registered
'FeatureDocumentStrategy' but type not found in compilation// ── DOC001: Orphaned Artifact ─────────────────────────────────
// A class has a DSL attribute but no [ForRequirement] link
warning DOC001: 'PaymentGatewayClient' has [Injectable] but is not linked
to any requirement via [ForRequirement]
// ── DOC002: Empty DocumentSuite Section ───────────────────────
// DocumentSuite<Order> includes deployment docs but no Ops DSL references Order
warning DOC002: [DocumentSuite<Order>] includes deployment docs but
no [DeploymentOrchestrator] references Order
— deployment section will be empty
// ── DOC003: Broken Composition Reference ──────────────────────
// [Composition] points to a type without [ValueObject] or [Entity]
error DOC003: [Document<AggregateRoot<Order>>] references [Composition]
OrderLine but OrderLine has no [ValueObject] or [Entity]
attribute — cannot generate relationship diagram
// ── DOC004: Stale Cross-Reference ─────────────────────────────
// A cross-reference target was removed
warning DOC004: Cross-reference from Feature 'OrderProcessing' to
Endpoint 'CreateOrder' — endpoint removed, link stale
// ── DOC005: Strategy Not Found ────────────────────────────────
// [DocumentStrategy] references a type that doesn't implement the interface
error DOC005: [DocumentStrategy<FeatureBase>] registered
'FeatureDocumentStrategy' but type not found in compilationDX: Onboarding in Three Lines
For a team that already uses the dev-side DSLs, adding documentation generation is three lines:
Line 1: Add the Project Reference
<!-- In your .csproj -->
<ProjectReference Include="Cmf.Document.Lib" /><!-- In your .csproj -->
<ProjectReference Include="Cmf.Document.Lib" />Line 2: Declare What to Document
// In AssemblyInfo.cs (or anywhere)
[assembly: DocumentSuite<Order>(CrossReference = true)]// In AssemblyInfo.cs (or anywhere)
[assembly: DocumentSuite<Order>(CrossReference = true)]Line 3: Build
dotnet build
# Output:
# docs/Order/index.md
# docs/Order/architecture.md
# docs/Order/requirements.md
# docs/Order/api.md
# docs/Order/tests.mddotnet build
# Output:
# docs/Order/index.md
# docs/Order/architecture.md
# docs/Order/requirements.md
# docs/Order/api.md
# docs/Order/tests.mdNo configuration files. No plugin setup. No template authoring. The DSL attributes you already have ARE the documentation source. The Document DSL just reads them.
What's Next
The Document DSL can document any DSL that has a registered strategy. The dev-side DSLs are covered. But the lifecycle doesn't end at dotnet test.
Parts IV–V introduce the Ops Meta-DSL — five independently published sub-DSLs for Deployment, Migration, Observability, Configuration, and Resilience. Each brings new attributes. Each gets its own Document Strategy. And Part VI shows the generated output: not just Markdown, but Grafana dashboard JSON, Prometheus alert YAML, Kubernetes probes, and Helm values.