From 5a5e06f471c88c1b9157dc77cd3112c58f165b40 Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Fri, 12 Jun 2026 22:23:42 +0100 Subject: [PATCH] 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' --- AGENTS.md | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3512b76..cbc70c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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"_