Compare commits

..
2 Commits
Author SHA1 Message Date
Matt PocockandClaude Opus 4.8 43ea0884b0 tdd: add tautological-test anti-pattern
Tests whose assertion is recomputed the way the code computes it pass by
construction and give zero confidence. Add it as a peer of the existing
implementation-coupling anti-pattern: a Philosophy principle, a per-cycle
checklist gate, and a BAD/GOOD example pair in tests.md. Includes a patch
changeset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 21:22:50 +01:00
Matt PocockandClaude Opus 4.8 a116824938 decision-mapping: key tickets by dash-case slugs, not numeric ids
Replace numeric ticket ids (#1, #2…) with short, token-efficient
dash-case slugs (e.g. relational-db) that read as mini-titles. Update
the Structure example, the next-ticket selection rule (lowest-numbered →
first in document order), the optional-arg wording, and the Handoff
copy-paste examples accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:50:46 +01:00
4 changed files with 40 additions and 10 deletions
+5
View File
@@ -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`.
+3
View File
@@ -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
``` ```
+16
View File
@@ -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);
});
```
+16 -10
View File
@@ -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.