---
name: spec-first-tdd
description: Use for any sf behaviour change. Purpose and consumer first, failing test before code, then lint/typecheck/build, then evidence. Operationalises the constitution in `docs/SPEC_FIRST_TDD.md`. Hand off to wave implementation only when the slice is too broad for one focused change.
---

# Spec-First TDD

This skill operationalises the constitution in [`docs/SPEC_FIRST_TDD.md`](../../../../../../docs/SPEC_FIRST_TDD.md). Read it first if you haven't. The doc owns the philosophy; this skill owns the procedure.

## Iron Law

```
THE TEST IS THE SPEC.  THE JSDOC IS THE PURPOSE.  CODE EXISTS TO FULFILL PURPOSE.

NO BEHAVIOUR CHANGE WITHOUT A FAILING TEST FIRST.
NO COMPLETION WITHOUT A REAL CONSUMER.
NO JUDGMENT CALL WITHOUT A CONFIDENCE AND FALSIFIER.
```

## When to Run

- A slice has been planned, the user has approved it (`go`, or autonomous mode portfolio approval), and the contract is decision-complete.
- A bug fix needs a regression test.
- A reviewer found a real behaviour gap (invoke from `receiving-code-review`).

## Skill Chain

```
brainstorming → clarify-spec (if underspecified) → spec-first-tdd → [wave-implementation if needed] → finish-and-verify
```

Default expectation:

- Write the contract test first.
- Make the smallest passing implementation locally.
- Hand off to multi-file implementation only when the slice is genuinely wave-shaped.

Side-chain (any bug/failure during TDD): invoke `systematic-debugging`. When the fix is verified, return here and continue.

## Workflow

### 1. PDD packet (all 8 fields)

Before any test, fill the PDD packet — same 8 fields as [`purpose-driven-development`](../purpose-driven-development/SKILL.md). Save to `.sf/active/{unit-id}/pdd.md`. The reviewer (`requesting-code-review`) reads from this same packet, so what you write here is what they review.

- **Purpose:** why this behaviour exists.
- **Consumer:** which production code path depends on it (`rg -n "fnName" src/ packages/`). If you can't name a real caller, stop.
- **Contract:** what observable behaviour proves success — what the consumer receives, not the internal mechanics.
- **Failure boundary:** what *correct failure* looks like if the purpose can't be fulfilled — degrade, surface, do not swallow.
- **Evidence:** the test or metric that proves the contract. Must be machine-executable (named test file, queryable metric, runnable command) or `[MANUAL: reviewer + scenario]`. Prose-only is rejected.
- **Non-goals:** what this slice explicitly does *not* cover.
- **Invariants:** what must remain true. If the change touches async / queues / timers / state machines: split safety ("X never happens") + liveness ("Y eventually happens"). Pure synchronous code may use safety-only.
- **Assumptions:** what conditions about the world MUST be true for this spec to be valid — locking protocols, API stability, caller invariants, deployment context, data shape.

Also useful to flag for downstream review (not part of the PDD packet, but used by reviewers to calibrate scrutiny):

- **Value at risk:** what breaks (and who notices) if the contract returns the wrong answer.

Label evidence:

- `Observed:` current behaviour, failing output, caller fact.
- `Inferred:` intended contract supported by those facts.
- `Proposed:` minimal change to satisfy the contract.

### 2. Choose the Test Type

| Behaviour | Test type |
|---|---|
| Pure logic, local invariants | Unit |
| Interface/schema contracts | Contract |
| Storage, orchestration, cross-module | Integration |
| Existing behaviour you must preserve | Characterisation |
| State machines, routing, normalisation | Property/invariant |

### 3. Write the Failing Test (this is the spec)

```ts
import { expect, test } from "vitest";

// behaviour contract: claim() rejects takeover when claim_until > now()
test("claim_when_active_lease_returns_false", () => {
  const now = 1000;
  const result = claim({ unitId: "u1", leaseMs: 60_000, now, holder: "w1", existingClaimUntil: now + 30_000 });
  expect(result.acquired).toBe(false);
});
```

