add problem breakdown with yaks skill
This commit is contained in:
parent
0c09b08009
commit
f29e3c456f
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,3 +9,4 @@ coverage/
|
|||||||
**/*.received.*
|
**/*.received.*
|
||||||
**/DS_Store/*
|
**/DS_Store/*
|
||||||
**/.DS_Store.yaks
|
**/.DS_Store.yaks
|
||||||
|
.yaks
|
||||||
|
|||||||
253
.pi/skills/problem-breakdown/SKILL.md
Normal file
253
.pi/skills/problem-breakdown/SKILL.md
Normal file
@ -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
|
||||||
4256
transcripts/fixed-character-implementation-maybe.html
Normal file
4256
transcripts/fixed-character-implementation-maybe.html
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user