Files
claude-skills/.wiki/concepts/tdd-criteria-design.md

11 KiB
Raw Blame History

date, source, status, type, title, ingested_at, ingested_by, source_project
date source status type title ingested_at ingested_by source_project
2026-05-07 .meeting-room/.archive/2026-05-07-tdd-criteria.md promoted design tdd-criteria-design 2026-05-07T04:01:23.616Z OpeItcLoc03@DESKTOP-NSEF0UK .meeting-room

TDD-criteria — design rationale

Design document for the tdd-criteria skill. Captures why the rule is shaped the way it is — the SKILL.md itself is the runtime artefact (procedure + triggers); this page is the argument.

Problem statement

User is ready to adopt TDD as default across projects but wants bright-line carve-outs to prevent TDD from degenerating into a tax that every task pays. The risk is the «exploratory loophole»: if the rule is «without TDD is OK for exploratory work», every task ex post facto becomes «exploratory».

Bright-line means: at decision time, the agent (human or LLM) answers each criterion with yes/no based on observable property — no «мне кажется», no judgment calls. If a criterion needs intuition, it's not a criterion, it's a loophole.

The argument behind TDD-default

The four classical arguments — bug fixes are easier with red-tests; pure logic is cheap to test; third-party contracts silently break; security/money has asymmetric blast radius — are about correctness of behaviour. They tell you when to defend against wrong behaviour.

The strongest argument, and the leading rationale in this design, is different: TDD-default is the only mechanism that protects behaviour from being silently deleted.

Mechanism:

  • Without a test, the contract for a piece of code is «it exists in the repo». That's an artefact, not an invariant. An agent (LLM coder, new hire, future self) sees «messy code» → deletes it → commits «cleaner now» → reports success. That the code implemented real behaviour is recorded nowhere except the code, which is now gone. Recovery is git log archaeology, after somebody notices the regression — days or weeks later.
  • With a test, the contract is «X(Y)=Z». That's an invariant. Delete X → test fails → pipeline red → success can't be reported. The test is the advocate of the behaviour at the moment when the behaviour itself no longer exists.

In a workflow that includes LLM coders (Claude, ChatGPT, future unknowns) and rotating contractors, this isn't theoretical. The user has personally observed agents deleting working code and reporting «I cleaned it up» — recovery required manual git archaeology. Tests would have made that impossible.

This reframes everything below:

  • Ironclad rules (TDD obligatory) — zones where recovery cost > defending cost. Signal mandatory.
  • Permissive carve-outs — zones of accepted risk for agentic vandalism, not «zones where TDD doesn't apply». You explicitly accept that an agent can delete-and-claim-success here, because recovery is cheap (eyeball the next render; throwaway by contract; one-shot already ran; wrapper trivial to reconstruct).

Decision algorithm

Walk through 8 questions top-to-bottom. First «yes» determines mode. All «no» → TDD by default.

1. Это исправление бага?                            → TDD (red-test первым)
2. Это код, потребляющий внешний контракт            → TDD (contract-test)
   (SDK, REST API, чужая schema)?
3. Это security / auth / money / identifiers?       → TDD
4. Это pure logic — функция (input → output)        → TDD
   без I/O, без global state, bounded inputs?

5. Это visual / config — CSS, layout, design        → SKIP, [skip-tdd: visual]
   tokens, .env.example, prompt-тексты,
   wiki, README?
6. Это явно объявленный spike (зафиксировано        → SKIP, [skip-tdd: spike]
   в commit/PR/task subject «POC, выкину»)?           + spike-survivor task если выживет
7. Это one-shot скрипт — миграция, ETL backfill,    → SKIP, [skip-tdd: oneshot]
   ad-hoc cleanup, runs once?
8. Это транзитный wrapper ≤10 строк                 → SKIP, [skip-tdd: wrapper]
   без branching (re-export, glue)?

(default)                                           → TDD

Ironclad — why TDD is cheaper than skipping

# Rule Checkable property Why TDD here
1 Bug fix «есть issue / failure log / repro?» Bug already reproduced — red-test is just codifying the repro. Marginal cost: 5 min. Marginal benefit: regression test forever. Without it, «fixed» = checked once, regresses on next refactor.
2 Pure logic, bounded inputs «функция читает диск/сеть/БД/mutates global state?» — no Cheapest TDD surface: no fixtures, no mocks, no setup. Test = input/output pair. Marginal cost ≈ 0. Skip is gratuitous.
3 Third-party contract «вызывает чужой SDK / парсит чужую schema?» SDK bumps change signatures silently. Contract-test pins «known input → known shape». Catches the break at first npm install instead of «endpoint висит несколько дней». Direct counter-case observed: modules-db/fill-fields-ai-sdk-migration post AI SDK v5→v6.
4 Security / auth / money / IDs «касается токенов/паролей/валюты/идентификаторов/прав?» Asymmetric blast radius: false positive (slow code, slow test) ≪ false negative (account takeover, data corruption, money loss). TDD = insurance.

Plus the meta-rationale: in all four, recovery cost from silent deletion is high. The test is the only artefact that makes deletion visible.

