Chapter 11 — EARS, Agile, and Kanban
Industrial rewrites the lifecycle. Lean rewrites the evidence chain. The three here each specialise one narrower axis.
Chapters 09 and 10 each took a full chapter because their registers demand structural changes at the Requirement type level. IndustrialStyle ships a thirteen-state lifecycle and a SIL hierarchy that the Default four-state workflow cannot absorb. LeanStyle replaces the evidence model with PDCA loops and A3 reports. Those are not "more vocabulary" — they are different shapes.
The three registers in this chapter are narrower in scope. EARS is a sentence-pattern convention usable inside any Style; DefaultStyle already ships it. AgileStyle is a compact specialisation focused on the artefact shape of backlog items — user stories with INVEST validation, Gherkin acceptance scenarios, Scrum/Kanban board columns. KanbanStyle is narrower still: it targets flow orchestration, Classes of Service, cost-of-delay curves, and lead-time SLEs.
Treating them together keeps the relative weights honest. A chapter of the same length as 09 would misrepresent how much structural novelty each one actually brings. So: three briefer deep-dives, one chapter, one diagram.
Why these three are grouped
The axis each register targets is different from the other two.
EARS targets the sentence — the grammar of a single requirement statement. The rest of the spec (kind, status, rationale, source, verification method) is unaffected. When DefaultStyle declares five EARS patterns in statementPatterns, the Style itself has not become "EARS-flavoured"; it has merely made EARS the default grammar for its statements. Any Style — Industrial, Lean, Agile, Kanban — could import and reuse those same patterns, and DefaultStyle does.
AgileStyle targets the artefact shape — what counts as a well-formed backlog item. A user story is not the same kind of thing as an EARS sentence; it carries a role, a capability, a benefit, a Gherkin acceptance fragment, an optional epic link, a story-point estimate. The validator enforces five INVEST-aligned rules on top of the base four (statement, spec, kind, status). The Style replaces requirementKinds (UserStory, Epic, Enabler, Spike, TechnicalDebt, Bug, NonFunctional, Research), the statusWorkflow (Backlog → Refined → Ready → InProgress → InReview → Done → Released → Archived, with rework loops), and the riskTaxonomy (Showstopper / High / Medium / Low / Trivial). But it does not invent a new lifecycle gate model the way Industrial does.
KanbanStyle targets the flow-orchestration axis. Its requirementKinds are Anderson's Classes of Service. Its statusWorkflow is a flow board. Its riskTaxonomy encodes the shape of the cost-of-delay curve, not an ISO SIL level. Its verificationMethods are flow-based evidence (p85 lead time, cumulative flow diagram health). WIP limits as such are not modelled inside the Style (they are a runtime policy, not a spec field); what the Style does model is the validator discipline that keeps each Class of Service well-formed.
Three different disciplines, three different axes, three compact Styles. The best map is a single diagram at the end of the chapter — but first, each one on its own.
EARS — a sentence pattern, not a register
EARS stands for Easy Approach to Requirements Syntax. Alistair Mavin and colleagues published it at IEEE RE'09 while at Rolls-Royce. The problem it solves is the one that ruins natural-language requirements everywhere: ambiguity about whether a statement is ubiquitous ("the system always does X"), triggered ("when Y happens, the system does X"), conditional on state ("while in Y, the system does X"), optional ("where Y is present, the system does X"), or defensive ("if the bad thing Y happens, the system does X").
Five patterns. A single-paragraph convention that any team can adopt without tooling. But the tooling helps — because once the pattern is a discriminant, a validator can check that the slots required by each pattern are filled.
In @frenchexdev/requirements, EARS is modelled at the statement level inside DefaultStyle. Here is the statementPatterns array, lifted verbatim from packages/requirements/src/styles/default.ts:
statementPatterns: [
{ pattern: 'ubiquitous', label: 'Ubiquitous',
template: 'The system shall {response}.',
slots: [ { name: 'response', required: true, hint: 'what the system does' } ] },
{ pattern: 'event-driven', label: 'Event-driven',
template: 'When {trigger}, the system shall {response}.',
slots: [
{ name: 'trigger', required: true, hint: 'the triggering event' },
{ name: 'response', required: true, hint: 'what the system does in response' },
] },
{ pattern: 'state-driven', label: 'State-driven',
template: 'While {state}, the system shall {response}.',
slots: [
{ name: 'state', required: true },
{ name: 'response', required: true },
] },
{ pattern: 'optional', label: 'Optional feature',
template: 'Where {feature}, the system shall {response}.',
slots: [
{ name: 'feature', required: true },
{ name: 'response', required: true },
] },
{ pattern: 'unwanted', label: 'Unwanted behaviour',
template: 'If {trigger}, then the system shall {response}.',
slots: [
{ name: 'trigger', required: true },
{ name: 'response', required: true },
] },
{ pattern: 'natural', label: 'Natural (fallback — generates warning)',
template: '{text}',
slots: [ { name: 'text', required: true } ] },
],statementPatterns: [
{ pattern: 'ubiquitous', label: 'Ubiquitous',
template: 'The system shall {response}.',
slots: [ { name: 'response', required: true, hint: 'what the system does' } ] },
{ pattern: 'event-driven', label: 'Event-driven',
template: 'When {trigger}, the system shall {response}.',
slots: [
{ name: 'trigger', required: true, hint: 'the triggering event' },
{ name: 'response', required: true, hint: 'what the system does in response' },
] },
{ pattern: 'state-driven', label: 'State-driven',
template: 'While {state}, the system shall {response}.',
slots: [
{ name: 'state', required: true },
{ name: 'response', required: true },
] },
{ pattern: 'optional', label: 'Optional feature',
template: 'Where {feature}, the system shall {response}.',
slots: [
{ name: 'feature', required: true },
{ name: 'response', required: true },
] },
{ pattern: 'unwanted', label: 'Unwanted behaviour',
template: 'If {trigger}, then the system shall {response}.',
slots: [
{ name: 'trigger', required: true },
{ name: 'response', required: true },
] },
{ pattern: 'natural', label: 'Natural (fallback — generates warning)',
template: '{text}',
slots: [ { name: 'text', required: true } ] },
],Six entries: five EARS patterns plus a natural fallback that is kept intentionally ugly. A natural statement is a plain string of prose; the Style accepts it (no required slots missing other than text) but tooling higher up emits a warning. The point is to let teams migrate in — you can land an imperfect spec in natural today and promote it to a proper EARS pattern next week — without pretending that prose is structurally equivalent.
The discriminated union
The JSON Schema shape of a statement is determined by its pattern tag. In TypeScript this is modelled as a discriminated union — EarsStatement in the package types:
type EarsStatement =
| { pattern: 'ubiquitous'; response: Sentence }
| { pattern: 'event-driven'; trigger: Sentence; response: Sentence }
| { pattern: 'state-driven'; state: Sentence; response: Sentence }
| { pattern: 'optional'; feature: Sentence; response: Sentence }
| { pattern: 'unwanted'; trigger: Sentence; response: Sentence }
| { pattern: 'natural'; text: Sentence };type EarsStatement =
| { pattern: 'ubiquitous'; response: Sentence }
| { pattern: 'event-driven'; trigger: Sentence; response: Sentence }
| { pattern: 'state-driven'; state: Sentence; response: Sentence }
| { pattern: 'optional'; feature: Sentence; response: Sentence }
| { pattern: 'unwanted'; trigger: Sentence; response: Sentence }
| { pattern: 'natural'; text: Sentence };Sentence is a branded string — a plain string on the wire, a nominal type at compile time, validated by its smart constructor. The discriminant pattern is what the validator switches on. The error the validator emits for a missing slot is not a generic "required string" — it carries the pattern name:
// from DEFAULT_VALIDATORS.validateStatement, packages/requirements/src/styles/default.ts
for (const slot of schema.slots) {
if (slot.required && (typeof s[slot.name] !== 'string' || (s[slot.name] as string).length === 0)) {
return err(`statement.${slot.name}`, `required slot missing for pattern '${schema.pattern}'`);
}
}// from DEFAULT_VALIDATORS.validateStatement, packages/requirements/src/styles/default.ts
for (const slot of schema.slots) {
if (slot.required && (typeof s[slot.name] !== 'string' || (s[slot.name] as string).length === 0)) {
return err(`statement.${slot.name}`, `required slot missing for pattern '${schema.pattern}'`);
}
}So when a user picks event-driven from the wizard and forgets to fill the trigger slot, the error reads "required slot missing for pattern 'event-driven'", not "statement.trigger is required". The error message teaches the pattern.
Each pattern in use
The best way to feel the difference is to rewrite one Requirement five ways. Take a hypothetical rule about the test scaffolder:
Ubiquitous — The system shall emit
@FeatureTest+@Verifiesdecorators on every generated test file.{ "pattern": "ubiquitous", "response": "emit @FeatureTest + @Verifies decorators on every generated test file" }{ "pattern": "ubiquitous", "response": "emit @FeatureTest + @Verifies decorators on every generated test file" }Always-active. No trigger, no state. This is the vast majority of functional requirements — the default choice if no other pattern fits.
Event-driven — When the CLI is invoked with
scaffold test <id>, the system shall create an@FeatureTest-annotated file at the configured path.{ "pattern": "event-driven", "trigger": "the CLI is invoked with scaffold test <id>", "response": "create an @FeatureTest-annotated file at the configured path" }{ "pattern": "event-driven", "trigger": "the CLI is invoked with scaffold test <id>", "response": "create an @FeatureTest-annotated file at the configured path" }A discrete event triggers a discrete response. Note how the
triggerslot eats the ambiguity — it is always clear what starts the behaviour.State-driven — While the current working directory is outside a
@frenchexdev/requirementsproject, the system shall refuse to runscaffold test.{ "pattern": "state-driven", "state": "the current working directory is outside a @frenchexdev/requirements project", "response": "refuse to run scaffold test" }{ "pattern": "state-driven", "state": "the current working directory is outside a @frenchexdev/requirements project", "response": "refuse to run scaffold test" }A condition holds; the behaviour applies for the duration. Useful for modes, contexts, feature flags in their "active" incarnation.
Optional — Where the project's
testDirsByLevelconfiguration includese2e, the system shall emit end-to-end scaffolds using that directory.{ "pattern": "optional", "feature": "the project's testDirsByLevel configuration includes e2e", "response": "emit end-to-end scaffolds using that directory" }{ "pattern": "optional", "feature": "the project's testDirsByLevel configuration includes e2e", "response": "emit end-to-end scaffolds using that directory" }Presence-conditional. The distinction from state-driven is subtle but useful:
optionalis about configuration or variant selection,state-drivenis about runtime state.Unwanted behaviour — If the filesystem port reports a write failure while scaffolding, then the system shall restore the previous contents and abort.
{ "pattern": "unwanted", "trigger": "the filesystem port reports a write failure while scaffolding", "response": "restore the previous contents and abort" }{ "pattern": "unwanted", "trigger": "the filesystem port reports a write failure while scaffolding", "response": "restore the previous contents and abort" }Defensive. The separation of
event-drivenfromunwantedis a discipline — it forces the author to mark whether the trigger is a normal expected event or an exceptional one. The two statements render to different English (When … vs If …, then …), and the risk register can treat them differently downstream.
REQ-DOG-FOOD as a ubiquitous statement
The running-example Requirement of this whole series, REQ-DOG-FOOD, is an ubiquitous EARS statement. Its specification (in packages/requirements/requirements/req-dog-food.ts) carries a statement of shape:
{ "pattern": "ubiquitous",
"response": "test every part of the DSL with the DSL itself — zero describe / zero it in the package test suite" }{ "pattern": "ubiquitous",
"response": "test every part of the DSL with the DSL itself — zero describe / zero it in the package test suite" }When the DEFAULT_REPORTER.renderStatement function gets hold of this object, it walks the pattern's template 'The system shall {response}.', substitutes the {response} slot, and emits:
The system shall test every part of the DSL with the DSL itself — zero describe / zero it in the package test suite.
That is the canonical sentence the series quotes. It is also, literally, what the JSON at rest says — rendered through a pure function with no side effects. The same shape feeds the Markdown reporter, the CLI report, the editor binding, and the JSON Schema validator. One datum, six surfaces.
EARS as a compositional piece
The crucial point about EARS in this package: it is not a Style. It is a contribution to a Style's statementPatterns array. DefaultStyle imports the five patterns into its vocabulary; so does (implicitly) any other Style that chooses to reuse them. IndustrialStyle ships its own statement patterns — "a safety function shall …", fixed to SIL levels — but it could freely mix in the five EARS patterns if a project wanted both. There is no boundary that says "Industrial cannot use EARS". The Style system is compositional at the vocabulary layer.
This matters for the open-closed principle. A project migrating from plain-English requirements to a typed spec wants to adopt EARS first, before they decide whether their Style will ultimately be Default, Industrial, or something custom. The package lets them: EARS lives at the statement level, the Style choice lives one layer up, and the two decisions are independent.
AgileStyle — INVEST and BDD acceptance
AgileStyle is rooted in the Agile Manifesto (Beck et al. 2001), the Scrum Guide (Sutherland & Schwaber), User Stories Applied (Cohn), the INVEST criteria (Wake), Given-When-Then / Gherkin (Dan North), Definition of Ready & Definition of Done, Story Points, SAFe's Epics and Enablers, and XP. The header comment in packages/requirements/src/styles/agile.ts lists the whole genealogy. The Style is the TypeScript expression of that lineage.
Vocabulary — kinds, states, risks, sources
Seven kinds, deliberately mirroring real Scrum/SAFe vocabulary:
requirementKinds: [
'UserStory',
'Epic',
'Enabler',
'Spike',
'TechnicalDebt',
'Bug',
'NonFunctional',
'Research',
],requirementKinds: [
'UserStory',
'Epic',
'Enabler',
'Spike',
'TechnicalDebt',
'Bug',
'NonFunctional',
'Research',
],Eight lifecycle states with rework loops for refinement blockers and review-to-dev bounce-backs:
statusWorkflow: {
states: ['Backlog', 'Refined', 'Ready', 'InProgress',
'InReview', 'Done', 'Released', 'Archived'],
initial: 'Backlog',
transitions: [
{ from: 'Backlog', to: 'Refined' },
{ from: 'Refined', to: 'Ready' },
{ from: 'Ready', to: 'InProgress' },
{ from: 'InProgress', to: 'InReview' },
{ from: 'InReview', to: 'Done' },
{ from: 'Done', to: 'Released' },
{ from: 'Released', to: 'Archived' },
// Rework: review sends work back to dev
{ from: 'InReview', to: 'InProgress' },
// Refinement blocker: a Ready story re-opens for clarification
{ from: 'Ready', to: 'Refined' },
],
terminal: ['Archived'],
},statusWorkflow: {
states: ['Backlog', 'Refined', 'Ready', 'InProgress',
'InReview', 'Done', 'Released', 'Archived'],
initial: 'Backlog',
transitions: [
{ from: 'Backlog', to: 'Refined' },
{ from: 'Refined', to: 'Ready' },
{ from: 'Ready', to: 'InProgress' },
{ from: 'InProgress', to: 'InReview' },
{ from: 'InReview', to: 'Done' },
{ from: 'Done', to: 'Released' },
{ from: 'Released', to: 'Archived' },
// Rework: review sends work back to dev
{ from: 'InReview', to: 'InProgress' },
// Refinement blocker: a Ready story re-opens for clarification
{ from: 'Ready', to: 'Refined' },
],
terminal: ['Archived'],
},The rework transitions are the interesting ones. A style-system that only admitted forward flow would lie about how agile teams actually work. The InReview → InProgress loop encodes the "review caught something, back to dev" reality; the Ready → Refined loop encodes "we thought this was ready but a refinement blocker surfaced". Both are real, both are legal, both are typed.
Risks use plain-English severity levels (Showstopper, High, Medium, Low, Trivial) paired with a likelihood × impact matrix so that the same risk register is usable by a PO and a test lead without translation.
Sources are eight agile-native kinds: product-owner, user-interview, market-research, sprint-retrospective, customer-support, backlog-grooming, technical-debt-register, product-kpi. A Requirement whose source is a customer-support ticket carries the ticketId, channel and date in its typed slots; the sync tool can surface them, the compliance report can link back to them.
Statement patterns — Connextra, Gherkin, SAFe
AgileStyle does not reuse EARS. It ships its own grammar:
statementPatterns: [
{ pattern: 'user-story',
label: 'User story (Connextra)',
template: 'As a {role}, I want {capability}, so that {benefit}.',
slots: [
{ name: 'role', required: true, hint: 'e.g. returning customer' },
{ name: 'capability', required: true, hint: 'what they want to do' },
{ name: 'benefit', required: true, hint: 'why they want it — the INVEST "V"' },
] },
{ pattern: 'given-when-then',
label: 'Acceptance scenario (Gherkin / BDD)',
template: 'Given {precondition}, when {trigger}, then {outcome}.',
slots: [
{ name: 'precondition', required: true },
{ name: 'trigger', required: true },
{ name: 'outcome', required: true },
] },
{ pattern: 'epic', /* ... Epic: {epic}. Business outcome: {outcome}. Measured by {kpi}. */ },
{ pattern: 'enabler', /* ... Technical enabler: {capability}. Required by {userStories}. Risk mitigation: {riskMitigated}. */ },
{ pattern: 'spike', /* ... Spike: investigate {question}. Timebox: {timebox}. Deliverable: {deliverable}. */ },
{ pattern: 'bug', /* ... Bug: {summary}. Expected: {expected}. Actual: {actual}. Repro: {repro}. */ },
{ pattern: 'natural', /* fallback warning */ },
],statementPatterns: [
{ pattern: 'user-story',
label: 'User story (Connextra)',
template: 'As a {role}, I want {capability}, so that {benefit}.',
slots: [
{ name: 'role', required: true, hint: 'e.g. returning customer' },
{ name: 'capability', required: true, hint: 'what they want to do' },
{ name: 'benefit', required: true, hint: 'why they want it — the INVEST "V"' },
] },
{ pattern: 'given-when-then',
label: 'Acceptance scenario (Gherkin / BDD)',
template: 'Given {precondition}, when {trigger}, then {outcome}.',
slots: [
{ name: 'precondition', required: true },
{ name: 'trigger', required: true },
{ name: 'outcome', required: true },
] },
{ pattern: 'epic', /* ... Epic: {epic}. Business outcome: {outcome}. Measured by {kpi}. */ },
{ pattern: 'enabler', /* ... Technical enabler: {capability}. Required by {userStories}. Risk mitigation: {riskMitigated}. */ },
{ pattern: 'spike', /* ... Spike: investigate {question}. Timebox: {timebox}. Deliverable: {deliverable}. */ },
{ pattern: 'bug', /* ... Bug: {summary}. Expected: {expected}. Actual: {actual}. Repro: {repro}. */ },
{ pattern: 'natural', /* fallback warning */ },
],The Connextra user-story template — As a X, I want Y, so that Z — is the default. The Gherkin Given/When/Then is a second, separate pattern; a single user story can exist as a user-story statement and have one or more given-when-then companion statements attached as acceptance criteria. SAFe's Epic carries a mandatory kpi — the measurable outcome without which an epic is just prose. SAFe's Enabler carries a userStories slot that names the downstream stories it unblocks and a riskMitigated slot so that technical enablers are never accepted without a reason. XP's Spike carries a timebox and a deliverable. Bug carries summary / expected / actual / repro.
Each pattern's template is a printable sentence. Each slot carries a hint so that the requirement new wizard can surface the right inline help. The same patterns feed the CLI report, the Markdown reporter, the JSON Schema, and the editor binding.
The validator — five INVEST-grounded rules
Here is where AgileStyle earns its keep. The INVEST criteria (Independent, Negotiable, Valuable, Estimable, Small, Testable) are a vocabulary every PO knows. Most of I-N-E-S-T is not enforceable by a pure validator — "Independent" and "Negotiable" live in backlog-grooming conversations; "Estimable" and "Small" surface in planning poker, not at spec-edit time; "Testable" can be surfaced but only weakly (is there a fit criterion? is there a given-when-then companion?). But "Valuable" — the V — is enforceable. A user-story statement without a benefit slot is, literally, a feature spec wearing a user-story template, not a user story.
The validator enforces five rules, all reading from packages/requirements/src/styles/agile.ts:
// RULE 1: UserStory kind must be expressed as user-story or given-when-then.
if (s.requirementKind === 'UserStory') {
if (pattern !== 'user-story' && pattern !== 'given-when-then') {
errors.push({ path: 'statement.pattern', message: "UserStory kind requires 'user-story' or 'given-when-then' pattern" });
}
}
// RULE 2: Epic requires the epic pattern with a non-empty kpi.
if (s.requirementKind === 'Epic') {
if (pattern !== 'epic') {
errors.push({ path: 'statement.pattern', message: "Epic kind requires 'epic' pattern" });
} else {
const kpi = statement?.kpi as string | undefined;
if (!kpi || kpi.length === 0) {
errors.push({ path: 'statement.kpi', message: 'Epic requires a non-empty kpi (measurable outcome)' });
}
}
}
// RULE 3: Spike requires a timebox — an open-ended spike is not a spike.
if (s.requirementKind === 'Spike') {
if (pattern !== 'spike') {
errors.push({ path: 'statement.pattern', message: "Spike kind requires 'spike' pattern" });
} else if (!statement?.timebox) {
errors.push({ path: 'statement.timebox', message: 'Spike requires a non-empty timebox' });
}
}
// RULE 4: Bug requires reproducible steps — "works on my machine" is not a bug report.
if (s.requirementKind === 'Bug') {
if (pattern !== 'bug') {
errors.push({ path: 'statement.pattern', message: "Bug kind requires 'bug' pattern" });
} else if (!statement?.repro) {
errors.push({ path: 'statement.repro', message: 'Bug requires non-empty repro steps' });
}
}
// RULE 5: INVEST "Valuable" — a user-story without benefit is a feature spec, not a story.
if (pattern === 'user-story') {
const benefit = statement?.benefit as string | undefined;
if (!benefit || benefit.length === 0) {
errors.push({ path: 'statement.benefit', message: 'INVEST: user-story must carry a non-empty benefit slot (the "V" in INVEST)' });
}
}// RULE 1: UserStory kind must be expressed as user-story or given-when-then.
if (s.requirementKind === 'UserStory') {
if (pattern !== 'user-story' && pattern !== 'given-when-then') {
errors.push({ path: 'statement.pattern', message: "UserStory kind requires 'user-story' or 'given-when-then' pattern" });
}
}
// RULE 2: Epic requires the epic pattern with a non-empty kpi.
if (s.requirementKind === 'Epic') {
if (pattern !== 'epic') {
errors.push({ path: 'statement.pattern', message: "Epic kind requires 'epic' pattern" });
} else {
const kpi = statement?.kpi as string | undefined;
if (!kpi || kpi.length === 0) {
errors.push({ path: 'statement.kpi', message: 'Epic requires a non-empty kpi (measurable outcome)' });
}
}
}
// RULE 3: Spike requires a timebox — an open-ended spike is not a spike.
if (s.requirementKind === 'Spike') {
if (pattern !== 'spike') {
errors.push({ path: 'statement.pattern', message: "Spike kind requires 'spike' pattern" });
} else if (!statement?.timebox) {
errors.push({ path: 'statement.timebox', message: 'Spike requires a non-empty timebox' });
}
}
// RULE 4: Bug requires reproducible steps — "works on my machine" is not a bug report.
if (s.requirementKind === 'Bug') {
if (pattern !== 'bug') {
errors.push({ path: 'statement.pattern', message: "Bug kind requires 'bug' pattern" });
} else if (!statement?.repro) {
errors.push({ path: 'statement.repro', message: 'Bug requires non-empty repro steps' });
}
}
// RULE 5: INVEST "Valuable" — a user-story without benefit is a feature spec, not a story.
if (pattern === 'user-story') {
const benefit = statement?.benefit as string | undefined;
if (!benefit || benefit.length === 0) {
errors.push({ path: 'statement.benefit', message: 'INVEST: user-story must carry a non-empty benefit slot (the "V" in INVEST)' });
}
}Five rules, each with an error message that teaches the discipline the rule comes from. Rule 5 literally cites INVEST in its error text; Rule 4 uses the phrase "works on my machine" as the thing it is refusing. The validator is not a silent accept/reject; it is a classroom.
The rules also cohere. Rule 1 forces the right pattern for a UserStory; Rule 5 then forces the right slot inside the user-story pattern. A violation of either fires a distinct error at a distinct path. No ambiguity, no cascading nonsense. The JSON path in the error — statement.benefit, statement.kpi, statement.timebox, statement.repro — points the editor plugin at exactly the field to highlight.
A worked AgileStyle example
Take a hypothetical project rule: new hires on an in-project onboarding flow must finish a guided tour before they can modify a feature. Rewritten as an AgileStyle Requirement in three layers:
Layer 1 — the user story (the backlog artefact):
{
$schemaVersion: '2026-04-14',
kind: 'requirement',
id: 'REQ-ONBOARDING-GUIDED-TOUR',
title: 'New hires finish a guided tour before editing any feature',
requirementKind: 'UserStory',
statement: {
pattern: 'user-story',
role: 'new hire on day one',
capability: 'complete a guided tour of the traceability graph in under 20 minutes',
benefit: 'I can modify a feature without corrupting the trace',
},
priority: 'Medium',
status: 'Ready',
// ...
}{
$schemaVersion: '2026-04-14',
kind: 'requirement',
id: 'REQ-ONBOARDING-GUIDED-TOUR',
title: 'New hires finish a guided tour before editing any feature',
requirementKind: 'UserStory',
statement: {
pattern: 'user-story',
role: 'new hire on day one',
capability: 'complete a guided tour of the traceability graph in under 20 minutes',
benefit: 'I can modify a feature without corrupting the trace',
},
priority: 'Medium',
status: 'Ready',
// ...
}The Connextra sentence renders as: As a new hire on day one, I want to complete a guided tour of the traceability graph in under 20 minutes, so that I can modify a feature without corrupting the trace. Rule 1 passes (UserStory kind, user-story pattern). Rule 5 passes (benefit is present and non-empty).
Layer 2 — the acceptance scenario (Gherkin, attached as a companion statement):
{
pattern: 'given-when-then',
precondition: 'a new hire has logged in for the first time and has never modified a feature',
trigger: 'they attempt to save a change to any feature file',
outcome: 'the save is blocked with a link to the guided tour',
}{
pattern: 'given-when-then',
precondition: 'a new hire has logged in for the first time and has never modified a feature',
trigger: 'they attempt to save a change to any feature file',
outcome: 'the save is blocked with a link to the guided tour',
}The Gherkin sentence renders as: Given a new hire has logged in for the first time and has never modified a feature, when they attempt to save a change to any feature file, then the save is blocked with a link to the guided tour. One Given, one When, one Then, all three slots typed.
Layer 3 — the story-card Markdown. The AGILE_REPORTER.renderMarkdown walks the spec and emits:
# REQ-ONBOARDING-GUIDED-TOUR — New hires finish a guided tour before editing any feature
> As a new hire on day one, I want to complete a guided tour of the traceability graph in under 20 minutes, so that I can modify a feature without corrupting the trace.
## Acceptance criteria
- [ ] Given a new hire has logged in for the first time and has never modified a feature, when they attempt to save a change to any feature file, then the save is blocked with a link to the guided tour.
- [ ] (additional AC)
---
**Source**: product-owner · **Priority**: Medium · **Status**: Ready · **Risk**: Medium# REQ-ONBOARDING-GUIDED-TOUR — New hires finish a guided tour before editing any feature
> As a new hire on day one, I want to complete a guided tour of the traceability graph in under 20 minutes, so that I can modify a feature without corrupting the trace.
## Acceptance criteria
- [ ] Given a new hire has logged in for the first time and has never modified a feature, when they attempt to save a change to any feature file, then the save is blocked with a link to the guided tour.
- [ ] (additional AC)
---
**Source**: product-owner · **Priority**: Medium · **Status**: Ready · **Risk**: MediumA printable story card, generated from the same datum that the validator checks, the editor binds, and the JSON Schema validates. The card is a reporter output; the story is the typed record.
AgileStyle in context
AgileStyle fits projects already on Scrum / XP / SAFe. It adds typed discipline to the "story" artefact without forcing the team into a heavier framework. A team can adopt it gradually: write new stories in AgileStyle, leave legacy tickets in DefaultStyle, and the compliance report will handle both. The five validator rules are a small, memorable set; the error messages teach the rules; the templates make the wizard emit correct scaffolds on the first try.
What AgileStyle does not do is model WIP limits, cost-of-delay curves, or flow metrics. That is the next Style's job.
KanbanStyle — Classes of Service and WIP
KanbanStyle, rooted in David J. Anderson's Kanban (2010), Goldratt's Theory of Constraints, Deming's flow principles, Little's Law, and the operational mechanics of cumulative flow diagrams and service-level expectations, is narrower than AgileStyle. Anderson's central insight — that work is pulled, not pushed, and that the unit of decision is the cost-of-delay curve, not the story-point estimate — becomes the Style's organising principle.
Classes of Service as requirementKinds
Anderson names four classes of service: Standard (FIFO pulled by the cost-of-delay curve), Expedite (drop everything, one in flight maximum), Fixed-Date (hard deadline, penalty if missed), and Intangible (no immediate cost of delay, but matters long-term). The Style adds two common extensions: LifeCycleReset (work that resets a cadence / refresh cycle) and Defect (production defect pulled back into flow). All six live where DefaultStyle puts Functional / NonFunctional:
requirementKinds: [
'Standard', // FIFO / cost-of-delay curve
'Expedite', // drop everything, one in flight max
'FixedDate', // hard deadline, penalty if missed
'Intangible', // no immediate cost of delay, but matters long-term
'LifeCycleReset', // work that resets a cadence / refresh cycle
'Defect', // production defect pulled back into flow
],requirementKinds: [
'Standard', // FIFO / cost-of-delay curve
'Expedite', // drop everything, one in flight max
'FixedDate', // hard deadline, penalty if missed
'Intangible', // no immediate cost of delay, but matters long-term
'LifeCycleReset', // work that resets a cadence / refresh cycle
'Defect', // production defect pulled back into flow
],Note the choice. Many Kanban implementations keep the Class of Service as a tag on a generic ticket. Here it is the requirementKind — the most structural axis a spec has. The validator then knows which slots each class must carry, and can demand cost-of-delay for Expedite, a deadline for FixedDate, and a promotion threshold for Intangible.
The flow workflow
Nine lifecycle states, with rework loops that encode the escaped-defect reality every operations team lives with:
statusWorkflow: {
states: ['Backlog', 'Selected', 'Analysis', 'Development',
'Testing', 'Ready', 'Deployed', 'Observed', 'Archived'],
initial: 'Backlog',
transitions: [
{ from: 'Backlog', to: 'Selected' },
{ from: 'Selected', to: 'Analysis' },
{ from: 'Analysis', to: 'Development' },
{ from: 'Development', to: 'Testing' },
{ from: 'Testing', to: 'Ready' },
{ from: 'Ready', to: 'Deployed' },
{ from: 'Deployed', to: 'Observed' },
{ from: 'Observed', to: 'Archived' },
// Rework loops
{ from: 'Testing', to: 'Development' },
{ from: 'Ready', to: 'Development' }, // escaped defect caught pre-deploy
// Pull-back to backlog from early columns
{ from: 'Selected', to: 'Backlog' },
{ from: 'Analysis', to: 'Backlog' },
],
terminal: ['Archived'],
},statusWorkflow: {
states: ['Backlog', 'Selected', 'Analysis', 'Development',
'Testing', 'Ready', 'Deployed', 'Observed', 'Archived'],
initial: 'Backlog',
transitions: [
{ from: 'Backlog', to: 'Selected' },
{ from: 'Selected', to: 'Analysis' },
{ from: 'Analysis', to: 'Development' },
{ from: 'Development', to: 'Testing' },
{ from: 'Testing', to: 'Ready' },
{ from: 'Ready', to: 'Deployed' },
{ from: 'Deployed', to: 'Observed' },
{ from: 'Observed', to: 'Archived' },
// Rework loops
{ from: 'Testing', to: 'Development' },
{ from: 'Ready', to: 'Development' }, // escaped defect caught pre-deploy
// Pull-back to backlog from early columns
{ from: 'Selected', to: 'Backlog' },
{ from: 'Analysis', to: 'Backlog' },
],
terminal: ['Archived'],
},Testing → Development is the standard bounce-back; Ready → Development is the rarer "we caught a defect in the last mile before deploy" loop; Selected → Backlog and Analysis → Backlog are the intake-discipline loops that let a team pull a ticket back to the backlog when it turns out not to be ready. WIP limits, in Kanban practice, are enforced by the team's tooling (the board software, the policy on each column). The Style does not inline them as fields — they are a policy concern, not a spec concern — but the transitions and columns are there, ready for a WIP-enforcing tool to sit on top.
Risk as cost-of-delay curve shape
In Kanban, "risk" does not mean SIL level or likelihood × impact in the safety-engineering sense. It means the shape of the cost-of-delay curve:
riskTaxonomy: {
levels: [
'DelayHarmUrgent', // steep curve — expedite territory
'DelayHarmFixedDate', // step-function at the deadline
'DelayHarmLinear', // standard cost-of-delay
'DelayHarmIntangible', // flat short-term, rising long-term
'DelayHarmMinimal', // near-zero cost of delay
],
matrix: { /* likelihood × impact → curve-shape level */ },
},riskTaxonomy: {
levels: [
'DelayHarmUrgent', // steep curve — expedite territory
'DelayHarmFixedDate', // step-function at the deadline
'DelayHarmLinear', // standard cost-of-delay
'DelayHarmIntangible', // flat short-term, rising long-term
'DelayHarmMinimal', // near-zero cost of delay
],
matrix: { /* likelihood × impact → curve-shape level */ },
},Each level names a curve shape, not a SIL tier. "DelayHarmUrgent" is a steep curve — the cost of delay rises sharply per unit time, so expedite. "DelayHarmFixedDate" is a step function — cost is near zero until the deadline, then catastrophic. "DelayHarmLinear" is the standard cost-of-delay that most Standard-class work follows. "DelayHarmIntangible" is flat in the short term but rises long-term (technical debt is the canonical example). "DelayHarmMinimal" is near-zero.
This taxonomy is the reason a team running Kanban cannot just use AgileStyle. Agile's risk taxonomy is severity; Kanban's risk taxonomy is curve-shape. They describe different things. Trying to squash them into one would produce either an over-generic or an over-specific result.
Verification — flow-based evidence
verificationMethods: [
'LeadTimeWithinSLE', // p85 lead time ≤ SLE
'DeployedToProduction', // reached Deployed column
'ObservedInProduction', // post-deploy telemetry confirms behaviour
'BlockerCleared', // expedite: the blocker is gone
'SlaMet', // customer SLA target hit
'FlowMetricGreen', // WIP / age / throughput within policy
'CumulativeFlowHealthy', // CFD bands are parallel, no growing queues
],verificationMethods: [
'LeadTimeWithinSLE', // p85 lead time ≤ SLE
'DeployedToProduction', // reached Deployed column
'ObservedInProduction', // post-deploy telemetry confirms behaviour
'BlockerCleared', // expedite: the blocker is gone
'SlaMet', // customer SLA target hit
'FlowMetricGreen', // WIP / age / throughput within policy
'CumulativeFlowHealthy', // CFD bands are parallel, no growing queues
],A Kanban team does not verify a ticket by running a unit test suite (though unit tests can still be a fit criterion). It verifies flow: the p85 lead time met its SLE, the cumulative flow diagram stays healthy, the expedite blocker was cleared, the customer SLA target was hit. These methods are flow-native and each one points at a specific metric or artefact.
Statement patterns — one per Class of Service
statementPatterns: [
{ pattern: 'service-request',
template: 'Deliverable: {outcome}. Customer: {customer}. Done when: {doneWhen}. Class of service: {cos}.',
slots: [ 'outcome', 'customer', 'doneWhen', 'cos' ] },
{ pattern: 'fixed-date-commitment',
template: 'By {deadline}, the system shall {deliverable}. Penalty if missed: {penalty}.',
slots: [ 'deadline', 'deliverable', 'penalty' ] },
{ pattern: 'expedite-pull',
template: 'Expedite pull: {work}. Blocking: {blocking}. Cost of delay: {cod}.',
slots: [ 'work', 'blocking', 'cod' ] },
{ pattern: 'intangible-investment',
template: 'Intangible work: {work}. Long-term benefit: {benefit}. Threshold for surfacing as Standard: {threshold}.',
slots: [ 'work', 'benefit', 'threshold' ] },
{ pattern: 'defect-pull',
template: 'Defect in {area}: {symptom}. Detected by {detection}. Reset lifecycle: {reset}.',
slots: [ 'area', 'symptom', 'detection', 'reset' ] },
{ pattern: 'sla-commitment',
template: 'Service: {service}. Lead-time SLE: {p85} at 85th percentile. Class of service: {cos}.',
slots: [ 'service', 'p85', 'cos' ] },
{ pattern: 'natural', /* fallback */ },
],statementPatterns: [
{ pattern: 'service-request',
template: 'Deliverable: {outcome}. Customer: {customer}. Done when: {doneWhen}. Class of service: {cos}.',
slots: [ 'outcome', 'customer', 'doneWhen', 'cos' ] },
{ pattern: 'fixed-date-commitment',
template: 'By {deadline}, the system shall {deliverable}. Penalty if missed: {penalty}.',
slots: [ 'deadline', 'deliverable', 'penalty' ] },
{ pattern: 'expedite-pull',
template: 'Expedite pull: {work}. Blocking: {blocking}. Cost of delay: {cod}.',
slots: [ 'work', 'blocking', 'cod' ] },
{ pattern: 'intangible-investment',
template: 'Intangible work: {work}. Long-term benefit: {benefit}. Threshold for surfacing as Standard: {threshold}.',
slots: [ 'work', 'benefit', 'threshold' ] },
{ pattern: 'defect-pull',
template: 'Defect in {area}: {symptom}. Detected by {detection}. Reset lifecycle: {reset}.',
slots: [ 'area', 'symptom', 'detection', 'reset' ] },
{ pattern: 'sla-commitment',
template: 'Service: {service}. Lead-time SLE: {p85} at 85th percentile. Class of service: {cos}.',
slots: [ 'service', 'p85', 'cos' ] },
{ pattern: 'natural', /* fallback */ },
],Each pattern matches a Class of Service. The slots are the ones a flow-discipline book would demand: an expedite pull without a quantified cost of delay is not really an expedite; an intangible investment without a promotion threshold is an intangible that will silently never surface.
The validator — four flow-discipline rules
// DISCIPLINE: Expedite class requires expedite-pull pattern with a quantified Cost of Delay.
if (s.requirementKind === 'Expedite') {
if (statement?.pattern !== 'expedite-pull') {
errors.push({ path: 'statement.pattern', message: "Expedite kind requires statement.pattern 'expedite-pull'" });
} else if (typeof statement.cod !== 'string' || (statement.cod as string).length === 0) {
errors.push({ path: 'statement.cod', message: 'Expedite kind requires a non-empty cost-of-delay (cod) slot' });
}
}
// DISCIPLINE: FixedDate class requires fixed-date-commitment with non-empty deadline.
if (s.requirementKind === 'FixedDate') {
if (statement?.pattern !== 'fixed-date-commitment') {
errors.push({ path: 'statement.pattern', message: "FixedDate kind requires statement.pattern 'fixed-date-commitment'" });
} else if (typeof statement.deadline !== 'string' || (statement.deadline as string).length === 0) {
errors.push({ path: 'statement.deadline', message: 'FixedDate kind requires a non-empty deadline slot' });
}
}
// DISCIPLINE (Anderson): Intangibles must carry an explicit promotion threshold.
if (s.requirementKind === 'Intangible') {
if (statement?.pattern !== 'intangible-investment') {
errors.push({ path: 'statement.pattern', message: "Intangible kind requires statement.pattern 'intangible-investment'" });
} else if (typeof statement.threshold !== 'string' || (statement.threshold as string).length === 0) {
errors.push({ path: 'statement.threshold', message: 'Intangible kind requires an explicit promotion threshold (Anderson rule)' });
}
}
// FLOW DISCIPLINE: Standard work should be decided from flow evidence, not opinion.
if (s.requirementKind === 'Standard') {
const source = s.source as Record<string, unknown> | undefined;
const t = source?.type as string | undefined;
const flowSources = ['flow-metric', 'cumulative-flow-diagram', 'lead-time-distribution', 'customer-sla'];
if (!t || !flowSources.includes(t)) {
errors.push({ path: 'source.type', message: `Standard kind should cite a flow-driven source (${flowSources.join(', ')})` });
}
}// DISCIPLINE: Expedite class requires expedite-pull pattern with a quantified Cost of Delay.
if (s.requirementKind === 'Expedite') {
if (statement?.pattern !== 'expedite-pull') {
errors.push({ path: 'statement.pattern', message: "Expedite kind requires statement.pattern 'expedite-pull'" });
} else if (typeof statement.cod !== 'string' || (statement.cod as string).length === 0) {
errors.push({ path: 'statement.cod', message: 'Expedite kind requires a non-empty cost-of-delay (cod) slot' });
}
}
// DISCIPLINE: FixedDate class requires fixed-date-commitment with non-empty deadline.
if (s.requirementKind === 'FixedDate') {
if (statement?.pattern !== 'fixed-date-commitment') {
errors.push({ path: 'statement.pattern', message: "FixedDate kind requires statement.pattern 'fixed-date-commitment'" });
} else if (typeof statement.deadline !== 'string' || (statement.deadline as string).length === 0) {
errors.push({ path: 'statement.deadline', message: 'FixedDate kind requires a non-empty deadline slot' });
}
}
// DISCIPLINE (Anderson): Intangibles must carry an explicit promotion threshold.
if (s.requirementKind === 'Intangible') {
if (statement?.pattern !== 'intangible-investment') {
errors.push({ path: 'statement.pattern', message: "Intangible kind requires statement.pattern 'intangible-investment'" });
} else if (typeof statement.threshold !== 'string' || (statement.threshold as string).length === 0) {
errors.push({ path: 'statement.threshold', message: 'Intangible kind requires an explicit promotion threshold (Anderson rule)' });
}
}
// FLOW DISCIPLINE: Standard work should be decided from flow evidence, not opinion.
if (s.requirementKind === 'Standard') {
const source = s.source as Record<string, unknown> | undefined;
const t = source?.type as string | undefined;
const flowSources = ['flow-metric', 'cumulative-flow-diagram', 'lead-time-distribution', 'customer-sla'];
if (!t || !flowSources.includes(t)) {
errors.push({ path: 'source.type', message: `Standard kind should cite a flow-driven source (${flowSources.join(', ')})` });
}
}Four rules. Each one refuses a common Kanban failure mode: Expedite without cost of delay (expediting on vibes), FixedDate without a deadline (a "deadline" without a date), Intangible without a promotion threshold (Anderson's warning — intangibles that never surface), and Standard with an opinion-based source (rather than a flow-metric / CFD / lead-time / customer-SLA). The fourth rule is the softest — it emits an error by default, but the Style's design note explicitly allows a higher-layer policy to downgrade it to a warning. That is honest about what is enforcement and what is norm.
A worked KanbanStyle example
Take a production defect in the CLI's scaffolder: a race condition corrupts test files when scaffolding is triggered twice in under 100 ms. The team tags it Expedite.
{
$schemaVersion: '2026-04-14',
kind: 'requirement',
id: 'REQ-SCAFFOLD-RACE-EXPEDITE',
title: 'Fix scaffolder race condition on rapid double-invocation',
requirementKind: 'Expedite',
statement: {
pattern: 'expedite-pull',
work: 'atomically serialise scaffold writes by target file path',
blocking: 'three customer projects cannot run CI because scaffolds corrupt under parallel invocation',
cod: '1 customer escalation per business day until fixed; 2 customers on 48h termination notice',
},
priority: 'Critical',
status: 'Development',
risk: { level: 'DelayHarmUrgent' },
verificationMethod: 'BlockerCleared',
source: { type: 'blocked-ticket-report', /* ... */ },
// ... optional: lead-time SLE
}{
$schemaVersion: '2026-04-14',
kind: 'requirement',
id: 'REQ-SCAFFOLD-RACE-EXPEDITE',
title: 'Fix scaffolder race condition on rapid double-invocation',
requirementKind: 'Expedite',
statement: {
pattern: 'expedite-pull',
work: 'atomically serialise scaffold writes by target file path',
blocking: 'three customer projects cannot run CI because scaffolds corrupt under parallel invocation',
cod: '1 customer escalation per business day until fixed; 2 customers on 48h termination notice',
},
priority: 'Critical',
status: 'Development',
risk: { level: 'DelayHarmUrgent' },
verificationMethod: 'BlockerCleared',
source: { type: 'blocked-ticket-report', /* ... */ },
// ... optional: lead-time SLE
}The expedite-pull sentence renders as: Expedite pull: atomically serialise scaffold writes by target file path. Blocking: three customer projects cannot run CI because scaffolds corrupt under parallel invocation. Cost of delay: 1 customer escalation per business day until fixed; 2 customers on 48h termination notice. The cod slot is non-empty (rule 1 passes). The requirementKind is Expedite, the pattern is expedite-pull (rule 1 passes). The risk is DelayHarmUrgent, consistent with the steep curve.
A companion sla-commitment statement could pin a 24-hour cycle-time SLA on the Expedite class generally: Service: scaffolder. Lead-time SLE: 24 hours at 85th percentile. Class of service: Expedite. That SLA lives in its own Requirement record, referenced by the Expedite ticket's tracedTo field.
The KANBAN_REPORTER.renderMarkdown walks the spec and produces a ticket-card Markdown:
# [Expedite] REQ-SCAFFOLD-RACE-EXPEDITE — Fix scaffolder race condition on rapid double-invocation
`Development` · priority: Critical · verification: BlockerCleared
**Cost of delay**: 1 customer escalation per business day until fixed; 2 customers on 48h termination notice
**Blocking**: three customer projects cannot run CI because scaffolds corrupt under parallel invocation
## Statement
> Expedite pull: atomically serialise scaffold writes by target file path. Blocking: three customer projects cannot run CI because scaffolds corrupt under parallel invocation. Cost of delay: 1 customer escalation per business day until fixed; 2 customers on 48h termination notice.
## Done when
- [ ] race-condition unit test passes with 1000 parallel invocations
- [ ] customer pilot projects confirm resolution in their CI
**Delay profile**: DelayHarmUrgent
---
_history_: Backlog@2026-04-12 → Selected@2026-04-12 → Analysis@2026-04-13 → Development@2026-04-14# [Expedite] REQ-SCAFFOLD-RACE-EXPEDITE — Fix scaffolder race condition on rapid double-invocation
`Development` · priority: Critical · verification: BlockerCleared
**Cost of delay**: 1 customer escalation per business day until fixed; 2 customers on 48h termination notice
**Blocking**: three customer projects cannot run CI because scaffolds corrupt under parallel invocation
## Statement
> Expedite pull: atomically serialise scaffold writes by target file path. Blocking: three customer projects cannot run CI because scaffolds corrupt under parallel invocation. Cost of delay: 1 customer escalation per business day until fixed; 2 customers on 48h termination notice.
## Done when
- [ ] race-condition unit test passes with 1000 parallel invocations
- [ ] customer pilot projects confirm resolution in their CI
**Delay profile**: DelayHarmUrgent
---
_history_: Backlog@2026-04-12 → Selected@2026-04-12 → Analysis@2026-04-13 → Development@2026-04-14The cost-of-delay is promoted to the card header; the class-of-service badge is next to the ID; the delay profile is a dedicated section; the history timeline is rendered as a crumb trail. None of this is special-cased in the reporter logic — it falls out of the vocabulary and the typed slots. Swap the pattern from expedite-pull to fixed-date-commitment, and the header surfaces a **Due**: … block instead of **Cost of delay**: …. The reporter is a pure function of the pattern.
KanbanStyle in context
KanbanStyle is narrower than AgileStyle. It does not model story points, epics, spikes, or the full Scrum ceremony vocabulary. What it does model is the flow-orchestration discipline — classes of service, cost-of-delay curves, SLEs, flow-based verification. A team running Kanban on top of operational or support work will find it a better fit than AgileStyle; a team running a feature backlog will find AgileStyle the better fit. Nothing prevents a project from using both Styles on different Requirement subsets — that is exactly what the registry is for.
Diagram — three axes, one registry
Three registers, three different axes, one picture.
{% mermaid caption="EARS, AgileStyle, KanbanStyle — the three axes each specialises and where the discipline lives. EARS contributes a sentence-pattern discipline at the statement level, usable inside any Style. AgileStyle contributes an artefact-shape discipline at the requirement level (INVEST, Gherkin). KanbanStyle contributes a flow-orchestration discipline at the workflow level (classes of service, cost-of-delay, SLEs). The three are additive, not mutually exclusive." alt="Flowchart with three columns. Left column: EARS with five pattern nodes (ubiquitous, event-driven, state-driven, optional, unwanted) feeding into a Sentence node. Middle column: AgileStyle with four nodes (user-story, given-when-then, epic, spike) feeding into an Artefact node; an INVEST-V-Rule arrow points at user-story. Right column: KanbanStyle with four nodes (expedite-pull, fixed-date-commitment, intangible-investment, sla-commitment) feeding into a Flow-Orchestration node; cost-of-delay, deadline, threshold rules arrow into the respective nodes. All three columns feed into a single StyleRegistry node at the bottom." %}
flowchart TB
subgraph ears["EARS — sentence pattern axis"]
direction TB
ubiq["ubiquitous
(The system shall ...)"]
evnt["event-driven
(When X, the system shall ...)"]
stat["state-driven
(While X, the system shall ...)"]
opt["optional
(Where X, the system shall ...)"]
unw["unwanted
(If X then Y, the system shall ...)"]
sent["Statement grammar"]
ubiq --> sent
evnt --> sent
stat --> sent
opt --> sent
unw --> sent
end
subgraph agile["AgileStyle — artefact shape axis"]
direction TB
us["user-story<br/>(As a X, I want Y, so that Z)"]
gwt["given-when-then<br/>(Given X, when Y, then Z)"]
ep["epic<br/>(Epic + KPI)"]
sp["spike<br/>(Question + timebox)"]
art["Artefact shape"]
us --> art
gwt --> art
ep --> art
sp --> art
iv["INVEST-V rule:<br/>user-story requires benefit"] -.enforces.-> us
end
subgraph kanban["KanbanStyle — flow orchestration axis"]
direction TB
exp["expedite-pull<br/>(Cost of delay)"]
fd["fixed-date-commitment<br/>(Deadline + penalty)"]
intan["intangible-investment<br/>(Promotion threshold)"]
sla["sla-commitment<br/>(p85 lead time)"]
flow["Flow orchestration"]
exp --> flow
fd --> flow
intan --> flow
sla --> flow
codR["cod rule:<br/>Expedite requires cod"] -.enforces.-> exp
ddR["deadline rule:<br/>FixedDate requires deadline"] -.enforces.-> fd
thR["threshold rule:<br/>Intangible requires threshold"] -.enforces.-> intan
end
reg["StyleRegistry<br/>BUILT_IN_STYLES"]
sent --> reg
art --> reg
flow --> regsubgraph agile["AgileStyle — artefact shape axis"]
direction TB
us["user-story<br/>(As a X, I want Y, so that Z)"]
gwt["given-when-then<br/>(Given X, when Y, then Z)"]
ep["epic<br/>(Epic + KPI)"]
sp["spike<br/>(Question + timebox)"]
art["Artefact shape"]
us --> art
gwt --> art
ep --> art
sp --> art
iv["INVEST-V rule:<br/>user-story requires benefit"] -.enforces.-> us
end
subgraph kanban["KanbanStyle — flow orchestration axis"]
direction TB
exp["expedite-pull<br/>(Cost of delay)"]
fd["fixed-date-commitment<br/>(Deadline + penalty)"]
intan["intangible-investment<br/>(Promotion threshold)"]
sla["sla-commitment<br/>(p85 lead time)"]
flow["Flow orchestration"]
exp --> flow
fd --> flow
intan --> flow
sla --> flow
codR["cod rule:<br/>Expedite requires cod"] -.enforces.-> exp
ddR["deadline rule:<br/>FixedDate requires deadline"] -.enforces.-> fd
thR["threshold rule:<br/>Intangible requires threshold"] -.enforces.-> intan
end
reg["StyleRegistry<br/>BUILT_IN_STYLES"]
sent --> reg
art --> reg
flow --> reg{% endmermaid %}
Three columns, one registry. EARS on the left — five patterns into a Sentence-grammar node. AgileStyle in the middle — four patterns into an Artefact-shape node, with the INVEST-V rule drawn as an enforcing arrow at the user-story node. KanbanStyle on the right — four patterns into a Flow-orchestration node, with three enforcing rules at expedite-pull, fixed-date-commitment, and intangible-investment. All three feed the single registry at the bottom.
The diagram makes the scope claim concrete. EARS is narrower than AgileStyle — it contributes at the statement level only. AgileStyle is narrower than a Style-level rewrite (Industrial, Lean) — it contributes at the artefact shape only, keeping the rest of the Style system intact. KanbanStyle is narrowest in functional scope — it targets flow orchestration — but its specialisation is sharp: the four validator rules catch four specific failure modes that Kanban practice has learned to guard against.
How the three compose
The three axes are orthogonal, so the three registers compose cleanly.
An AgileStyle Requirement can still use an EARS pattern. The INVEST validator does not prohibit it. A team that wants Connextra and EARS can add EARS patterns to their AgileStyle-derived Style's statementPatterns array and have a user-story statement plus an event-driven statement attached as an acceptance-level criterion. The patterns are just entries in an array; the validator switches on the pattern tag. (natural is always there as a fallback.)
A KanbanStyle Requirement can still be written in EARS. The service-request pattern carries the shape of a Kanban ticket, but an underlying technical requirement attached to that ticket can perfectly well be event-driven — in which case the event-driven statement lives on a child Requirement linked via @Refines, and the parent ticket's class-of-service discipline is not in conflict with the child's EARS discipline.
A Class-of-Service annotation can be added to a DefaultStyle Requirement without migrating the whole project to KanbanStyle. The open/closed principle is the point. A project running DefaultStyle today can subclass the Requirement abstract class and add a typed field — say classOfService: 'Standard' | 'Expedite' | 'FixedDate' | 'Intangible' — without importing KanbanStyle at all. The Style system is fork-friendly. The package does not require an all-or-nothing switch.
The three compose because they target different axes. EARS lives at the sentence axis, AgileStyle at the artefact axis, KanbanStyle at the orchestration axis. A given Requirement is a point in the product space; the same spec can simultaneously be an EARS ubiquitous statement and an AgileStyle UserStory kind and carry a Kanban class-of-service tag. Nothing in the type system forbids it. The compositionality is the reason the package ships these as separable pieces instead of baking them into a single monolithic Style.
Running-example recap
REQ-DOG-FOOD — "test every part of the DSL with the DSL itself; zero describe / zero it" — can be expressed in each register without losing meaning.
In DefaultStyle it is a
ubiquitousEARS statement: The system shall test every part of the DSL with the DSL itself — zero describe / zero it in the package test suite. The canonical form, and the one the package ships.In AgileStyle it would be a
user-story: As a framework maintainer, I want every test in the package to use @FeatureTest / @Verifies decorators rather than describe/it, so that the package validates its own DSL (the dog-food contract). Role = maintainer, capability = decorator-only tests, benefit = self-validation. Rule 5 passes — the benefit slot is non-empty.In KanbanStyle it would be a Standard-class
service-requestcarrying a flow-driven source (a lead-time distribution over test-file migration throughput): Deliverable: every test in the package migrated from describe/it to @FeatureTest/@Verifies. Customer: framework maintainers. Done when:rg "\\b(describe|it)\\(" test/returns empty and CI enforces the check. Class of service: Standard. Rule 4 passes — the source type is a flow-driven metric.
Same fact, three registers. The Style choice is not about what the Requirement says; it is about which discipline the team wants the package to enforce on how it says it.
Related reading
- Chapter 08 — The Style System — the registry, the vocabulary / validator / template / reporter split, and how a project swaps Styles.
- Chapter 09 — IndustrialStyle deep dive — the heavy register for safety-critical systems; 13-state lifecycle, SIL hierarchy, regulated evidence.
- Chapter 10 — LeanStyle deep dive — PDCA / A3 / Gemba / VSM / Kaizen; evidence via direct observation and experiment cycles.
- Non-identity between Markdown and HTML — the surface-vs-essence parallel: an EARS statement renders to an English sentence but is a discriminated union.
- A style is a tiny compiler — the broader claim about what a Style actually compiles and why the vocabulary/validator/reporter triad is the right shape.
Previous: Chapter 10 — LeanStyle Deep Dive Next: Chapter 12 — Traceability Graphs, the Big Picture