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.
**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.
## Anti-Pattern: Horizontal Slices
@@ -103,6 +105,7 @@ After all tests pass, look for [refactor candidates](refactoring.md):
[ ] Test describes behavior, not implementation
[ ] Test uses public interface only
[ ] Test would survive internal refactor
[ ] Expected values are independent literals, not recomputed from the code
[ ] Code is minimal for this test
[ ] No speculative features added
```
+16
View File
@@ -59,3 +59,19 @@ test("createUser makes user retrievable", async () => {
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
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
## #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
Type: Research | Prototype | Grilling
@@ -32,7 +34,11 @@ Type: Research | Prototype | Grilling
<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.
@@ -62,10 +68,10 @@ User invokes with a loose idea.
### 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.
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`.
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.
@@ -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.
> **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.
>
> **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:
> ```
> Invoke /decision-mapping with the map at <path>, ticket #4.
> Invoke /decision-mapping with the map at <path>, ticket #5.
> Invoke /decision-mapping with the map at <path>, ticket #6.
> Invoke /decision-mapping with the map at <path>, ticket auth-strategy.
> Invoke /decision-mapping with the map at <path>, ticket cache-layer.
> 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.