12 KiB
name, description, disable-model-invocation, license, metadata
| name | description | disable-model-invocation | license | metadata | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| user-story-conversation | Guide users through Ron Jeffries' Card, Conversation, Confirmation workflow using Example Mapping, Allium specs, and fast-check properties. | true | MIT |
|
User Story Conversation
A skill for turning user stories into formal specifications and executable properties, following Ron Jeffries' Card, Conversation, Confirmation mantra enhanced with Matt Wynne's Example Mapping.
The Six Steps
Card → User story (the "what")
Conversation → Example Mapping (the "understanding")
→ Allium Spec (the "formal model")
Confirmation → fast-check Properties (the "executable verification")
→ TypeScript ADTs (the "type-safe implementation")
When to Use
- Starting a new feature or user story
- Clarifying ambiguous requirements
- Breaking down a complex user story into testable properties
- Aligning the team on domain understanding before implementation
How to Run
- Start with the Card — Ask the user for the user story (or use an existing one from
user-stories.md) - Example Map — Work through the story together, identifying Rules, Examples, Questions, and Answers
- Allium Spec — Formalize the rules and answers into a
.alliumspec - fast-check Properties — Translate invariants into property-based tests
- TypeScript ADTs — Implement with value objects and discriminated unions
- Confirmation — Verify the implementation satisfies all properties
Step 1: Card
Present the user story as a card. Use the format:
As a [role]
I want [feature]
So that [benefit]
If the user already has a user story (e.g., from user-stories.md), use that. Otherwise, help them write one.
Step 2: Conversation — Example Mapping
Work through the user story using Example Mapping. For each rule, identify:
Rules (yellow) — Domain concepts
- Nouns and concepts in the domain
- What the system manages
- Example: "Health", "Damage", "Character", "Level", "Faction"
Examples (blue) — Concrete scenarios
- Specific instances of rules
- Should be testable
- Example: "Character with 500 health takes 200 damage → 300 health"
Questions (pink) — Ambiguities
- Things we don't know or aren't sure about
- Example: "What happens when damage exceeds health?"
Answers (green) — Resolved questions
- Write directly on the pink sticky
- Example: "Health becomes 0, character dies"
Example Mapping Output Format
Create a structured output like this:
## Example Map: [User Story Title]
### Rules
1. [Rule description]
2. [Rule description]
### Examples
- [Example 1]
- [Example 2]
### Questions
- [Question 1]
- [Question 2]
### Answers
- [Answer to Question 1]
- [Answer to Question 2]
Step 3: Allium Spec
Formalize the rules, examples, and answers into an Allium spec. Use the skills in this repository:
/skill:elicit— if you need to explore requirements further/skill:tend— if you need to evolve an existing spec/skill:allium— for language reference
Spec Structure
Create a .allium file with:
-- allium: 3
-- allium: [module-name]
------------------------------------------------------------
-- Entities and Variants
------------------------------------------------------------
entity Character {
name: String
health: Health
status: alive | dead
level: Level
factions: Set<Faction>
}
------------------------------------------------------------
-- Rules
------------------------------------------------------------
rule DamageReducesHealth {
when: CharacterTakesDamage(character, damage)
ensures: character.health = character.health - damage
}
------------------------------------------------------------
-- Invariants
------------------------------------------------------------
invariant HealthNonNegative {
for c in Characters:
c.health >= 0
}
invariant DeathAtZeroHealth {
for c in Characters:
c.health = 0 implies c.status = dead
}
Key Principles
- Entities represent domain objects with identity
- Rules describe state transitions and behaviour
- Invariants capture properties that must always hold
- Enums and value types for constrained data
- Rules should be testable — each one maps to a property
Step 4: fast-check Properties
Translate Allium invariants and rules into fast-check properties.
Property Patterns
Invariant Properties
import fc from 'fast-check';
// "Health is never negative"
fc.property(fc.integer({ min: 0, max: 10000 }), (health) => {
const c = new Character({ health: Health.create(health) });
return c.health.value >= 0;
});
Rule Properties (State Transitions)
// "Damage reduces health, capped at 0"
fc.property(
fc.integer({ min: 0, max: 10000 }),
fc.integer({ min: 0, max: 10000 }),
(health, damage) => {
const c = new Character({ health: Health.create(health) });
c.takeDamage(Damage.create(damage));
return c.health.value === Math.max(0, health - damage);
},
);
Edge Case Properties
// "Zero damage changes nothing"
fc.property(fc.integer({ min: 0, max: 10000 }), (health) => {
const c = new Character({ health: Health.create(health) });
c.takeDamage(Damage.create(0));
return c.health.value === health;
});
Property Naming
Name properties after the rule or invariant they verify:
describe('DamageReducesHealth', () => {
it('property: health is reduced by damage amount', () => { ... });
it('property: health never goes below zero', () => { ... });
it('property: character dies when health reaches zero', () => { ... });
});
Step 5: TypeScript ADTs
Implement with "I can't believe it's not Haskell" principles:
Discriminated Unions (ADTs)
type Status = { kind: 'alive' } | { kind: 'dead' };
type FactionMember = { kind: 'member'; faction: Faction };
type FactionNeutral = { kind: 'neutral' };
type FactionMembership = FactionMember | FactionNeutral;
Value Objects
class Health {
private constructor(private readonly value: number) {}
static create(n: number): Health {
if (n < 0) throw new Error('Health cannot be negative');
return new Health(n);
}
static maxForLevel(level: number): number {
return level >= 6 ? 1500 : 1000;
}
get value(): number {
return this.value;
}
add(amount: number): Health {
return Health.create(Math.min(this.value + amount, Health.maxForLevel(this.level.value)));
}
sub(amount: number): Health {
return Health.create(Math.max(0, this.value - amount));
}
}
Immutable Entities
class Character {
constructor(
readonly name: string,
readonly health: Health,
readonly status: Status,
readonly level: Level,
readonly factions: ReadonlySet<Faction>,
) {}
// Pure functions — no mutation
takeDamage(damage: Damage): Character {
const newHealth = this.health.sub(damage.value);
const newStatus = newHealth.value === 0 ? { kind: 'dead' as const } : this.status;
return new Character(this.name, newHealth, newStatus, this.level, this.factions);
}
}
Key Principles
- ADTs over classes — discriminated unions for state/variants
- Value objects over primitives —
Health,Damage,Levelinstead ofnumber - Immutability — return new instances, never mutate
- Invariants at construction — invalid states are unrepresentable
- Pure functions — domain logic has no side effects
Step 6: Confirmation
Verify that the implementation satisfies all properties:
npm test
All properties should pass. If any fail, the property reveals a gap in understanding — revisit the Example Map or Allium spec.
Output Artifacts
After completing the workflow, produce:
- Example Map — Markdown file with structured domain knowledge
- Allium Spec —
.alliumfile with formal behavioural specification - fast-check Properties — Test files with property-based tests
- TypeScript Implementation — ADTs, value objects, and pure functions
Example: Full Workflow
Here's a complete example walking through "Characters can Deal Damage":
Card
Characters can Deal Damage to Characters.
Example Map
Rules:
- Damage is subtracted from Health
- When damage exceeds health, health becomes 0 and character dies
- A Character cannot Deal Damage to itself
Examples:
- Character with 1000 health takes 200 damage → 800 health
- Character with 100 health takes 200 damage → 0 health, dead
- Character tries to deal damage to self → no effect
Questions:
- Can a dead character take damage?
- Does damage stack across multiple attacks?
Answers:
- Dead characters cannot take damage (they're already dead)
- Yes, damage stacks (each attack reduces health further)
Allium Spec
entity Character {
name: String
health: Health
status: alive | dead
}
rule DamageReducesHealth {
when: CharacterTakesDamage(character, damage)
requires: character.status = alive
ensures: character.health = character.health - damage
ensures:
if character.health <= 0:
character.status = dead
}
invariant HealthNonNegative {
for c in Characters:
c.health >= 0
}
invariant SelfDamageForbidden {
for a in Characters, t in Characters:
a.dealDamage(t, _) implies a.name != t.name
}
fast-check Properties
describe('DamageReducesHealth', () => {
it('property: health is reduced by damage', () => { ... });
it('property: health never goes below zero', () => { ... });
it('property: character dies when health reaches zero', () => { ... });
it('property: dead characters cannot take damage', () => { ... });
it('property: self-damage is forbidden', () => { ... });
});
TypeScript Implementation
type Status = { kind: 'alive' } | { kind: 'dead' };
class Health {
static create(n: number): Health { ... }
sub(amount: number): Health { ... }
}
class Character {
constructor(
readonly name: string,
readonly health: Health,
readonly status: Status,
) {}
dealDamage(target: Character, damage: Damage): void {
if (this.name === target.name) return; // self-damage forbidden
if (target.status.kind === 'dead') return; // dead can't take damage
const newHealth = target.health.sub(damage.value);
const newStatus = newHealth.value === 0 ? { kind: 'dead' as const } : target.status;
// ... update target
}
}
Integration with Allium Skills
This skill works alongside the Allium skills:
- Before starting — Use
/skill:alliumto understand the language - During spec writing — Use
/skill:tendto evolve specs,/skill:propagateto generate test obligations - After implementation — Use
/skill:weedto check spec-code alignment - When clarifying requirements — Use
/skill:elicitto explore with stakeholders
Tips
- Start small — Map one user story at a time
- Examples first — Concrete examples help surface ambiguities
- Properties before implementation — Write fast-check properties before code
- Let properties guide design — If a property is hard to write, the design needs work
- Iterate — Example Mapping is a conversation, not a document. Revise as understanding deepens
- Use the Allium CLI — Run
allium checkto validate specs - Run fast-check with many iterations — Use
fc.property(...).check({ numRuns: 1000 })for thorough testing