update agent skill with YAGNI
This commit is contained in:
parent
e267c35e55
commit
8450f90e89
48
AGENTS.md
48
AGENTS.md
@ -16,13 +16,14 @@ An implementation of the RPG Combat rules engine. There are six user stories des
|
|||||||
|
|
||||||
This project combines three practices:
|
This project combines three practices:
|
||||||
|
|
||||||
1. **Allium** (`.allium` specs) — formal behavioural specifications that capture *what* the system does
|
1. **Allium** (`.allium` specs) — formal behavioural specifications that capture _what_ the system does
|
||||||
2. **fast-check** — property-based testing that verifies those properties hold across thousands of random inputs
|
2. **fast-check** — property-based testing that verifies those properties hold across thousands of random inputs
|
||||||
3. **"I can't believe it's not Haskell"** — TypeScript with ADTs, value objects, and immutability
|
3. **"I can't believe it's not Haskell"** — TypeScript with ADTs, value objects, and immutability
|
||||||
|
|
||||||
### Step 1: Spec with Allium
|
### Step 1: Spec with Allium
|
||||||
|
|
||||||
Use the Allium skills to formalize user stories into `.allium` specs:
|
Use the Allium skills to formalize user stories into `.allium` specs:
|
||||||
|
|
||||||
- `/skill:elicit` — explore requirements with stakeholders
|
- `/skill:elicit` — explore requirements with stakeholders
|
||||||
- `/skill:distill` — extract specs from existing code
|
- `/skill:distill` — extract specs from existing code
|
||||||
- `/skill:tend` — evolve specs as understanding deepens
|
- `/skill:tend` — evolve specs as understanding deepens
|
||||||
@ -38,13 +39,10 @@ import fc from 'fast-check';
|
|||||||
import { Character } from './domain';
|
import { Character } from './domain';
|
||||||
|
|
||||||
// Invariant: "A character's health is never negative"
|
// Invariant: "A character's health is never negative"
|
||||||
fc.property(
|
fc.property(fc.integer({ min: 0, max: 1000 }), (initialHealth) => {
|
||||||
fc.integer({ min: 0, max: 1000 }),
|
const c = new Character({ name: 'hero', health: initialHealth });
|
||||||
(initialHealth) => {
|
return c.health >= 0;
|
||||||
const c = new Character({ name: 'hero', health: initialHealth });
|
});
|
||||||
return c.health >= 0;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Property: "Dealing damage reduces health, capped at 0"
|
// Property: "Dealing damage reduces health, capped at 0"
|
||||||
fc.property(
|
fc.property(
|
||||||
@ -58,13 +56,13 @@ fc.property(
|
|||||||
const t = new Character({ name: target, health: 1000 });
|
const t = new Character({ name: target, health: 1000 });
|
||||||
a.dealDamage(t, damage);
|
a.dealDamage(t, damage);
|
||||||
return t.health === Math.max(0, 1000 - damage);
|
return t.health === Math.max(0, 1000 - damage);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 3: "I can't believe it's not Haskell"
|
### Step 3: "I can't believe it's not Haskell"
|
||||||
|
|
||||||
Use TypeScript's type system to encode domain constraints:
|
Use TypeScript's type system to encode domain constraints. Add only code necessary to make the property pass, no more:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// ADTs via discriminated unions
|
// ADTs via discriminated unions
|
||||||
@ -77,9 +75,15 @@ class Health {
|
|||||||
if (n < 0) throw new Error('Health cannot be negative');
|
if (n < 0) throw new Error('Health cannot be negative');
|
||||||
return new Health(n);
|
return new Health(n);
|
||||||
}
|
}
|
||||||
get value() { return this.value; }
|
get value() {
|
||||||
add(n: number) { return Health.create(this.value + n); }
|
return this.value;
|
||||||
sub(n: number) { return Health.create(Math.max(0, this.value - n)); }
|
}
|
||||||
|
add(n: number) {
|
||||||
|
return Health.create(this.value + n);
|
||||||
|
}
|
||||||
|
sub(n: number) {
|
||||||
|
return Health.create(Math.max(0, this.value - n));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Immutable entities
|
// Immutable entities
|
||||||
@ -98,16 +102,28 @@ class Character {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Key principles:**
|
**Key principles:**
|
||||||
|
|
||||||
- **ADTs over classes** — use discriminated unions for state/variants
|
- **ADTs over classes** — use discriminated unions for state/variants
|
||||||
- **Value objects over primitives** — `Health`, `Damage`, `Level` instead of `number`
|
- **Value objects over primitives** — `Health`, `Damage`, `Level` instead of `number`
|
||||||
- **Immutability** — no `this.health = ...`, return new instances
|
- **Immutability** — no `this.health = ...`, return new instances
|
||||||
- **Invariants at boundaries** — constructors enforce invariants, not getters/setters
|
- **Invariants at boundaries** — constructors enforce invariants, not getters/setters
|
||||||
- **Pure functions** — domain logic has no side effects, testable in isolation
|
- **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:
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Example: Damage Property
|
### Example: Damage Property
|
||||||
|
|
||||||
From user story: *"When damage received exceeds current Health, Health becomes 0 and the character dies"*
|
From user story: _"When damage received exceeds current Health, Health becomes 0 and the character dies"_
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Allium invariant (in .allium spec)
|
// Allium invariant (in .allium spec)
|
||||||
@ -122,13 +138,14 @@ fc.property(
|
|||||||
const c = new Character({ name: 'goblin', health: Health.create(health) });
|
const c = new Character({ name: 'goblin', health: Health.create(health) });
|
||||||
c.takeDamage(Damage.create(damage));
|
c.takeDamage(Damage.create(damage));
|
||||||
return c.health.value === Math.max(0, health - damage);
|
return c.health.value === Math.max(0, health - damage);
|
||||||
}
|
},
|
||||||
).check(/* ... */);
|
).check(/* ... */);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Skill Invocation
|
## Skill Invocation
|
||||||
|
|
||||||
Allium skills are available in this project:
|
Allium skills are available in this project:
|
||||||
|
|
||||||
- `/skill:allium` — entry point and language reference
|
- `/skill:allium` — entry point and language reference
|
||||||
- `/skill:elicit` — explore requirements
|
- `/skill:elicit` — explore requirements
|
||||||
- `/skill:distill` — extract specs from code
|
- `/skill:distill` — extract specs from code
|
||||||
@ -137,4 +154,5 @@ Allium skills are available in this project:
|
|||||||
- `/skill:weed` — check spec-code alignment
|
- `/skill:weed` — check spec-code alignment
|
||||||
|
|
||||||
Domain workflow skill:
|
Domain workflow skill:
|
||||||
|
|
||||||
- `/skill:user-story-conversation` — Card, Conversation, Confirmation workflow with Example Mapping, Allium specs, and fast-check properties
|
- `/skill:user-story-conversation` — Card, Conversation, Confirmation workflow with Example Mapping, Allium specs, and fast-check properties
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user