docs(AGENTS): tighten YAGNI discipline, resolve value-object contradiction

- Clarify 'value objects over primitives' is reactive, not anticipatory
- Mark the Step 3 example as aspirational domain shape, not per-story template
- Add 'What to skip per story' trap table with concrete boundaries
- Add litmus test: 'point to a failing property or don't write it'
This commit is contained in:
Willem van den Ende 2026-06-12 22:23:42 +01:00
parent e05572bcec
commit 5a5e06f471

View File

@ -62,7 +62,7 @@ fc.property(
### Step 3: "I can't believe it's not Haskell"
Use TypeScript's type system to encode domain constraints. Add only code necessary to make the property pass, no more:
Use TypeScript's type system to encode domain constraints. **The example below shows the complete domain shape — not what to implement per story.** Implement only what a property forces you to write.
```typescript
// ADTs via discriminated unions
@ -107,20 +107,37 @@ class Character {
**Key principles:**
- **ADTs over classes** — use discriminated unions for state/variants
- **Value objects over primitives**`Health`, `Damage`, `Level` instead of `number`
- **Value objects over primitives**when a property reveals that a bare `number` or `string` is insufficient, introduce a value object. Don't anticipate — react.
- **Immutability** — no `this.health = ...`, return new instances
- **Invariants at boundaries** — constructors enforce invariants, not getters/setters
- **Pure functions** — domain logic has no side effects, testable in isolation
- **YAGNI** Write the minimum code necessary to make the current property pass.
Before writing a method, ask:
**YAGNI discipline:** Write only the minimum code necessary to make the current property pass.
1. Does a story property require this? If no → don't write it.
2. Does this method touch a concept from a different story? If yes → it's scope creep.
3. Am I implementing something because it feels useful, not because a property forces it? If yes → stop.
Before writing a method or class, ask:
1. **Does a story property require this?** If no → don't write it.
2. **Does this touch a concept from a different story?** If yes → it's scope creep.
3. **Am I implementing this because it feels useful, not because a property forces it?** If yes → stop.
> **The litmus test:** If you can't point to a failing property that demands this code, don't write it. Future stories will reveal what abstractions are actually needed — and they'll look different than you expect.
### What to skip per story (common traps)
| Story | Don't implement | Belongs to |
|---|---|---|
| 1 (Creation) | `isAllyOf`, `isAlive`, `isDead` | Stories 3+ |
| 1 (Creation) | `Health.add()`, `Health.isMax()` | Stories 3/4 |
| 1 (Creation) | `Level.next()`, `Level.diff()` | Story 5 |
| 2 (Damage) | `Damage` value object | Only if a property demands it |
| 2 (Damage) | Level modifier (±50%) | Story 3 |
| 2 (Damage) | Faction/ally checks | Story 3 |
| 3 (Levels) | MagicalObjects | Story 4 |
| 3 (Levels) | Level-up tracking | Story 5 |
| 4 (Objects) | `joinFaction` / `leaveFaction` | Story 3 |
### Example: Damage Property
From user story: _"When damage received exceeds current Health, Health becomes 0 and the character dies"_