Permissive — accepted-risk zones

Not «TDD doesn't apply». «You accept that an agent can vandalise this without immediate signal, because recovery is cheap.» Entry into the zone is explicit, marked in the commit subject.

# Category Trigger Marker Recovery cost (= reason TDD doesn't pay)
5 Visual / config CSS, layout, design tokens, .env.example, prompts, wiki, README [skip-tdd: visual] Eyeball on next render. Visual regression infra exists (Playwright screenshots, Percy) but heavyweight for most projects.
6 Spike Explicit POC «throwaway» in commit/PR/task subject [skip-tdd: spike] Throwaway by contract — deletion isn't a problem. Survivor rule: if spike code reaches master, the same merge-commit creates [backfill-tests-<slug>] task. Otherwise this category becomes the loophole.
7 One-shot Migrations, ETL backfill, ad-hoc cleanup; runs once [skip-tdd: oneshot] Test never re-executes — cost not recovered. After run, deletion is irrelevant.
8 Wrapper ≤10 lines, no branching (re-export, glue) [skip-tdd: wrapper] Test on function foo(x) { return bar(x) } re-states bar. Reconstruct cost ≈ delete cost. The defence is on bar, not foo.

Anti-loophole

  • Skip без категории не существует. One of four explicit categories — not «other reasons». No marker = violation, regardless of perceived justification.
  • Spike survivor rule. If spike code is merged to master → same merge-commit creates [backfill-tests-<slug>] task in .tasks/STATUS.md. Otherwise «spike» becomes «skipped tests forever».
  • Friction is the point. [skip-tdd: visual] 50 раз подряд при web-design-system итерации раздражает — это и есть замысел. Friction = fence, not bug. If it becomes unbearable, re-evaluate after ≥2 weeks of usage, not before.

What's excluded as not bright-line

These tempting formulations were rejected:

  • «Exploratory» — every task becomes exploratory in retrospect.
  • «When time is short» — time is never abundant.
  • «When the logic is obvious» — it seems obvious until the first bug.
  • «When the code is temporary» — there's no such thing; temporary code becomes permanent.

All require judgment at decision time → automatically become loopholes. The four Permissive categories are bound to observable properties (file extension, marker in commit subject, directory, line count) — not mood.

Composite tasks

A task that doesn't fit any single category cleanly is a composite task — break it down by artefact type.

Example: «add a user-profile-settings page»:

  • HTML/CSS layout → [skip-tdd: visual]
  • Validation form (email format, password strength) → Ironclad-4 (security) → TDD
  • API call wrapper for save → Ironclad-3 (third-party contract if PUT to external endpoint) → TDD
  • Update Pinia store reducer → Ironclad-2 (pure logic if bounded reducer) → TDD
  • Hook useProfileForm composing the above → wrapper if ≤10 lines glue, else Ironclad-2

One «task» yields 4-5 commits with different modes. The criterion applies per artefact, not per task. This is the point — no «overall this is exploratory».

Project-level overrides

Project CLAUDE.md files may extend Permissive (e.g. karu is a pure-CSS project — wider visual carve-out is appropriate) or extend Ironclad with project-specific categories (e.g. books could add «scheduler tasks touching Mongo state» as Ironclad-5). Project files may not narrow Ironclad — the global floor stands.

Trade-offs (honest)

  • Supportive evidence is n=1. The observable contrast is books (TDD applied selectively, features ship) vs modules-db/pilonuxt (TDD not applied, projects buksuyut). books could be more productive for reasons unrelated to TDD (codebase maturity, author experience, task type). The direction-of-effect agrees with first principles, so inversion is unlikely, but the magnitude is uncertain.
  • Friction in UI iterations. [skip-tdd: visual] repeated 50× during a design-system rework is annoying. Intentional. Don't relax before ≥2 weeks of usage.
  • «Don't codify at all» was rejected. Precisely «not codified» is what produced the situation in books where, after the author left, his TDD criterion can't be reconstructed — it lived only in his head and his commits. A departed contributor is an argument for codification, not against.
  • The anti-vandalism argument is uncomfortable. It says: «I don't trust agents not to delete my code». That's the position. If it changes, this rationale changes. Until then, this is what's load-bearing.

Cross-agent applicability

Pure policy — no Claude-specific tool references (no Read/Edit/Bash/Glob calls in the SKILL.md body). Hermes mapping: mode: auto, category: software-development, no replace-rules. Future agents (Gemini, Copilot, in-house) inherit via their respective rollout adaptors.

See also

  • Skill artefact: claude-skills/skills/tdd-criteria/SKILL.md (runtime: triggers + procedure).
  • Hermes mapping: claude-skills/hermes/mapping.yaml entry tdd-criteria.
  • Original brainstorm: .meeting-room/.archive/2026-05-07-tdd-criteria.md (full discussion arc with 6 rounds + observable evidence from mcp__projects-meta__tasks_get on books/modules-db/pilonuxt).
  • Precedent for «move a rule from ~/.claude/CLAUDE.md into a skill»: claude-skills/skills/recommend-dont-menu/SKILL.md («Why this exists» section).