From f29e3c456feb098502c5d7a693ce3836795a5ded Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Sun, 14 Jun 2026 10:31:03 +0100 Subject: [PATCH] add problem breakdown with yaks skill --- .gitignore | 1 + .pi/skills/problem-breakdown/SKILL.md | 253 + .../fixed-character-implementation-maybe.html | 4256 +++++++++++++++++ 3 files changed, 4510 insertions(+) create mode 100644 .pi/skills/problem-breakdown/SKILL.md create mode 100644 transcripts/fixed-character-implementation-maybe.html diff --git a/.gitignore b/.gitignore index 0616f8f..cb69465 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ coverage/ **/*.received.* **/DS_Store/* **/.DS_Store.yaks +.yaks diff --git a/.pi/skills/problem-breakdown/SKILL.md b/.pi/skills/problem-breakdown/SKILL.md new file mode 100644 index 0000000..9773767 --- /dev/null +++ b/.pi/skills/problem-breakdown/SKILL.md @@ -0,0 +1,253 @@ +--- +name: problem-breakdown +description: 'Break down a problem into small, independently executable steps using yx. Use after user-story-conversation to create a test execution list, or during horizontal refactoring to plan file moves and transformations. Each step becomes a yak with yx add and includes execution context so another agent can execute it independently.' +disable-model-invocation: true +license: MIT +metadata: + tool: yx +--- + +# Problem Breakdown + +Break a problem into small, independently executable steps using the `yx` CLI. Each step becomes a yak with execution context, enabling another agent (or future you) to execute it independently. + +## When to Use + +1. **After user-story-conversation** — Convert the output (Allium spec + fast-check properties) into a concrete test execution list: which files to create/change, what tests to write, where to place them. +2. **Horizontal refactoring** — Plan cross-cutting changes like moving value objects into a `value-objects/` directory, extracting interfaces, or restructuring modules. +3. **Feature scaffolding** — Break a feature into file creation, implementation, wiring, and test steps. + +## The Method + +### Step 1: Identify the work items + +From the problem description, extract discrete, independently executable units of work. Each work item should satisfy: + +- **Self-contained** — can be executed without waiting for another yak to finish +- **Small** — one file, one method, one move, one test +- **Verifiable** — has a clear pass/fail condition (compiles, tests green, linter clean) +- **Ordered** — parents block children (use `yx add --under`) + +### Step 2: Create yaks with `yx add` + +For each work item: + +```bash +yx add "create src/domain/health.value-objects.ts" +yx add "implement Health.create() with invariant n >= 0" --under "create src/domain/health.value-objects.ts" +yx add "write fast-check property: Health.create rejects negative numbers" --under "implement Health.create() with invariant n >= 0" +``` + +Use `--under` to express dependency hierarchy. Children block their parent. + +### Step 3: Add execution context with `yx context` + +For each yak, add enough detail for another agent to execute it independently: + +```bash +echo "Create src/domain/health.value-objects.ts with a Health value object class. +- Private constructor taking number +- Static create(n: number): Health — throws if n < 0 +- get value(): number +- sub(amount: number): Health — returns Health.create(max(0, this.value - amount)) +- add(amount: number): Health — returns Health.create(this.value + amount) +- No dependency on other domain entities yet" | yx context "implement Health.create() with invariant n >= 0" +``` + +### Step 4: Execute + +The executor agent reads each yak's context, executes the step, and marks it done: + +```bash +yx start "implement Health.create() with invariant n >= 0" +# ... execute ... +yx done "implement Health.create() with invariant n >= 0" +``` + +## Output Patterns + +### Pattern A: Test Execution List (after user-story-conversation) + +After a user-story-conversation produces an Allium spec and fast-check properties, break them into file-level execution steps. Before writing any test yak, run the **Test Strategy Decision** below. + +#### Test Strategy Decision + +For each rule/invariant from the spec, decide whether to write a **property-based test** or an **example-based test**. Discuss with the user: + +| Signal | Choose | Rationale | +| --------------------------------------------------- | --------------- | -------------------------------------------- | +| `fc.property` would be trivially short (< 5 lines) | Example-based | Property overhead not worth it | +| Invariant is a simple arithmetic relationship | Example-based | One or two examples cover all cases | +| State transition has a small, finite input space | Example-based | Exhaustive examples are feasible | +| Invariant involves collections, sequences, or math | Property-based | Need random inputs to find edge cases | +| Rule has complex guards (requires + ensures chains) | Property-based | Random inputs surface hidden preconditions | +| User says "just show it works" | Example-based | Confidence test, not a robustness guarantee | +| User says "prove it always holds" | Property-based | That's what properties are for | + +**Default:** start with example-based tests. Escalate to property-based only when the user or the spec demands broader coverage. This keeps the yak list smaller and faster to execute. + +After deciding, create the test yaks with the chosen approach in context. + +``` +Feature: Characters Deal Damage +├── create src/domain/status.value-objects.ts ← ADT for alive/dead +│ └── write Status discriminated union ← {kind: 'alive'} | {kind: 'dead'} +├── create src/domain/health.value-objects.ts ← Health value object +│ ├── implement Health.create() with invariant ← throws if n < 0 +│ └── implement Health.sub() ← capped at 0 +├── create src/domain/character.entity.ts ← Character entity +│ ├── implement Character constructor ← name, health, status +│ └── implement Character.dealDamage() ← with self-damage guard +├── write tests/health.spec.ts ← example + PBT tests +│ ├── example: 500 - 200 = 300 ← Health.sub arithmetic (simple, example-based) +│ ├── property: Health.sub never goes below zero ← fc.property (invariant, property-based) +│ └── example: 100 - 200 = 0 ← Health.sub boundary (simple, example-based) +├── write tests/character.spec.ts ← example + PBT tests +│ ├── example: 1000 health, 200 damage → 800 ← dealDamage happy path (example-based) +│ ├── property: dealDamage reduces target health ← fc.property (invariant, property-based) +│ └── example: self-damage is forbidden ← dealDamage guard (example-based) +└── run npm test ← verify all pass +``` + +### Pattern B: Horizontal Refactoring + +For cross-cutting structural changes: + +``` +Refactor: Move value objects to value-objects/ +├── create src/domain/value-objects/ ← new directory +│ └── write barrel index.ts ← re-export all value objects +├── move src/domain/health.ts → src/domain/value-objects/health.ts +│ └── update all imports to point to value-objects/health +├── move src/domain/damage.ts → src/domain/value-objects/damage.ts +│ └── update all imports to point to value-objects/damage +├── move src/domain/level.ts → src/domain/value-objects/level.ts +│ └── update all imports to point to value-objects/level +├── update src/domain/index.ts ← update barrel exports +└── run npm run checks ← format, lint, typecheck, test +``` + +## Context Template + +Each yak's context should contain: + +``` +### Location +File: src/path/to/file.ts +Line: ~line numbers (if modifying existing) + +### What to create/modify +Clear description of the change. + +### Implementation details +- Key signatures +- Invariants to enforce +- Dependencies (what already exists) +- What NOT to implement (scope guard) + +### Verification +- npm run typecheck passes +- npm test passes (specific test file) +- npm run lint:fix clean + +### References +- Allium spec: .allium/path/allium-file.allium +- Related yak: "name of parent yak" +``` + +## Rules + +1. **One yak per file operation** — create, move, or modify a single file +2. **Context is king** — if another agent can't execute it from the context alone, add more detail +3. **Scope guards** — explicitly state what NOT to implement in each yak's context (prevents scope creep) +4. **Dependencies via `--under`** — use the hierarchy, not just flat list +5. **Verification yak** — always add a final yak to run the full check suite +6. **No implementation details in yak names** — yak names should be action-oriented summaries; details go in context +7. **Keep yaks small** — if a yak's context is more than 30 lines, split it + +## Integration with Other Skills + +| Skill | When to run problem-breakdown after | +| ---------------------- | ------------------------------------------------ | +| user-story-conversation| After Allium spec + properties are produced | +| distill | After spec extraction, before test generation | +| tend | After spec changes, to plan implementation updates | +| propagate | When propagate produces obligations, to break them into file-level steps | +| weed | After divergence is found, to plan alignment fixes | + +## Example: Full Breakdown + +After a user-story-conversation on "Characters can Deal Damage": + +```bash +# Phase 1: Create value objects +yx add "create src/domain/status.value-objects.ts" +echo "Create src/domain/status.value-objects.ts +- Discriminated union: type Status = { kind: 'alive' } | { kind: 'dead' } +- No methods, just the type +- Export as default" | yx context "create src/domain/status.value-objects.ts" + +# Phase 2: Health value object +yx add "create src/domain/health.value-objects.ts" --under "create src/domain/status.value-objects.ts" +echo "Create src/domain/health.value-objects.ts +- class Health with private constructor +- static create(n: number): Health — throw if n < 0 +- get value(): number +- sub(amount: number): Health — Health.create(max(0, this.value - amount)) +- add(amount: number): Health — Health.create(this.value + amount) +- NO: maxForLevel, NO: isMax, NO: isZero — those belong to later stories +- NO: dependency on Character or Level" | yx context "create src/domain/health.value-objects.ts" + +# Phase 3: Character entity +yx add "create src/domain/character.entity.ts" --under "create src/domain/health.value-objects.ts" +echo "Create src/domain/character.entity.ts +- class Character with readonly name, health, status +- constructor(name: string, health: Health, status: Status) +- dealDamage(target: Character, damage: number): void — pure logic, no mutation + - self-damage guard (this.name === target.name → return) + - health reduced by damage amount (calls target.health.sub) +- NO: factions, NO: level, NO: magicalObjects — those belong to later stories +- NO: isAllyOf, NO: isDead — those belong to later stories" | yx context "create src/domain/character.entity.ts" + +# Phase 4: Test Strategy Decision + +Before writing test yaks, decide property vs example for each test item: + +| Spec item | Decision | Why | +| ---------------------------- | --------------- | -------------------------------------- | +| Health.create rejects neg. | Example-based | One negative input is sufficient | +| Health.sub never below zero | Property-based | Invariant over arbitrary input range | +| Health.sub correct arithmetic| Example-based | Simple arithmetic, examples cover all | +| dealDamage reduces health | Property-based | Invariant: result = max(0, h - d) | +| Self-damage forbidden | Example-based | One case (same name) proves the rule | + +# Phase 4: Tests +yx add "write tests/health.spec.ts" --under "create src/domain/health.value-objects.ts" +echo "Write tests/health.spec.ts +- Import Health from src/domain/health.value-objects.ts +- Example: Health.create rejects negative (it('rejects -1', () => { expect(() => Health.create(-1)).toThrow() })) +- Property: Health.sub never below zero (fc.property(fc.integer({min:0,max:10000}), fc.integer({min:0,max:10000}), (h, d) => { const c = new Character({ health: Health.create(h) }); c.takeDamage(d); return c.health >= 0; })) +- Example: Health.sub 500 - 200 = 300 +- Example: Health.sub 100 - 200 = 0 +- Use vitest describe/it blocks with fc.assert()" | yx context "write tests/health.spec.ts" + +yx add "write tests/character.spec.ts" --under "create src/domain/character.entity.ts" +echo "Write tests/character.spec.ts +- Import Character, Health, Status from domain +- Example: dealDamage 1000 health, 200 damage → 800 (it('reduces health', () => { ... })) +- Property: dealDamage reduces target health (fc.property(fc.integer({min:0,max:10000}), fc.integer({min:0,max:10000}), (h, d) => { ... return target.health === Math.max(0, h - d) })) +- Example: self-damage forbidden (it('does nothing when target is self', () => { ... })) +- Use vitest describe/it blocks with fc.assert()" | yx context "write tests/character.spec.ts" + +# Phase 5: Verify +yx add "run npm test and npm run typecheck" --under "write tests/health.spec.ts" +yx add "run npm run checks" --under "run npm test and npm run typecheck" +``` + +## Tips + +- **Start from the spec** — the Allium spec's entities and rules map directly to yak groups +- **Group by file** — create yaks for file creation first, then implementation, then tests +- **Add scope guards** — explicitly state what NOT to implement to prevent scope creep +- **Use yx tags** — tag yaks with `test`, `domain`, `refactor` for filtering: `yx tag "write tests/health.spec.ts" test` +- **Review before executing** — run `yx list` to verify the hierarchy makes sense before starting work diff --git a/transcripts/fixed-character-implementation-maybe.html b/transcripts/fixed-character-implementation-maybe.html new file mode 100644 index 0000000..32e32e1 --- /dev/null +++ b/transcripts/fixed-character-implementation-maybe.html @@ -0,0 +1,4256 @@ + + + + + + Session Export + + + + + +
+ + +
+
+
+
+
+ +
+
+ + + + + + + + + + + + +