Requirements-as-Code
Type the problem. Constrain the solution. Verify the delivery.
Please read Requirements-as-Code - Type It before
Please view Requirements-as-Code with AI: A Controlled Delivery Workflow for Workflow
What This Is
Requirements-as-Code turns the problem–solution layer into a type system. Requirements, features, acceptance criteria and their verification relationships become first-class engineering artifacts: versioned, navigable, refactorable and mechanically checked inside the development environment.
The goal is not to ask developers — or an AI agent — to remember a better document. It is to stop rebuilding the meaning of the assignment from disconnected prose every time the work changes hands.
A human-approved requirement describes the obligation. A feature declares the capability intended to satisfy it. Acceptance criteria make the expected outcomes explicit. Tests declare which criteria they verify. Compiler checks, static analysis and delivery gates reject particular classes of broken relationships; executed checks produce evidence for a particular version of the system.
The typed model is authoritative for the approved delivery contract. Markdown, a tracker, a dashboard or an AI context package can remain useful interfaces, but they must not silently become competing versions of that contract.
This does not mean that a type checker understands a customer's business, or that passing tests proves a product correct in every situation. It means that part of the relationship between intent and delivery stops depending on memory and convention and becomes inspectable engineering structure.
For a solution architect, the value is a common model connecting business obligations, technical decisions and acceptance. For a delivery team, it is earlier feedback on missing or broken links. For an AI-assisted workflow, it is a stable, approved contract that different models and different runs must work against rather than reinterpret independently.
There are two implementations: TypeScript, with a broad, port-driven tooling surface, and C#, with a generic-constrained hierarchy and Roslyn integration. They share an idea, not yet complete feature parity. Implemented mechanisms, documented status and proposed industrial integrations are distinguished below.
This is Anything-as-Code applied to software intent, with two starting points. A new need can lead forward to reviewed requirements and verification. I am also experimenting with the reverse path, using existing code and tests to propose requirements, features and acceptance criteria for human review. The approved model can then guide gap analysis, refactoring or redesign. Starting from existing code and tests develops this with an illustrative example.
In that experiment, observed behavior describes what the system does; approval decides what it should continue to do or how it should change. A passing characterization test can preserve a defect. Semantic recovery is distinct from the existing TypeScript ↔ JSON synchronization, which translates declarations of an already expressed specification.
From Typed Specs to Requirements-as-Code
Typed Specs was the prototype name of Requirements-as-Code. The later name makes the purpose more explicit: not just typing a specification's syntax, but representing the problem, the chosen capabilities and the obligations connecting them to delivery.
Three concepts should not be collapsed into one:
| Concept | The question it answers | Its role here |
|---|---|---|
| Spec-driven development | What should guide implementation, and when should it be agreed? | The development process: specify, review, refine, implement and verify. |
| Requirements-as-Code | How is the approved problem–solution contract represented? | A typed, versioned model with identities and explicit relationships. |
| Compiler/static-analysis/CI enforcement | Which deviations will actually be rejected? | The concrete checks implemented and enabled for the project. |
They are complementary. A spec-driven workflow can use Requirements-as-Code as its representation and enforcement layer. A document-oriented process can already have sophisticated validation; typed requirements do not make all other approaches “unvalidated.” Conversely, putting a requirement in a .ts or .cs file does not automatically make all its meaning enforceable.
The important distinction is which obligations are represented, which checks exist, and which failures block delivery.
Documents remain valuable for discovery, rationale, stakeholder discussion and trade-offs. The operating rule is that a change to an approved obligation is an explicit change to the model, not an incidental edit to a prompt or a generated explanation.
This is also why Requirements-as-Code is not a replacement for BDD, contract testing, DDD or a work tracker. Those can provide scenarios, interface checks, domain models and planning views. Requirements-as-Code connects obligations to those artifacts and checks the relationships that its implementation knows how to validate.
The Chain — Requirement → Feature → AC → Test
The shared core has four layers. Execution evidence is a separate consequence of running verification, not another spelling of “there is a test.”
Requirement: the problem and the obligation
A requirement expresses what must be achieved and why it matters. The TypeScript model includes an EARS-style statement, rationale, priority, provenance, risk and fit criteria. A statement such as “When an approved request is submitted, the platform shall provision an isolated environment” captures a trigger and a required response.
The source may be a customer, an internal decision or a regulation. Provenance should identify the source and, where applicable, the approval authority; a source string alone is not a signature. Risk explains the consequence of failure and informs the verification strategy.
EARS-style phrasing structures the sentence; it does not turn its natural-language content into a mathematical proof. Stakeholders still have to decide whether “isolated,” “approved” and “available” have been defined adequately.
Feature: the solution capability
A feature is the capability chosen to address one or more obligations. In the original site example it is user-facing Markdown rendering. In a platform programme it might be an environment-provisioning capability, a reusable delivery pipeline or an auditable access workflow.
@Satisfies(...) and [ForRequirement(...)] express a declared satisfaction relationship. They do not establish that the architecture is the right business choice. That remains a reviewed decision supported by acceptance criteria and evidence.
Acceptance criterion: the verification obligation
An acceptance criterion names an outcome that can be evaluated. In these implementations it is represented by an abstract method returning ACResult or AcceptanceCriterionResult.
The abstract method is a contract declaration. It is not the implementation, and its existence does not execute the predicate described in its comment. The tests and other verification adapters supply the actual observations and assertions.
A good criterion states an observable result, a relevant condition and, when necessary, a measurement threshold. “Secure” is not enough. “An unauthorised identity cannot create or read an environment” is a more useful acceptance obligation, provided the identities, operations and environment are specified.
Test: an executable attempt to falsify a claim
A test declares which criterion it verifies and executes against the relevant system. A single criterion may need multiple tests, at multiple levels. A unit test of a policy function does not replace a deployment-time check of the actual platform boundary.
The source model includes functional and non-functional concerns: accessibility, internationalisation, visual behaviour and performance alongside ordinary unit and end-to-end checks. Test level and verification concern are distinct axes: a performance concern may require several levels, and an end-to-end test may address more than one concern.
Evidence: what happened for this version
A test binding answers “what is meant to be verified?” Evidence answers “what actually ran, against which version, in which environment, and what happened?”
For industrial delivery, the evidence record should identify at least the approved requirement baseline, source revision, test identity, runner/tool versions, execution environment and outcome. Relevant reports and artifacts should be retained according to the project's policy. This is an integration requirement for the delivery system; it is not a claim that both libraries already implement a complete evidence store.
A previously passing test does not automatically verify a changed requirement. A linked test that was skipped is not successful evidence. A report for one environment is not proof about a different environment.
What Is Actually Enforced
“Compiler-enforced” needs a precise object. The compiler can reject specific invalid shapes and references. Other obligations require analyzers, executed tests or governance outside the compiler.
| Layer | What it can establish | What it cannot establish by itself |
|---|---|---|
| Language/type system | A referenced type or member exists; declared generic constraints are satisfied; a constrained name belongs to an allowed type-level set. | That the requirement is complete, that two independently valid references belong together, or that a business outcome is achieved. |
| Semantic analysis / AST scanner | Declared relationships resolve; a criterion has the required bindings; selected orphan, lifecycle or policy rules are satisfied, where implemented. | That a linked test is meaningful, executed or passing. |
| Test runner | Particular checks pass or fail against the exercised implementation under the run's conditions. | Universal correctness, exhaustive scenarios or the completeness of the requirements. |
| Source coverage | Which instrumented code paths were exercised. | Requirement satisfaction or the strength of assertions. |
| Mutation testing | Whether tests detect the selected artificial changes. | Completeness of the fault model or proof that all defects are found. |
| Versioned evidence and delivery gate | Required checks have acceptable results for the approved baseline and selected artifacts, under the configured policy. | Truth outside that policy's scope or immunity to tampering with the gate itself. |
| Human approval and repository permissions | Who can approve an obligation, a policy exception or a release. | A guarantee supplied merely by writing status = 'Approved' in source. |
A useful shorthand is:
well-typed ≠ correctly specified
linked ≠ executed
executed ≠ passed
passed ≠ exhaustive
100% AC linkage ≠ 100% source coverage ≠ complete business correctnesswell-typed ≠ correctly specified
linked ≠ executed
executed ≠ passed
passed ≠ exhaustive
100% AC linkage ≠ 100% source coverage ≠ complete business correctnessThis is not a weakness to hide. It is the basis of a credible assurance story: every guarantee names its mechanism and its limits.
The same discipline applies to change impact. A graph can reveal dependants connected through recorded edges. It cannot discover every unmodelled relationship in the business. Changing a criterion's prose may preserve its type and method name; detecting that change needs versioning, content hashing and review, not just a rename error.
TypeScript: @frenchexdev/requirements
The TypeScript implementation is the documented reference implementation with the broadest tooling surface. It lives in packages/requirements-lib/, with requirements-core and requirements-styles, inside the FrenchExDev TypeScript ecosystem. It is used on this site and in related packages; that is a concrete dogfooding context, not a claim of independently demonstrated enterprise adoption.
The Full Chain — Requirement → Feature → Tests
The following three-file example preserves the Markdown-rendering scenario from the project documentation. The package exports, test-registration integration and rendering implementation must be supplied by the consuming project. These are API illustrations, not a standalone project with a verified dependency lockfile.
1. The Requirement — requirements/requirements/content-loading.ts
import { Requirement, Priority } from '@frenchexdev/requirements';
export abstract class ContentMustLoadFromMarkdown extends Requirement {
readonly id = 'REQ-MD-CONTENT';
readonly title = 'Page content authored in Markdown, rendered at build time';
readonly priority = Priority.Critical;
readonly kind = 'Functional';
readonly status = 'Approved';
readonly statement = {
pattern: 'event-driven',
trigger: 'When the build pipeline visits a content/**/*.md file',
response: 'parse YAML frontmatter, render Markdown to HTML, replace mermaid placeholders with inline SVG, and emit a static page reachable by its canonical URL',
};
readonly rationale = {
claim: 'Authoring stays human-readable; HTML is a build artifact, not the source of truth. The site must work without JavaScript and remain crawlable.',
kind: 'design-decision',
};
readonly fitCriteria = [
{
kind: 'demonstration' as const,
scenario: 'Run `npm run build:static` and open every page in toc.json with JS disabled — body, navigation, and diagrams render.',
},
{
kind: 'narrative' as const,
text: 'Build is hermetic: the content/ tree fully determines the published site.',
},
];
readonly verificationMethod = 'Test';
readonly source = {
type: 'internal',
decision: 'CV site authoring discipline',
date: '2026-04-01',
};
readonly risk = {
level: 'High',
ifNotMet: 'Authoring drifts to ad-hoc HTML; accessibility and SEO regress.',
mitigations: ['Strict Markdown linters', 'No raw HTML pages', 'Single render pipeline'],
};
}import { Requirement, Priority } from '@frenchexdev/requirements';
export abstract class ContentMustLoadFromMarkdown extends Requirement {
readonly id = 'REQ-MD-CONTENT';
readonly title = 'Page content authored in Markdown, rendered at build time';
readonly priority = Priority.Critical;
readonly kind = 'Functional';
readonly status = 'Approved';
readonly statement = {
pattern: 'event-driven',
trigger: 'When the build pipeline visits a content/**/*.md file',
response: 'parse YAML frontmatter, render Markdown to HTML, replace mermaid placeholders with inline SVG, and emit a static page reachable by its canonical URL',
};
readonly rationale = {
claim: 'Authoring stays human-readable; HTML is a build artifact, not the source of truth. The site must work without JavaScript and remain crawlable.',
kind: 'design-decision',
};
readonly fitCriteria = [
{
kind: 'demonstration' as const,
scenario: 'Run `npm run build:static` and open every page in toc.json with JS disabled — body, navigation, and diagrams render.',
},
{
kind: 'narrative' as const,
text: 'Build is hermetic: the content/ tree fully determines the published site.',
},
];
readonly verificationMethod = 'Test';
readonly source = {
type: 'internal',
decision: 'CV site authoring discipline',
date: '2026-04-01',
};
readonly risk = {
level: 'High',
ifNotMet: 'Authoring drifts to ad-hoc HTML; accessibility and SEO regress.',
mitigations: ['Strict Markdown linters', 'No raw HTML pages', 'Single render pipeline'],
};
}2. The Feature — requirements/features/markdown-rendering.ts
The feature imports the actual requirement class. Its abstract methods name the acceptance obligations.
import { Feature, Priority, Satisfies, type ACResult } from '@frenchexdev/requirements';
import { ContentMustLoadFromMarkdown } from '../requirements/content-loading';
@Satisfies(ContentMustLoadFromMarkdown)
export abstract class MarkdownPageRendering extends Feature {
readonly id = 'FEAT-MD-RENDER';
readonly title = 'Markdown Page Rendering';
readonly priority = Priority.Critical;
/** Frontmatter is parsed into typed page metadata before render. */
abstract frontmatterIsParsed(): ACResult;
/** `<!--mermaid ... -->` placeholders are replaced by inline SVG. */
abstract mermaidPlaceholdersAreReplaced(): ACResult;
/** Internal `[...](slug.md)` links resolve to canonical /<section>/<slug>/ URLs. */
abstract internalLinksResolveToCanonicalPaths(): ACResult;
}import { Feature, Priority, Satisfies, type ACResult } from '@frenchexdev/requirements';
import { ContentMustLoadFromMarkdown } from '../requirements/content-loading';
@Satisfies(ContentMustLoadFromMarkdown)
export abstract class MarkdownPageRendering extends Feature {
readonly id = 'FEAT-MD-RENDER';
readonly title = 'Markdown Page Rendering';
readonly priority = Priority.Critical;
/** Frontmatter is parsed into typed page metadata before render. */
abstract frontmatterIsParsed(): ACResult;
/** `<!--mermaid ... -->` placeholders are replaced by inline SVG. */
abstract mermaidPlaceholdersAreReplaced(): ACResult;
/** Internal `[...](slug.md)` links resolve to canonical /<section>/<slug>/ URLs. */
abstract internalLinksResolveToCanonicalPaths(): ACResult;
}The source documentation additionally expresses expected levels with @Expects(TestLevel.Unit, TestLevel.EndToEnd) and similar annotations. The obligations in this example are:
| Acceptance criterion | Expected levels / concerns in the source example |
|---|---|
frontmatterIsParsed |
Unit and end-to-end |
mermaidPlaceholdersAreReplaced |
Unit |
internalLinksResolveToCanonicalPaths |
End-to-end and visual |
Decorator compatibility matters. The portable excerpt above deliberately keeps decorators off abstract methods: ordinary TypeScript checking can reject a method decorator without an implementation. A version using that syntax needs its documented project-specific transformation or a compatible metadata representation. The expected-level obligations still need machine-readable metadata in the real integration; the table is an explanation, not an executable substitute.
3. The Tests — test/unit/markdown-rendering.test.ts
import { expect } from 'vitest';
import { FeatureTest, Verifies } from '@frenchexdev/requirements';
import { MarkdownPageRendering } from '../../requirements/features/markdown-rendering';
import { renderMarkdownPage } from '../../src/lib/markdown-renderer';
@FeatureTest(MarkdownPageRendering)
class MarkdownPageRenderingTests {
@Verifies<MarkdownPageRendering>('frontmatterIsParsed')
'extracts title and section from YAML frontmatter'() {
const page = renderMarkdownPage('---\ntitle: About\nsection: about\n---\n# Hello');
expect(page.meta.title).toBe('About');
expect(page.meta.section).toBe('about');
expect(page.body).toContain('<h1>Hello</h1>');
}
@Verifies<MarkdownPageRendering>('mermaidPlaceholdersAreReplaced')
'swaps mermaid placeholders for inline SVG'() {
const page = renderMarkdownPage('# Page\n<!--mermaid id=foo-->\n```mermaid\ngraph TD;A-->B\n```');
expect(page.body).toContain('<svg');
expect(page.body).not.toContain('<!--mermaid');
}
@Verifies<MarkdownPageRendering>('internalLinksResolveToCanonicalPaths')
'rewrites markdown links to canonical site URLs'() {
const page = renderMarkdownPage('See [the index](index.md) for more.');
expect(page.body).toContain('href="/index/"');
expect(page.body).not.toContain('.md');
}
}import { expect } from 'vitest';
import { FeatureTest, Verifies } from '@frenchexdev/requirements';
import { MarkdownPageRendering } from '../../requirements/features/markdown-rendering';
import { renderMarkdownPage } from '../../src/lib/markdown-renderer';
@FeatureTest(MarkdownPageRendering)
class MarkdownPageRenderingTests {
@Verifies<MarkdownPageRendering>('frontmatterIsParsed')
'extracts title and section from YAML frontmatter'() {
const page = renderMarkdownPage('---\ntitle: About\nsection: about\n---\n# Hello');
expect(page.meta.title).toBe('About');
expect(page.meta.section).toBe('about');
expect(page.body).toContain('<h1>Hello</h1>');
}
@Verifies<MarkdownPageRendering>('mermaidPlaceholdersAreReplaced')
'swaps mermaid placeholders for inline SVG'() {
const page = renderMarkdownPage('# Page\n<!--mermaid id=foo-->\n```mermaid\ngraph TD;A-->B\n```');
expect(page.body).toContain('<svg');
expect(page.body).not.toContain('<!--mermaid');
}
@Verifies<MarkdownPageRendering>('internalLinksResolveToCanonicalPaths')
'rewrites markdown links to canonical site URLs'() {
const page = renderMarkdownPage('See [the index](index.md) for more.');
expect(page.body).toContain('href="/index/"');
expect(page.body).not.toContain('.md');
}
}@Verifies<MarkdownPageRendering>('frontmatterIsParsed') constrains the supplied name through the feature's type. A stale literal can then fail type checking instead of remaining an unchecked tracker string. However, plain keyof F includes properties as well as methods: recognising that the selected member really is an AC requires a narrower type or semantic validation. It should not be assumed from keyof alone.
The class decorator and project test integration perform registration. Vitest does not discover these classes merely because decorators exist. The required adapter must load and register them. String-quoted method names are useful descriptions for the registered tests; banning bare describe / it is a repository policy described by the source, not a universal requirement of Requirements-as-Code.
These three unit tests illustrate bindings and assertions. They do not satisfy every level in the expectations table: actual end-to-end and visual verification still has to be provided. That distinction is exactly what a level-aware gate should expose.
The compliance scanner walks the declarations, joins feature identities and criterion names, and emits a BindingsManifest. The documented tooling also produces requirements-bindings.json, matrices and reports. A manifest records relationships; accepting a delivery additionally requires checking execution results and the applicable policy.
Lifecycle, Styles, and Branded Primitives
The documented primitives include RequirementId, FeatureId, AcName, IsoDate and Sentence. Branding distinguishes otherwise interchangeable primitive values in the type system. Input still needs validation at trust boundaries, and an unsafe cast is not validation.
Lifecycle vocabulary is supplied through a RequirementStyle. The Default style describes Draft → Approved → Implemented → Verified → Deprecated. The Industrial style adds Safety/Security/Customer context and FAT/SAT gates; Lean, Agile and Kanban styles adapt vocabulary and validators.
A lifecycle model is not the same as enforcement of every transition. A project must establish which transition checks run, which evidence is required and who may approve the change. Likewise, having an Industrial style is not evidence of certification against an industrial or safety standard.
More Than a Compliance Gate
The TypeScript package is designed as a library of command cores, not as business logic trapped inside a CLI. The source snapshot describes more than 35 sub-path exports and ports including FileSystem, Logger, Prompt, Vcs, FitCriterionAdapter and ToggleProvider.
The core accepts dependencies and output interfaces; the consumer chooses file access, interaction and presentation. This is the architectural reason the same logic can serve a CLI, a watch process or a future IDE/LSP consumer. Being designed for an LSP does not mean an LSP extension has already shipped.
| Capability group | Documented modules and purpose |
|---|---|
| Analysis and compliance | compliance-core, test-bindings-scanner, trace-graph, ac-suggester: inspect relationships, expose gaps and suggest criteria. |
| Authoring and synchronization | requirement-core, feature-new-core, spec-from-source, spec-to-source, bidi-sync-core: create artifacts and round-trip between supported source and spec forms. |
| Change and refactoring | watch-core, rename-core, versioning, toggling: monitor changes, perform coordinated renames and identify supported drift. |
| Verification scaffolding | scaffold-core and seven documented pluggable scaffolders, covering integrations such as Vitest, Playwright, axe, pa11y and Lighthouse. |
| History and extensibility | audit-hooks, optional signature hooks and injected ports for integration into the host workflow. |
The documented CLI includes:
npx requirements compliance --strict
npx requirements trace gaps
npx requirements scaffold e2e FEAT-MD-RENDERnpx requirements compliance --strict
npx requirements trace gaps
npx requirements scaffold e2e FEAT-MD-RENDERIn a reproducible pipeline, use the project's pinned package and documented scripts rather than relying on an unspecified package version being fetched at execution time.
contentHashOf(spec) and detectVersionDrift() address silent edits by comparing normalized content and version expectations. Since the documented hashing excludes NON_STRUCTURAL_FIELDS, a deployment must check which fields are excluded. A field excluded from the hash is outside that particular drift check, even when it matters to the business.
Bidirectional synchronization is useful for authoring, but it cannot create two independent authorities. The proposed governed workflow treats JSON edits as candidates that must normalize into the typed model, pass validation and receive approval before replacing the approved baseline. Generated Markdown remains a view.
rename-core is valuable because refactoring has to update declarations, specifications, tests and bindings coherently. Type checking catches invalid references after a rename; automatically rewriting every occurrence depends on the actual IDE or refactoring implementation, and should be tested rather than assumed.
Append-only audit hooks and optional signatures are integration points. Tamper resistance requires control over storage, signing identities and write permissions outside an agent's editable working tree.
A Real Report, With a Defined Scope
The original project page includes a ts-codegen-pipeline report for test-results/2026-05-08T06-17-07/. Its summary is:
Features: 19 active
Acceptance criteria: 143/143 ACs covered (100%)
Total tests linked to ACs: 261
Runtime coverage warnings: 0
Unbound features: 0
Critical uncovered: 0
Orphan source files: 0
Quality gate: PASSFeatures: 19 active
Acceptance criteria: 143/143 ACs covered (100%)
Total tests linked to ACs: 261
Runtime coverage warnings: 0
Unbound features: 0
Critical uncovered: 0
Orphan source files: 0
Quality gate: PASSThe source explicitly notes that legacy .js tests not migrated to .ts remain unlinked. The detailed table also includes source-coverage entries shown as src 0% (0 files). This report demonstrates the gate's output and a historical set of bindings; it should not be relabelled as current results, universal code coverage or a proof of business correctness.
An industrial report should make its denominator, exclusions, skipped checks and evidence revision as visible as its green status.
C#: FrenchExDev.Net.Requirements
The C# implementation belongs to the FrenchExDev .NET DSL family, alongside Dsl, Ddd and Diem. Its documented foundation combines a requirement hierarchy, attributes and a Roslyn IIncrementalGenerator.
C# offers a particularly direct expression of symbol references through typeof and nameof, together with nominal inheritance and generic constraints. TypeScript also supports generics and constraints; the difference is the concrete representation and tooling, not that one ecosystem has constraints and the other does not.
The Full Chain — Epic → Feature → Tests
The C# snapshot uses Epic at the requirement tier. That is an implementation vocabulary choice, not a claim that every business requirement is naturally an epic. The richer EARS, rationale, risk and provenance shape described on the TypeScript side is not presented as already feature-complete in C#.
As on the TypeScript side, these examples illustrate the documented API and assume the real package and rendering implementation. Namespaces have been made explicit to keep the three files consistent.
1. The Epic — Site.Requirements/ContentLoading.cs
using FrenchExDev.Net.Requirements;
using FrenchExDev.Net.Requirements.Attributes;
namespace Site.Requirements;
[MetaConcept]
public abstract class ContentLoading : Epic
{
public override string Title => "Page content authored in Markdown, rendered at build time";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "stephane.erard@gmail.com";
}using FrenchExDev.Net.Requirements;
using FrenchExDev.Net.Requirements.Attributes;
namespace Site.Requirements;
[MetaConcept]
public abstract class ContentLoading : Epic
{
public override string Title => "Page content authored in Markdown, rendered at build time";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "stephane.erard@gmail.com";
}2. The Feature — Site.Requirements/MarkdownRendering.cs
Feature<TParent> constrains its parent to an Epic in the documented model. A type that does not meet that constraint is rejected by the C# compiler. This is a structural rule; it does not establish that the chosen parent is the correct business requirement.
using FrenchExDev.Net.Requirements;
using FrenchExDev.Net.Requirements.Attributes;
namespace Site.Requirements;
[MetaConcept]
[ForRequirement(typeof(ContentLoading))]
public abstract class MarkdownRendering : Feature<ContentLoading>
{
public override string Title => "Markdown Page Rendering";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "stephane.erard@gmail.com";
/// <summary>Frontmatter is parsed into typed page metadata before render.</summary>
public abstract AcceptanceCriterionResult FrontmatterIsParsed();
/// <summary><![CDATA[<!--mermaid ... --> placeholders are replaced by inline SVG.]]></summary>
public abstract AcceptanceCriterionResult MermaidPlaceholdersAreReplaced();
/// <summary>Internal [text](slug.md) links resolve to canonical /section/slug/ URLs.</summary>
public abstract AcceptanceCriterionResult InternalLinksResolveToCanonicalPaths();
}using FrenchExDev.Net.Requirements;
using FrenchExDev.Net.Requirements.Attributes;
namespace Site.Requirements;
[MetaConcept]
[ForRequirement(typeof(ContentLoading))]
public abstract class MarkdownRendering : Feature<ContentLoading>
{
public override string Title => "Markdown Page Rendering";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "stephane.erard@gmail.com";
/// <summary>Frontmatter is parsed into typed page metadata before render.</summary>
public abstract AcceptanceCriterionResult FrontmatterIsParsed();
/// <summary><![CDATA[<!--mermaid ... --> placeholders are replaced by inline SVG.]]></summary>
public abstract AcceptanceCriterionResult MermaidPlaceholdersAreReplaced();
/// <summary>Internal [text](slug.md) links resolve to canonical /section/slug/ URLs.</summary>
public abstract AcceptanceCriterionResult InternalLinksResolveToCanonicalPaths();
}3. The Tests — Site.Requirements.Tests/MarkdownRenderingTests.cs
using Xunit;
using FrenchExDev.Net.Requirements.Attributes;
using Site.Rendering;
using Site.Requirements;
namespace Site.Requirements.Tests;
[TestsFor(typeof(MarkdownRendering))]
public class MarkdownRenderingTests
{
[Fact]
[Verifies(typeof(MarkdownRendering),
nameof(MarkdownRendering.FrontmatterIsParsed))]
public void Extracts_title_and_section_from_yaml_frontmatter()
{
var page = MarkdownRenderer.Render("---\ntitle: About\nsection: about\n---\n# Hello");
Assert.Equal("About", page.Meta.Title);
Assert.Equal("about", page.Meta.Section);
Assert.Contains("<h1>Hello</h1>", page.Body);
}
[Fact]
[Verifies(typeof(MarkdownRendering),
nameof(MarkdownRendering.MermaidPlaceholdersAreReplaced))]
public void Swaps_mermaid_placeholders_for_inline_svg()
{
var page = MarkdownRenderer.Render("# Page\n<!--mermaid id=foo-->\n```mermaid\ngraph TD;A-->B\n```");
Assert.Contains("<svg", page.Body);
Assert.DoesNotContain("<!--mermaid", page.Body);
}
[Fact]
[Verifies(typeof(MarkdownRendering),
nameof(MarkdownRendering.InternalLinksResolveToCanonicalPaths))]
public void Rewrites_markdown_links_to_canonical_site_urls()
{
var page = MarkdownRenderer.Render("See [the index](index.md) for more.");
Assert.Contains("href=\"/index/\"", page.Body);
Assert.DoesNotContain(".md", page.Body);
}
}using Xunit;
using FrenchExDev.Net.Requirements.Attributes;
using Site.Rendering;
using Site.Requirements;
namespace Site.Requirements.Tests;
[TestsFor(typeof(MarkdownRendering))]
public class MarkdownRenderingTests
{
[Fact]
[Verifies(typeof(MarkdownRendering),
nameof(MarkdownRendering.FrontmatterIsParsed))]
public void Extracts_title_and_section_from_yaml_frontmatter()
{
var page = MarkdownRenderer.Render("---\ntitle: About\nsection: about\n---\n# Hello");
Assert.Equal("About", page.Meta.Title);
Assert.Equal("about", page.Meta.Section);
Assert.Contains("<h1>Hello</h1>", page.Body);
}
[Fact]
[Verifies(typeof(MarkdownRendering),
nameof(MarkdownRendering.MermaidPlaceholdersAreReplaced))]
public void Swaps_mermaid_placeholders_for_inline_svg()
{
var page = MarkdownRenderer.Render("# Page\n<!--mermaid id=foo-->\n```mermaid\ngraph TD;A-->B\n```");
Assert.Contains("<svg", page.Body);
Assert.DoesNotContain("<!--mermaid", page.Body);
}
[Fact]
[Verifies(typeof(MarkdownRendering),
nameof(MarkdownRendering.InternalLinksResolveToCanonicalPaths))]
public void Rewrites_markdown_links_to_canonical_site_urls()
{
var page = MarkdownRenderer.Render("See [the index](index.md) for more.");
Assert.Contains("href=\"/index/\"", page.Body);
Assert.DoesNotContain(".md", page.Body);
}
}typeof(MarkdownRendering) refers to the feature type. nameof(MarkdownRendering.FrontmatterIsParsed) is resolved against a symbol at compile time and produces a string constant. Its benefit over a manually typed string is that a missing member is detected and symbol-aware refactoring can update the expression. It would be inaccurate to say that nameof “is not a string.”
There is another important boundary: an attribute accepting Type and string can receive two independently valid but mismatched arguments. typeof(FeatureA) and nameof(FeatureB.SomeCriterion) may each compile. Checking that the selected criterion belongs to the selected feature requires the analyzer or an API that encodes the pairing more tightly.
The Roslyn Generator + Analyzer Pair
The documented RequirementRegistryGenerator discovers abstract declarations in the hierarchy, identifies kinds such as Epic, Feature, Story, RequirementTask and Bug, reads supported metadata, collects abstract methods returning AcceptanceCriterionResult, and emits RequirementRegistry.g.cs.
Incremental generation makes registry construction part of compilation. Downstream consumers can use the generated metadata for reports or further checks. A generated registry is not itself a complete verification gate.
The following is illustrative registry output, using the top-level type layout of the three files above. It is not a fresh capture of generator execution:
using System;
using FrenchExDev.Net.Requirements;
namespace FrenchExDev.Net.Requirements.Generated;
public static class RequirementRegistry
{
public static readonly RequirementInfo[] All = new[]
{
new RequirementInfo(
kind: RequirementKind.Epic,
type: typeof(global::Site.Requirements.ContentLoading),
title: "Page content authored in Markdown, rendered at build time",
priority: RequirementPriority.Critical,
acceptanceCriteria: Array.Empty<string>(),
children: new[] { typeof(global::Site.Requirements.MarkdownRendering) }
),
new RequirementInfo(
kind: RequirementKind.Feature,
type: typeof(global::Site.Requirements.MarkdownRendering),
title: "Markdown Page Rendering",
priority: RequirementPriority.Critical,
acceptanceCriteria: new[]
{
nameof(global::Site.Requirements.MarkdownRendering.FrontmatterIsParsed),
nameof(global::Site.Requirements.MarkdownRendering.MermaidPlaceholdersAreReplaced),
nameof(global::Site.Requirements.MarkdownRendering.InternalLinksResolveToCanonicalPaths)
},
children: Array.Empty<Type>()
)
};
}using System;
using FrenchExDev.Net.Requirements;
namespace FrenchExDev.Net.Requirements.Generated;
public static class RequirementRegistry
{
public static readonly RequirementInfo[] All = new[]
{
new RequirementInfo(
kind: RequirementKind.Epic,
type: typeof(global::Site.Requirements.ContentLoading),
title: "Page content authored in Markdown, rendered at build time",
priority: RequirementPriority.Critical,
acceptanceCriteria: Array.Empty<string>(),
children: new[] { typeof(global::Site.Requirements.MarkdownRendering) }
),
new RequirementInfo(
kind: RequirementKind.Feature,
type: typeof(global::Site.Requirements.MarkdownRendering),
title: "Markdown Page Rendering",
priority: RequirementPriority.Critical,
acceptanceCriteria: new[]
{
nameof(global::Site.Requirements.MarkdownRendering.FrontmatterIsParsed),
nameof(global::Site.Requirements.MarkdownRendering.MermaidPlaceholdersAreReplaced),
nameof(global::Site.Requirements.MarkdownRendering.InternalLinksResolveToCanonicalPaths)
},
children: Array.Empty<Type>()
)
};
}The source documentation reserves REQ100–REQ302 for RequirementCoverageAnalyzer rules covering concerns such as orphans, uncovered critical criteria, lifecycle, provenance and test-level mismatches. That snapshot reports REQ100 as implemented and REQ101–REQ302 as scaffolded or in progress. This is the documented baseline, not a newly verified release inventory. Reserved diagnostic identifiers must not be marketed as working checks.
Before a rule becomes part of a contractual gate, the selected package version should demonstrate it with both valid and deliberately invalid fixtures. An analyzer also has to be included in the relevant projects and configured at a severity that actually blocks the intended workflow. A source generator is not a substitute for either of those steps.
The six documented projects remain:
FrenchExDev.Net.Requirements
FrenchExDev.Net.Requirements.Attributes
FrenchExDev.Net.Requirements.SourceGenerator
FrenchExDev.Net.Requirements.SourceGenerator.Lib
FrenchExDev.Net.Requirements.Analyzers
FrenchExDev.Net.Requirements.TestingFrenchExDev.Net.Requirements
FrenchExDev.Net.Requirements.Attributes
FrenchExDev.Net.Requirements.SourceGenerator
FrenchExDev.Net.Requirements.SourceGenerator.Lib
FrenchExDev.Net.Requirements.Analyzers
FrenchExDev.Net.Requirements.TestingThey separate core types, attributes and MetaConcept integration, generation, analysis and examples/tests. The ecosystem reuses its Result and Builder foundations. AcceptanceCriterionResult exposes Satisfied / Failed factories; the precise conversion behaviour should be taken from the pinned API rather than interpreted as a guarantee that failure cannot be ignored.
Same Idea, Two Asymmetries
| Concern | TypeScript | C# |
|---|---|---|
| Core representation | Requirement/Feature classes and abstract AC declarations. | Epic/Feature/Story/Task/Bug hierarchy and abstract AC declarations. |
| Identity and references | Class references, branded identifiers and type-constrained member-name literals. | typeof(T), nameof(T.Member) and constrained parent types. |
| Binding mechanism | Decorators, AST scanning and project-specific test registration. | Attributes, test framework integration and Roslyn analysis where implemented. |
| Analysis and generation | TypeScript compiler API / ts-morph; manifests and command cores. |
Incremental registry generator and diagnostic analyzers. |
| Lifecycle | Documented pluggable styles and validators. | Hierarchy is documented; that is not equivalent to full lifecycle/provenance parity. |
| Tooling surface | CLI, watch, rename, sync, versioning, graph, scaffolding and hooks. | Generated registry and documented analyzer foundation; broader tooling remains a roadmap. |
| Runtime evidence | Requires runner results to be associated with the approved model and artifacts. | Also requires runner results and a consumer/integration; compilation alone is insufficient. |
| IDE direction | Port-driven cores designed for future LSP/IDE consumers. | Compiler and analyzer integration provide the route to IDE feedback; richer extensions are planned. |
| Adoption evidence | Documented live use in the author's site and related TypeScript work. | Documented structural foundation; rule-by-rule release verification still matters. |
TypeScript goes broad; C# goes deep into its compiler ecosystem. That is an engineering emphasis, not a blanket superiority claim. The portable part is the meaning of the model and the expected relationships. Transport formats, symbol identities, analyzers and test adapters need explicit mappings.
A cross-language interchange should therefore be versioned and tested against shared fixtures. Exporting two registries does not automatically establish semantic equivalence. Stable external IDs remain useful when a requirement crosses repository or language boundaries; they complement, rather than replace, local symbol references.
AI-Assisted Workflows — Human Authority, Mechanical Feedback
The AI workflow starts before code generation, at the point where an intention is turned into a candidate contract. It has two different feedback loops: a human-controlled loop for deciding the obligation, and a mechanically checked loop for satisfying it.
The AI may propose requirements. It may not approve them on the stakeholder's behalf. After approval, it may not change the requirement baseline without explicit human authorisation.
This is the central control, not a prompt-writing preference. A model that can silently relax the criterion, remove the test or change the gate can make its own work pass without satisfying the original intention.
Workflow 1 — Discover, Review, Freeze
Start with a bounded slice: the business outcome, actors, constraints, explicit exclusions and unresolved questions. Ask the model to produce candidates in the project's typed vocabulary, not to turn every assumption into an approved fact.
The human review has three outcomes: green approves a specific candidate and version; red rejects it; refine sends it back with questions or changes. Agreement includes measurable fit criteria and risk, not only a feature title.
Freeze the accepted slice as a versioned baseline. “Freeze” means controlled change, not waterfall: the baseline can evolve through the same review loop. Its purpose is to distinguish an authorised change of goal from an implementation that drifted away from the goal.
A generated stakeholder view should show the same baseline in readable language. Editing a Markdown page or a JSON export starts a new candidate change; it does not silently supersede the typed contract.
Workflow 2 — Derive Verification, Then Implement
From the approved requirement slice, derive or refine the verification obligations. This is a separate activity from deciding how the production code will work. AI may propose criteria and scenarios, but the human owner approves their adequacy before they become the implementation target.
For each acceptance criterion, specify the relevant level, concern, observable result, test environment, risk and evidence requirement. These are proposed governance fields where not already present in the implementation.
| Example concern | Suitable checks to consider | Why one green unit test is insufficient |
|---|---|---|
| Correct provisioning | Unit checks of decisions, integration checks of orchestration, end-to-end creation and teardown. | A correct plan can still fail against real permissions or providers. |
| Isolation and access | Negative authorisation scenarios and deployment-level isolation checks. | Mocked dependencies do not establish the real security boundary. |
| Repeatability | Re-apply/retry scenarios, idempotency assertions and cleanup verification. | A successful first execution does not establish safe repetition. |
| Performance | Measurements under an approved load, dataset and environment. | A fast local test is not evidence for a production-scale target. |
| Operability | Failure handling, diagnostic output, rollback/recovery and ownership checks. | Successful provisioning does not prove recoverability or support readiness. |
These are examples of verification design, not features claimed to be built into either library.
Give the implementation agent a bounded context bundle: the approved baseline, relevant feature/AC declarations, allowed source areas, interfaces and ports, architecture decisions, test conventions, dependencies and exact validation commands. Unresolved assumptions belong in an explicit questions section.
The bundle should be reproducible from a baseline and revision. It is a useful proposed consumer of the registry/graph, not a claim that every context-pack feature has already shipped.
The agent then writes tests and implementation within the authorised scope. Where practical, establish an observed failure against the previous implementation before accepting the change. Otherwise a passing test may never have been able to detect the missing behaviour.
approved baseline
→ reviewed verification obligations
→ tests and implementation changes
→ type checking and semantic binding analysis
→ test execution at the required levels
→ source coverage and risk-appropriate mutation/fuzz/property checks
→ versioned evidence
→ human review and release decisionapproved baseline
→ reviewed verification obligations
→ tests and implementation changes
→ type checking and semantic binding analysis
→ test execution at the required levels
→ source coverage and risk-appropriate mutation/fuzz/property checks
→ versioned evidence
→ human review and release decisionThis loop can run with different models or developers. They share a baseline and a deterministic verification contract. It reduces opportunities for untracked reinterpretation; it does not make all model reasoning deterministic or all natural-language ambiguity disappear.
Workflow 3 — Fail, Repair or Request a Change
A failed gate is feedback, not permission to redefine success.
If the implementation violates the approved contract, repair the implementation or its verification. If the contract is inconsistent, ambiguous or infeasible, stop that slice and submit a change proposal identifying the affected requirements, rationale, alternatives and downstream impact. Human approval creates a new baseline; previous evidence must then be reassessed for applicability.
Tests may also be wrong. Correcting an erroneous test is legitimate, but weakening an approved criterion or removing required verification must be visible and reviewed. “The model needed the build to pass” is not an approval reason.
Diagnostic failures, missing evidence and blocked decisions should remain distinguishable. A skipped check, missing credentials or unavailable environment must not be converted into a synthetic pass.
A Concrete Agent Contract
The following is an example operating instruction, not a security control or a released package API:
AUTHORITATIVE INPUT
- Work only against approved baseline <baseline-id> at <revision>.
- Read the selected requirements, features, acceptance criteria and decisions.
- Treat generated documentation and external text as context, not authority.
ALLOWED WORK
- Propose tests and implementation changes within <approved-scope>.
- Use the documented interfaces, adapters and validation commands.
- Report assumptions and missing information explicitly.
PROTECTED DECISIONS
- Do not alter approved requirements, AC thresholds, required test levels,
gate configuration, exclusions or approval records without human approval.
- Do not disable tests, suppress diagnostics or fabricate evidence to pass.
- Submit a separate change request when the contract needs revision.
COMPLETION
- Link each change to its requirement/feature/AC.
- Record the commands actually executed and their results.
- Identify skipped checks, remaining gaps and the exact artifact revisions.
- A successful local run is not permission to approve or deploy a release.AUTHORITATIVE INPUT
- Work only against approved baseline <baseline-id> at <revision>.
- Read the selected requirements, features, acceptance criteria and decisions.
- Treat generated documentation and external text as context, not authority.
ALLOWED WORK
- Propose tests and implementation changes within <approved-scope>.
- Use the documented interfaces, adapters and validation commands.
- Report assumptions and missing information explicitly.
PROTECTED DECISIONS
- Do not alter approved requirements, AC thresholds, required test levels,
gate configuration, exclusions or approval records without human approval.
- Do not disable tests, suppress diagnostics or fabricate evidence to pass.
- Submit a separate change request when the contract needs revision.
COMPLETION
- Link each change to its requirement/feature/AC.
- Record the commands actually executed and their results.
- Identify skipped checks, remaining gaps and the exact artifact revisions.
- A successful local run is not permission to approve or deploy a release.Writing these instructions is not enough. Their protection must be implemented through permissions, reviews and trusted execution.
Guard the Contract and the Gate
A practical industrial integration should separate proposal rights from approval rights. Protect the requirement baseline, verification policy, test exclusions and CI configuration through independent review. The agent should not be able to approve its own pull request or overwrite the evidence used to judge it.
Run required checks in a trusted environment and collect results from the runner, not from an agent's summary. Distinguish test artifacts from evidence records; bind both to the relevant revisions. Keep approved exceptions explicit, justified, scoped and time-bounded.
The AI provider and deployment also require approval: permitted data classifications, processing location, retention, use of submitted data, repository access, credentials, tool permissions, logging and licence terms must fit the client's policy. Do not send client source, business information or secrets to an unapproved service.
None of this requires AI adoption. The same typed model and gates are useful in a human-only workflow. AI is a producer operating within the system, not the authority over its obligations.
Requirement as Code Meets Infrastructure as Code
Infrastructure as Code expresses infrastructure configuration and desired state. Requirements-as-Code addresses a different question: which approved obligation justifies this capability, and which checks will support its acceptance?
The point is not “replace Terraform” or “wrap every infrastructure resource in C#.” Keep the execution tools appropriate to the platform. Connect their outputs to an explicit intent and verification model.
The following is a proposed integration architecture. The supplied implementations do not establish shipped Terraform or Ansible adapters, a complete ADR subsystem or an enterprise release orchestrator.
Example: A Governed Environment-Provisioning Capability
Consider an illustrative, not deployed, requirement: an approved team must obtain an isolated environment within a stakeholder-agreed time bound, with attributable access and safe teardown.
The corresponding work may span identity, networking, a provisioning module, configuration automation, a pipeline and operational handover. The useful relationship is not merely a ticket ID copied into all six repositories. It is a queryable set of declared obligations and verification responsibilities.
| Workstream | Delivery artifact | Example acceptance evidence |
|---|---|---|
| Architecture | Chosen design and recorded constraints/trade-offs. | Review against the approved business and operational requirements. |
| Identity and security | Access model and policy configuration. | Approved positive and negative authorisation scenarios. |
| Infrastructure | Versioned provisioning modules and configuration. | Validation, integration runs and checks against deployed resources. |
| Delivery | Reproducible pipeline and release/rollback procedures. | Results tied to module, configuration and baseline revisions. |
| Operations | Runbook, monitoring, teardown/recovery procedure and named owner. | An exercised handover or recovery scenario appropriate to the risk. |
A change in the required isolation model should trigger impact review of the recorded dependants. A missing dependency remains a modelling gap; the tool must not claim to infer an unrecorded architecture automatically.
Governance That Helps Delivery
For a Lead Solution Architect or Automation Delivery Lead, this model supports questions that matter across workstreams: who owns an obligation, which architecture decision addresses it, which team supplies the capability, what is missing before acceptance, and which evidence still applies after a change?
The practical outputs can include an approved requirement baseline, decision records, a dependency map, reusable automation patterns, a verification matrix, reproducible reports and operational handover criteria. Those outputs need owners and an adoption process; adding a DSL does not create decision rights or stakeholder agreement.
Start with a bounded pilot where requirements and verification are already painful. Establish the actual baseline: where work is approved, which tools own which records, what counts as acceptance and who can approve exceptions. Add a thin traceable slice through the existing toolchain rather than requiring a replacement of every tracker and pipeline.
Measure useful outcomes: detection of broken links, evidence freshness, time to analyse a change, onboarding effort, escaped requirement defects and maintenance cost. Do not use the number of typed classes or a green percentage as the sole measure of success.
Where Each One Sits
The distinction between a concept, a live implementation and industrial qualification should stay visible.
| Area | Documented baseline | What should not be inferred |
|---|---|---|
| TypeScript core | Live dogfooding on the author's site and related work; CLI, scanning, sync/refactoring/versioning and ports described in the source. | A fresh audit of every export, large-enterprise adoption, or a shipped LSP extension. |
| C# core | Typed hierarchy, attributes and incremental registry described; the source snapshot reports REQ100 implemented. | Completion of the reserved REQ101–REQ302 analyzer suite or parity with TypeScript. |
| Cross-language model | Shared intent: requirements, features, ACs, bindings and verification relationships. | A verified lossless interchange or identical lifecycle semantics. |
| AI-assisted delivery | Typed declarations and reports support bounded context and mechanical feedback. | An agent-proof approval system, provider authorisation or automatically trustworthy evidence. |
| IaC / enterprise governance | A coherent integration pattern described in this article. | Released Terraform/Ansible integrations, certification or a turnkey governance platform. |
The C# roadmap includes the remaining analyzer rules, richer requirement metadata, trace-graph/reporting integration and richer Visual Studio/Rider experiences. TypeScript's ports provide a basis for IDE/LSP consumers. Those are directions, not substitutes for a versioned release record.
For an industrial pilot, the next proof points are concrete: pin the packages; exercise each claimed rejection rule with negative fixtures; verify scanner and test discovery; document supported decorator/transformation settings; validate level-aware evidence handling; benchmark performance at the intended scale; protect approval and gate configuration; and show a maintainable upgrade path.
The wider FrenchExDev work remains relevant. The Métacratie cross-compiler explores traceability across legal sources and derived models; legal validity still requires competent review, not only compilation. The planned Ide.Dsl-TS direction can consume the TypeScript ports directly. These are related consumers and research directions, not additional evidence of production qualification.
The Core Promise
Requirements-as-Code is not “the compiler proves the business is right.” It is a way to make a selected part of the problem–solution contract explicit enough that tools can detect broken structure, missing verification relationships and policy violations before they are accepted as delivery.
Humans own intent and approval. Types and analyzers constrain the declared relationships. Tests challenge the implementation. Evidence records what actually happened. AI can accelerate the work without acquiring the right to redefine success.
Two implementations, one direction: keep approved intent connected to delivery through a model that developers, tools and AI agents can all work against — and make every claimed guarantee traceable to an actual mechanism.
Further Reading and Technical Boundaries
Spec-Driven vs Typed Specifications presents the earlier comparison under the prototype name. AI-Driven Self-Implementation describes the use of feature declarations, test conventions, dependency ports and feedback in an agent workflow. The human-approval and protected-baseline workflow in this V2 makes the authority boundary explicit.
Language details: C# nameof (ouvre dans une nouvelle fenêtre), C# generic constraints (ouvre dans une nouvelle fenêtre), TypeScript keyof (ouvre dans une nouvelle fenêtre), and TypeScript decorators (ouvre dans une nouvelle fenêtre).
Editorial baseline: 4 September 2026. Implementation descriptions derive from the supplied project documentation; code excerpts were not executed against the complete C# or TypeScript packages for this revision. The historical CLI report is dated explicitly. Proposed AI governance, evidence hardening and infrastructure integrations are architecture patterns, not release claims.