Rules:

- Name = `<what>_<when>_<expected>`. The name **is** the contract claim.
- Test through the public contract production callers use. Don't expose `_helpers`.
- Assert what the consumer receives, not internal wiring.

### 4. Verify RED

```bash
npm run test:unit -- path/to/file.test.ts
```

Confirm: fails for the **right** reason (feature missing, not a typo). If it passes immediately, you're testing existing behaviour — fix the test.

### 5. Smallest Production Change

Smallest edit that makes the test green while serving the purpose. No surrounding cleanup, no YAGNI features. Do not weaken the test.

### 6. Verify GREEN

```bash
npm run typecheck:extensions
npm run test:unit -- <pattern>
```

All tests green, zero TS errors. Then refactor while green: tighten types, names, JSDoc.

### 7. Confidence Gate

After each major step, state confidence as `0.0–1.0` plus a one-line reason.

| Step | Threshold | Below threshold |
|---|---|---|
| Purpose & consumer | 0.95 | Run `advisory-partner` Q3 (abuse) + Q5 (strongest objection). |
| Contract test | 0.90 | Adversarial review wave. |
| Implementation | 0.95 | Add a specialist reviewer for the boundary touched. |
| Evidence | 0.97 | Full advocate + challenger + specialist. |

LLM confidence is poorly calibrated in absolute terms — the relative signal matters. If you write 0.7, you're guessing. Act on it.

### 8. Verify Consumer and Record Evidence

- Re-run `rg` for the changed symbol; confirm a real caller still depends on it.
- For non-trivial runtime/provider changes, capture a **before** repro and an **after** repro.
- For non-trivial slices, persist the contract to sf memory:

```
log_reasoning(
  category="contract",
  content="<symbol> — <what the test proved> — slice <sliceId>",
  confidence=0.9
)
```

This feeds the next unit's `research` phase via the memory recall step.

## Authorship Boundary

This skill owns:

- The contract test.
- The first minimal implementation.
- The RED → GREEN proof for one focused slice.

Escalate to wave-style multi-file implementation when:

- The change spans 4+ files with independent test surfaces.
- Sub-parts need independent review gates.
- A bounded refactor remains after the contract is green.

If the change is already green and verified here, go directly to `finish-and-verify`.

## Brownfield Rules

For existing behaviour:

1. Find the real consumer first (`rg`, `git log -- <path>`).
2. Write a characterisation test for the current contract.
3. Decide: correct, accidental, or obsolete?
4. Change code to satisfy the intended contract.
5. Delete tests that only preserve dead compatibility paths.

## Red Flags — STOP

- Code written before a failing test.
- Test passes immediately without seeing it fail (you're testing existing behaviour, or the test is wrong).
- Acceptance criteria like "make it better" or "clean this up".
- No production caller identified.
- Test adjusted to match bad code — fix the code; tests are the spec.
- JSDoc missing `Purpose:` line — code without stated purpose cannot be verified.
- Test asserts call counts or mock interactions — that's mechanical, not purposeful. Label it as a guard or rewrite.

## Bug Fixes

Bug = missing correct-behaviour test.

1. Write a test for the **correct** behaviour. It must fail (RED) because the bug exists.
2. If it passes immediately → the test is testing the broken behaviour. Fix the test first.
3. Fix the code to make the correct-behaviour test GREEN.

Never fix code without a failing test first. The test is the proof that the bug existed and is now gone.

## Evidence (mandatory before declaring done)

- Failing test → passing test result.
- `npm run typecheck:extensions` clean.
- For non-trivial runtime/provider fixes: explicit repro before code, solved boundary after.
- Updated active-slice artifact when one exists in `.sf/active/{unit-id}/`.

Each evidence claim must include a Command/Output trace:

```
### Check: <what was verified>
**Command run:** <exact command>
**Output observed:** <stdout/stderr copy-paste — truncate if long, keep the relevant part>
**Result: PASS** (or FAIL — with Expected vs Actual)
```

Reading test source and asserting "this should pass" is not evidence. Run it; paste the output; then claim PASS.
