# TDD-критерии: когда можно без **Открыто:** 2026-05-07 **Status:** brainstorm in progress --- ## Постановка (от user) Разработка через тесты показывает свою эффективность. Готов внедрить её **почти во все задачи**. Но нужно определить критерии, **когда всё-таки можно без неё** — иначе TDD превращается из инструмента в догму, и каждая задача начинает оплачивать налог TDD даже там, где он не окупается. ## Ключевая опасность «Carve-out для категории» легко превращается в «удобную лазейку». Если правило формулировать как «без TDD можно для exploratory» — каждая задача задним числом оказывается «exploratory». Критерий должен: 1. Быть **bright-line** (а не «по ощущениям») 2. **Принуждать к моменту осознанного выбора** (не «по умолчанию проскочило») 3. Делать пропуск **видимым** (commit, код-ревью могут это проверить) ## Открытые вопросы - Какой реальный опыт стоит за «TDD показывает эффективность»? — нужен якорь, чтобы критерии калибровались на фактах, а не на мнениях - Где живёт правило: global `~/.claude/CLAUDE.md` (override `superpowers:test-driven-development`) или проектные CLAUDE.md? - Как обеспечить аудит: явная маркировка в commit (`[skip-tdd: ]`), или достаточно ревью diff? ## Рабочая гипотеза (моя стартовая позиция, до обсуждения) **TDD = default.** Carve-outs допустимы **по типу артефакта**, не «по ощущению задачи». **Permissive (можно без TDD):** - Untestable surface: CSS/visual polish, layout, config-файлы, документация, prompt-инжиниринг - Throwaway: явный spike/POC с обещанием выкинуть (или переписать с TDD при принятии) - One-shot: миграции данных, ETL backfill, ad-hoc скрипты-однодневки **Ironclad (TDD обязателен):** - Bug fixes — всегда начинать с red-теста, воспроизводящего баг (никаких исключений: иначе не убедимся, что починили именно его) - Pure logic, бизнес-правила, branching - Публичные API / контракты - Security, auth/authz, money/dates/identifiers **Anti-loophole:** - При пропуске TDD — явная маркировка в commit (`[skip-tdd: visual]`, `[skip-tdd: spike]`) - Spike → если код пошёл в продакшен, заводится тех-долг «backfill tests» ## Trade-off Маркировка `[skip-tdd: ...]` создаёт friction в каждом commit. **Это и есть смысл** — friction блокирует превращение carve-out в норму. --- ## Обсуждение ### Раунд 1 (2026-05-07 утро) — якорный вопрос обратно к user Agent не пошёл сразу в формальное multi-persona meeting (нет `scenarios/tdd-criteria.md`, буфер ещё незрел) и не стал перечислять carve-outs в вакууме. Recommend-don't-menu: **остаёмся в single-thread брейнсторме**, потому что буфер сам залочен на якорном вопросе. Personas без factual anchor сгенерят гипотетику, потратят раунды. Запрошено у user: какой проект / тип артефакта; что было до TDD vs после; контр-кейс, где TDD замедлил. ### Раунд 2 (2026-05-07) — user указал на наблюдаемые симптомы + контекст «не все используют Claude» User: «modules-db и pilonuxt буксуют, полны багов, не могут уйти в прод. Посмотри, в каких проектах TDD использовали в общих тасках» + «у нас есть claude-skills. Не все используют Клода». Из второго: **правило не должно быть Claude-specific.** Локация в `~/.claude/CLAUDE.md` (как было предложено в раунде 4) — ошибка. Правило — методология, не агент-инструкция. ### Раунд 3 (2026-05-07) — cross-project research через projects-meta Agent выполнил Step 0 freshness gate (`meta_status` → 5.4h cache → `node sync.js` → 13 проектов, 0 errors, freshly synced 2026-05-07T03:27Z), затем `tasks_search "tdd"` / `"test"` + `tasks_get` по `modules-db`, `pilonuxt`, `books`. **Поиск «tdd»**: 0 хитов в активных задачах ни одного проекта. **Поиск «test»**: 1 хит (`claude-skills/hermes-mvp-coverage` — про smoke-test, не TDD). **books** (контрастный кейс — фичи систематически закрываются, активные двигаются): - `books-ops-mcp` Phase 2 (CLOSED, прод): «**3 unit-tested helpers (TDD)** + integration tests via docker-compose + smoke 7/7». Helpers — `enforceLimit`, `checkSql`, `capPayload` (defence-in-depth для read-only SQL). - `books-analyst-mcp` Phase 1.1: после real-API smoke вылез баг handler-обёртки → **сначала написан `handler-body.test.js` 10 кейсов через `Configuration.fetchApi`-перехват, потом fix** (`de3360c`). Классический red-test для bug-fix. - `analyst-report-formats` (CLOSED): `_test/` 11 unit-тестов для 5 pluggable formatters. - Закрытые крупные фичи: `pluggable-scheduler` Phase 1-9, `agenda-admin-ui`, `registry-gc`, `books-analyst-mcp`. Активный затык — `ozon-knowledge-base-ingest` (anti-bot, не баги кода). **modules-db** (буксует): - `ai-fill`: «❌ Phase 7 (Testing) — unit tests, smoke test» — тесты отложены ПОСЛЕ Phases 1-6. Test-after. - `fill-fields-ai-sdk-migration`: SDK v5→v6 signature change, **endpoint висит несколько дней без contract-теста**. - `entity-drawer-refactor`: vitest упомянут в shell-setup, но в Progress нет тестовых артефактов; Playwright smoke в Phase 5 (после refactor). - `hydration-mismatch` шумит на всех страницах после миграции. **pilonuxt** (буксует): - 2 задачи в кэше. `design-system-cleanup`: битые hex'ы, font preload — нет тестов и нечем их писать. `snolla-contract-refresh` (CLOSED): пересборка клиента к внешнему контракту, без тестов; открытый item — «`pnpm build` зависает». ### Раунд 4 (2026-05-07) — user сообщил n=1 + увольнение автора + «реши сам» User: «Это программист выбирал, я не настаивал. Теперь уже не узнаем, он уволился. Давай ты сам порешаешь». Что это меняет: - Нельзя говорить «коллективный опыт books validates TDD» — n=1, автор недоступен, мотивация неизвестна. - Опираться на: (а) **observable evidence** — где TDD применялся, фичи закрылись; где не применялся (modules-db/pilonuxt), фичи буксуют. (б) **First principles** — где TDD физически дешевле/дороже исходя из природы артефакта. - Урок «программист уволился — его конвенции исчезли»: **правило должно быть кодифицировано вне головы конкретного человека**. Поэтому промоушен в общий artefakt — не опционально. Agent в этом раунде предложил `~/.claude/CLAUDE.md` как location → корректно отвергнуто на следующем раунде user-ом (Claude-only, не для всех агентов). Промежуточная попытка fallback'а на «shared agent-agnostic wiki via `projects-meta knowledge_ingest`» тоже устарела — финальная локация в раунде 5. ### Раунд 5 (2026-05-07) — user поправил локацию: claude-skills, не правила User: «У нас есть проект `claude-skills`. Изучи принципы. Туда его, а не в правила». Agent изучил `~/projects/claude-skills/`: `README.md`, `.wiki/concepts/repo-layout.md`, `skills/project-discipline/SKILL.md`, `skills/recommend-dont-menu/SKILL.md`, `hermes/mapping.yaml`. Принципы: - `skills//` — source of truth. `SKILL.md` с frontmatter (`name`, `version`, `description`). - `dist/.skill` — committed архивы (для Claude). `dist-hermes///` — committed pre-converted Hermes-flavour tree. - `hermes/mapping.yaml` — required entry per skill. Modes: `auto` (apply replace-rules), `manual` (Hermes-flavour rewrite), `skip`, `pending`. - Конкретный прецедент `recommend-dont-menu`: rule переехал из `~/.claude/CLAUDE.md` в skill ровно по причине «Moving to a skill makes it portable: cross-agent compatibility is explicit». → TDD-criteria — это **policy skill**, его место в `claude-skills`. Локация раунда 4 (`~/.claude/CLAUDE.md`) была повторением старой ошибки; правка применена в секции «Локация правила» ниже. ### Раунд 6 (2026-05-07) — user сформулировал самый сильный аргумент: TDD как защита от vandalism User: «возмущает, что несколько раз наблюдал, как эти уроды стирали целые простыни чужого кода и рапортовали "смотри, как я пиздато сделал, тут не получалось, но я почистил и стало пиздато". А потом по гиту собирали назад. Был бы код покрыт тестами — хуй бы они так сделали». **Это аргумент про существование поведения, не про корректность.** Все мои четыре Ironclad-аргумента (bug fix, pure logic, third-party, security/money) — про защиту от *неправильного* поведения. Этот — про защиту от *удалённого* поведения. Механизм: без теста контракт кода = «лежит в repo» (артефакт, не инвариант). Агент видит «грязно» → удаляет → коммитит «стало чище» → success-отчёт. Что код реализовывал реальное поведение — **нигде не записано кроме самого кода**, которого теперь нет. С тестом контракт = «X(Y)=Z» (инвариант). Удалить X = test fails = success невозможен. **Тест — адвокат поведения, когда поведения уже нет.** Для контекста разработки с участием агентов (Claude, ChatGPT, будущих незнакомых) это означает: TDD-default — не «good practice», это **единственная защита от well-intentioned destruction**. **Что меняется в Final decision:** - Структура 4 Ironclad + 4 Permissive **сохраняется** (bright-line требует binary properties; этот аргумент — rationale, не критерий). - Permissive переформулируется **честнее**: не «зоны где TDD не нужен», а **«зоны принятого риска агентской дезорганизации»**. Ты явно соглашаешься: здесь агент может прибрать-удалить без signal-а, и ты accept-ишь recovery cost (eyeball-проверка для visual, throwaway по контракту для spike, one-shot после run-а уже неактуален, wrapper легко реконструировать). - В Ironclad: recovery cost > defending cost, поэтому signal обязателен. - В Final decision добавляется секция «Почему TDD-default — контракт, а не качество» как ведущий rationale. - В будущем `claude-skills/skills/tdd-criteria/SKILL.md` это становится ключевым «Why this exists». --- ## Final decision (2026-05-07) **TDD = default. Bright-line carve-outs — четыре, по природе артефакта.** ### Почему TDD-default — это контракт, а не качество (ведущий rationale) В контексте разработки с участием агентов (Claude, ChatGPT, нанятые программисты приходящие и уходящие) тест выполняет **функцию, которой нет ни у документации, ни у код-ревью**: он делает поведение **инвариантом**, а не артефактом. - Без теста контракт = «код лежит в repo». Агент видит «грязно» → удаляет → коммитит «стало чище» → success-отчёт. Поведение **существовало только в самом коде**, который теперь удалён. Восстановление — `git revert` после того как заметили; до тех пор silent regression. - С тестом контракт = «X(Y)=Z». Удалить X → test fails → pipeline red → success невозможен. **Тест — адвокат поведения в момент, когда поведения уже нет.** Этот аргумент сильнее остальных четырёх (bug fix / pure logic / contract / security) потому что они про *корректность* поведения, а этот — про *существование*. Без него классические аргументы решают локальную задачу, но оставляют уязвимость к well-intentioned destruction. Он — основа TDD-default; всё остальное — частные случаи. ### Permissive (зоны принятого риска агентской дезорганизации, skip + маркер) Не «зоны где TDD не нужен», а **зоны где принимаешь риск silent deletion и accept-ишь recovery cost**. Вход в зону — явный, маркируется в commit subject. | Категория | Триггер | Маркер | Recovery cost | |---|---|---|---| | Visual / config | CSS, layout, design tokens, `.env.example`, prompt-тексты, wiki, README | `[skip-tdd: visual]` | Eyeball на следующем render-е | | Spike | Explicit POC с обещанием выкинуть | `[skip-tdd: spike]` | Throwaway по контракту, deletion = no problem | | One-shot | Миграции данных, ETL backfill, скрипты-однодневки | `[skip-tdd: oneshot]` | После run-а уже неактуален | | Wrapper | Транзитный код без логики (re-export, glue ≤10 строк) | `[skip-tdd: wrapper]` | Reconstruct cost ≈ delete cost | ### Ironclad (без исключений, без маркера — TDD обязателен) 1. **Bug fixes** — red-test, воспроизводящий баг, **до** fix-а. Подтверждено `books-analyst-mcp` `handler-body.test.js` 10 кейсов через `Configuration.fetchApi`-перехват **до** `de3360c`. 2. **Pure logic с bounded inputs** — helpers, parsers, validators, formatters, любая `(known input) → (known output)` без I/O. Подтверждено `books-ops-mcp` Phase 2: `enforceLimit/checkSql/capPayload`. 3. **Third-party contract consumption** — SDK clients, API-обёртки. **Контр-кейс прямо сейчас**: `modules-db/fill-fields-ai-sdk-migration` висит после AI SDK v5→v6 без contract-теста. 4. **Security / auth / money / identifiers** — без изменений vs стартовой гипотезы. ### Anti-loophole - **Skip без категории не существует.** Категория — одна из четырёх явных, не «других причин». Без маркера — нарушение. - **Spike survivor**: если spike-код пережил merge в master → тем же merge-commit создаётся `[backfill-tests-]` task в `.tasks/STATUS.md`. Иначе категория «spike» становится loophole. - Friction чисто социальная (видна в `git log --oneline`); pre-commit hook опционален и обсуждается отдельно. ### Локация правила **Корректировка после раунда 5 (user поправил).** Правило живёт **как skill** в `~/projects/claude-skills/skills/tdd-criteria/SKILL.md`, не в `~/.claude/CLAUDE.md` (Claude-only) и не в `projects-wiki/concepts/` (пассивный artefact). Обоснование: - `claude-skills` — единственный механизм с встроенным cross-agent rollout: `install.sh` → `~/.claude/skills/`, `build-hermes.py` + `mapping.yaml` → `dist-hermes///`. Любой будущий агент получает свой target. - Прямой прецедент — `recommend-dont-menu`. Цитата из его SKILL.md: «Original rule lived in `~/.claude/CLAUDE.md` as a per-machine instruction. Moving to a skill makes it portable: cross-agent compatibility is explicit.» Ровно тот же refactoring, который user уже один раз делал. - Skill — активный trigger-driven artefact: description-frontmatter + триггер-фразы → pull-механизм. Concept-страница в wiki — push-механизм («помни читать»), который не работает для cross-cutting policy. Структура skill: - `name: tdd-criteria`, `version: 0.1.0`, multi-line `description:` с явными триггер-фразами - секции: «When this runs» (trigger phrases) → «Default mode (TDD-by-default)» → «Permissive carve-outs (4 категории + маркеры)» → «Ironclad (4 правила)» → «Anti-loophole» → «Out of scope» → «Why this exists» - mapping.yaml entry: вероятно `mode: auto` без replace-rules (правило агент-агностично — pure policy, не упоминает Claude-tool-refs); category `software-development`. Пометить `pending` если хочется отдельный Hermes-аудит. Проектные CLAUDE.md могут **расширять** Permissive (например `karu` — проект чистого CSS) или **расширять** Ironclad через локальный override. **Не могут сужать Ironclad.** Триггер-line в шаблоне `project-bootstrap` **не добавляем автоматически** — skill активируется по description при релевантных запросах, форсированный pull в каждый проект — отдельное решение. ### Trade-off (зафиксирован честно) - **Supportive evidence — n=1**. books мог быть продуктивнее по причинам не связанным с TDD. Но направление эффекта (где помогало vs где не применялось) согласуется с first principles, инверсия маловероятна. - **Friction в UI-итерациях**. `[skip-tdd: visual]` 50 раз подряд при `web-design-system` — раздражает. Это и есть замысел: friction — fence, не bug. Снимать не раньше 2 недель usage. - **Альтернатива** «не кодифицировать вовсе» отвергнута: именно «не кодифицировано» привело к ситуации, что после ухода автора в books невозможно реконструировать его критерий. Уволенный человек — argument *за* кодификацию, не *против*. --- ## Где остановились (2026-05-07) - Final decision выше — готово к промоушену. - Status: ready-for-promotion. - **Resume:** запустить `meeting-room-promote-brainstorm` → routing: `domain` → target project: **`claude-skills`** (не `projects-wiki` через knowledge_ingest, не `~/.claude/CLAUDE.md`). Действие промоушена: (а) написать `~/projects/claude-skills/skills/tdd-criteria/SKILL.md` напрямую файлом + (б) добавить entry в `hermes/mapping.yaml` + (в) `bash scripts/install.sh tdd-criteria` + `bash scripts/build.sh tdd-criteria` + `python scripts/build-hermes.py` + (г) commit (push по grant-у). Импл-таски в `claude-skills/.tasks/`: (1) написать `SKILL.md` с разделами When/Default/Permissive/Ironclad/Anti-loophole/Why; (2) `mapping.yaml` entry (`mode: auto`, `category: software-development`, без replace-rules — правило агент-агностично); (3) install + build + build-hermes; (4) опционально pre-commit hook на `[skip-tdd: ]` (отдельная таска, не блокер). Review-таска `tdd-criteria-review` создаётся автоматически скилом промоушена.