mirror of
https://github.com/mattpocock/skills.git
synced 2026-07-29 19:12:34 +07:00
Compare commits
2
Commits
448d0adee7
...
43ea0884b0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43ea0884b0 | ||
|
|
a116824938 |
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"mattpocock-skills": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add the **tautological test** anti-pattern to the `tdd` skill. Tests whose assertion is recomputed the way the code computes it pass by construction and give zero confidence — distinct from the implementation-coupling anti-pattern already covered. Added as a peer at the same three sites: a Philosophy principle (expected values must come from an independent source of truth), a per-cycle checklist gate, and a BAD/GOOD example pair in `tests.md`.
|
||||||
@@ -13,6 +13,8 @@ description: Test-driven development. Use when the user wants to build features
|
|||||||
|
|
||||||
**Bad tests** are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying a database directly instead of using the interface). The warning sign: your test breaks when you refactor, but behavior hasn't changed. If you rename an internal function and tests fail, those tests were testing implementation, not behavior.
|
**Bad tests** are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying a database directly instead of using the interface). The warning sign: your test breaks when you refactor, but behavior hasn't changed. If you rename an internal function and tests fail, those tests were testing implementation, not behavior.
|
||||||
|
|
||||||
|
**Tautological tests** restate the implementation inside the assertion, so they pass by construction and give zero confidence. When the expected value is computed the way the code computes it — `expect(add(a, b)).toBe(a + b)`, snapshotting a figure you derived by hand the same way the code does, asserting a constant equals itself — the test can never disagree with the code: break the code wrong and the assertion breaks wrong with it. The expected value must come from an independent source of truth — a known-good literal, a worked example, the spec.
|
||||||
|
|
||||||
See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for mocking guidelines.
|
See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for mocking guidelines.
|
||||||
|
|
||||||
## Anti-Pattern: Horizontal Slices
|
## Anti-Pattern: Horizontal Slices
|
||||||
@@ -103,6 +105,7 @@ After all tests pass, look for [refactor candidates](refactoring.md):
|
|||||||
[ ] Test describes behavior, not implementation
|
[ ] Test describes behavior, not implementation
|
||||||
[ ] Test uses public interface only
|
[ ] Test uses public interface only
|
||||||
[ ] Test would survive internal refactor
|
[ ] Test would survive internal refactor
|
||||||
|
[ ] Expected values are independent literals, not recomputed from the code
|
||||||
[ ] Code is minimal for this test
|
[ ] Code is minimal for this test
|
||||||
[ ] No speculative features added
|
[ ] No speculative features added
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -59,3 +59,19 @@ test("createUser makes user retrievable", async () => {
|
|||||||
expect(retrieved.name).toBe("Alice");
|
expect(retrieved.name).toBe("Alice");
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Tautological tests**: Expected value restates the implementation, so the test passes by construction.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Expected value is recomputed the way the code computes it
|
||||||
|
test("calculateTotal sums line items", () => {
|
||||||
|
const items = [{ price: 10 }, { price: 5 }];
|
||||||
|
const expected = items.reduce((sum, i) => sum + i.price, 0);
|
||||||
|
expect(calculateTotal(items)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
// GOOD: Expected value is an independent, known literal
|
||||||
|
test("calculateTotal sums line items", () => {
|
||||||
|
expect(calculateTotal([{ price: 10 }, { price: 5 }])).toBe(15);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ Assets created during tickets should be linked to from the map, not duplicated w
|
|||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
|
|
||||||
Numbered entries ("tickets"), each its own section keyed by its number:
|
Entries ("tickets"), each its own section keyed by a short dash-case slug that
|
||||||
|
reads as a mini-title (e.g. `relational-db`, `auth-strategy`, `cache-layer`) —
|
||||||
|
terse enough to stay token-efficient, and unique within the map.
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
## #1: Relational Or Non-Relational Database?
|
## relational-db: Relational Or Non-Relational Database?
|
||||||
|
|
||||||
Blocked by: #<ticket-number>, #<ticket-number>
|
Blocked by: <slug>, <slug>
|
||||||
Status: open | in-progress | resolved
|
Status: open | in-progress | resolved
|
||||||
Type: Research | Prototype | Grilling
|
Type: Research | Prototype | Grilling
|
||||||
|
|
||||||
@@ -32,7 +34,11 @@ Type: Research | Prototype | Grilling
|
|||||||
<answer-here>
|
<answer-here>
|
||||||
```
|
```
|
||||||
|
|
||||||
A ticket is **unblocked** when every ticket in its `Blocked by` list is `resolved`. A session **claims** its ticket by setting `Status: in-progress` and saving the map before any work, so concurrent sessions skip it.
|
The slug is the canonical id, used in every `Blocked by` edge and prose
|
||||||
|
reference; the title after the colon is optional. A ticket
|
||||||
|
is **unblocked** when every ticket in its `Blocked by` list is `resolved`. A
|
||||||
|
session **claims** its ticket by setting `Status: in-progress` and saving the map
|
||||||
|
before any work, so concurrent sessions skip it.
|
||||||
|
|
||||||
Each ticket must be sized to one 100K token agent session.
|
Each ticket must be sized to one 100K token agent session.
|
||||||
|
|
||||||
@@ -62,10 +68,10 @@ User invokes with a loose idea.
|
|||||||
|
|
||||||
### Work through the map
|
### Work through the map
|
||||||
|
|
||||||
User invokes with a path to an existing map. A ticket number is **optional** — without one, you pick the next decision, not the user.
|
User invokes with a path to an existing map. A ticket slug is **optional** — without one, you pick the next decision, not the user.
|
||||||
|
|
||||||
1. Load the **whole map** as context.
|
1. Load the **whole map** as context.
|
||||||
2. Choose the ticket. If the user named one, use it. Otherwise pick the lowest-numbered `open` ticket that is [unblocked](#structure). [Claim it](#structure): set `Status: in-progress` and save before any work.
|
2. Choose the ticket. If the user named one, use it. Otherwise pick the first `open` ticket in document order that is [unblocked](#structure). [Claim it](#structure): set `Status: in-progress` and save before any work.
|
||||||
3. Resolve it, invoking skills as needed. If in doubt, use `/grilling` and `/domain-modeling`.
|
3. Resolve it, invoking skills as needed. If in doubt, use `/grilling` and `/domain-modeling`.
|
||||||
4. Record the answer in the ticket's body and set `Status: resolved`.
|
4. Record the answer in the ticket's body and set `Status: resolved`.
|
||||||
5. Add newly-discovered tickets with correct `Blocked by` edges. If the decisions made invalidate other parts of the map, update or delete those nodes.
|
5. Add newly-discovered tickets with correct `Blocked by` edges. If the decisions made invalidate other parts of the map, update or delete those nodes.
|
||||||
@@ -79,7 +85,7 @@ End every session by clearing the context and opening one or more fresh sessions
|
|||||||
|
|
||||||
**Open tickets remain.** List the currently-unblocked tickets, then give two copy-paste options: a bare command for one session (you pick the next ticket), and one pinned command per unblocked ticket for running them in parallel. Paste one line per fresh window — opening one, some, or all of them.
|
**Open tickets remain.** List the currently-unblocked tickets, then give two copy-paste options: a bare command for one session (you pick the next ticket), and one pinned command per unblocked ticket for running them in parallel. Paste one line per fresh window — opening one, some, or all of them.
|
||||||
|
|
||||||
> **Next steps** — 3 tickets unblocked: #4, #5, #6.
|
> **Next steps** — 3 tickets unblocked: `auth-strategy`, `cache-layer`, `rate-limits`.
|
||||||
> Clear the context, then open fresh sessions.
|
> Clear the context, then open fresh sessions.
|
||||||
>
|
>
|
||||||
> **One session** — resolves the next unblocked ticket:
|
> **One session** — resolves the next unblocked ticket:
|
||||||
@@ -89,9 +95,9 @@ End every session by clearing the context and opening one or more fresh sessions
|
|||||||
>
|
>
|
||||||
> **Parallel** — paste one line per window, up to all 3:
|
> **Parallel** — paste one line per window, up to all 3:
|
||||||
> ```
|
> ```
|
||||||
> Invoke /decision-mapping with the map at <path>, ticket #4.
|
> Invoke /decision-mapping with the map at <path>, ticket auth-strategy.
|
||||||
> Invoke /decision-mapping with the map at <path>, ticket #5.
|
> Invoke /decision-mapping with the map at <path>, ticket cache-layer.
|
||||||
> Invoke /decision-mapping with the map at <path>, ticket #6.
|
> Invoke /decision-mapping with the map at <path>, ticket rate-limits.
|
||||||
> ```
|
> ```
|
||||||
|
|
||||||
**No open tickets remain.** The fog is pushed back far enough that the path to the finish line is clear — the map is done. (The initial grilling may also surface no fog at all, in which case there was never a map to build.) Recommend implementing directly, or using `/to-prd` to schedule a multi-session implementation.
|
**No open tickets remain.** The fog is pushed back far enough that the path to the finish line is clear — the map is done. (The initial grilling may also surface no fog at all, in which case there was never a map to build.) Recommend implementing directly, or using `/to-prd` to schedule a multi-session implementation.
|
||||||
|
|||||||
Reference in New Issue
Block a user