--- name: user-story-conversation description: Guide users through Ron Jeffries' Card, Conversation, Confirmation workflow using Example Mapping, Allium specs, and fast-check properties. disable-model-invocation: true license: MIT metadata: methodology: example-mapping references: - https://www.examplerelated.com/ - https://www.ronjeffries.com/ --- # 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 1. **Start with the Card** — Ask the user for the user story (or use an existing one from `user-stories.md`) 2. **Example Map** — Work through the story together, identifying Rules, Examples, Questions, and Answers 3. **Allium Spec** — Formalize the rules and answers into a `.allium` spec 4. **fast-check Properties** — Translate invariants into property-based tests 5. **TypeScript ADTs** — Implement with value objects and discriminated unions 6. **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: ```markdown ## 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 -- allium: 3 -- allium: [module-name] ------------------------------------------------------------ -- Entities and Variants ------------------------------------------------------------ entity Character { name: String health: Health status: alive | dead level: Level factions: Set } ------------------------------------------------------------ -- 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 ```typescript 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) ```typescript // "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 ```typescript // "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: ```typescript 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) ```typescript type Status = { kind: 'alive' } | { kind: 'dead' }; type FactionMember = { kind: 'member'; faction: Faction }; type FactionNeutral = { kind: 'neutral' }; type FactionMembership = FactionMember | FactionNeutral; ``` ### Value Objects ```typescript 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 ```typescript class Character { constructor( readonly name: string, readonly health: Health, readonly status: Status, readonly level: Level, readonly factions: ReadonlySet, ) {} // 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`, `Level` instead of `number` - **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: ```bash 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: 1. **Example Map** — Markdown file with structured domain knowledge 2. **Allium Spec** — `.allium` file with formal behavioural specification 3. **fast-check Properties** — Test files with property-based tests 4. **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:** 1. Damage is subtracted from Health 2. When damage exceeds health, health becomes 0 and character dies 3. 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 ```allium 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 ```typescript 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 ```typescript 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:allium` to understand the language - **During spec writing** — Use `/skill:tend` to evolve specs, `/skill:propagate` to generate test obligations - **After implementation** — Use `/skill:weed` to check spec-code alignment - **When clarifying requirements** — Use `/skill:elicit` to 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 check` to validate specs - **Run fast-check with many iterations** — Use `fc.property(...).check({ numRuns: 1000 })` for thorough testing