В предыдущем уроке мы разобрали, когда SDD не стоит брать: быстро меняющиеся требования, enterprise-легаси с кросс-командными зависимостями. Это правда — но не приговор. Проблема не в самом SDD, а в попытке применить его сразу и везде. В большую кодовую базу SDD входит инкрементально — и именно эту механику мы сейчас разберём.

Главная проблема: спека уже есть — только она невидима

В greenfield-проекте спека предшествует коду. В легаси всё наоборот: код написан, тесты (если есть) описывают поведение частично, а настоящие требования живут в головах разработчиков, в старых тикетах Jira и в комментариях к PR трёхлетней давности. Это «спек-долг» (spec debt) — накопленная невыраженная спецификация.

Попытка написать полную спеку на кодовую базу из 500k строк заранее обречена: займёт месяцы, устареет до завершения и встретит сопротивление команды. Нужна другая стратегия.

Check yourself
Тимлид предлагает «написать спеки на всё» перед тем, как двигаться дальше в легаси-кодовой базе. Почему это плохая идея и что делать вместо этого?
Quick recall
Что такое спек-долг в контексте легаси-кода?

Три стратегии входа

Все три применяются параллельно — выбор зависит от типа задачи.

Стратегия «острова»: новые фичи — всегда по спеке

Самый простой вход. Любой новый модуль или новая фича пишется по SDD-воркфлоу с нуля: requirements.mddesign.mdtasks.md. Старый код не трогаем.

Эффект накапливается быстро: через три-четыре спринта в кодовой базе уже есть несколько «островов» с документированным поведением. Команда видит разницу между «островами» и легаси — и постепенно хочет расширять покрытие.

Стратегия «сейма»: фиксируем контракты на границах

«Сейм» (seam) — граница в коде, через которую можно вставить спеку без переписывания модуля целиком. Хорошие кандидаты: публичные API, HTTP-эндпоинты, интерфейсы между сервисами, точки интеграции с внешними системами.

Спека для сейма — это контракт: что модуль принимает, что возвращает, какие инварианты соблюдает. Внутренняя реализация за сеймом не описывается — только наблюдаемое поведение.

# Контракт: PaymentService.processPayment()

WHEN авторизованный пользователь вызывает processPayment с валидными данными
  AND сумма не превышает лимит транзакции
THEN сервис возвращает transactionId в течение 5 секунд


::widget{id="rc-2"}

## Acceptance Criteria
- [ ] При сумме выше лимита — PaymentLimitExceeded, транзакция не создаётся
- [ ] Повторный вызов с тем же requestId возвращает тот же transactionId
- [ ] Тайм-аут шлюза обрабатывается как GATEWAY_TIMEOUT, не как успех

Такая спека полезна даже без рефакторинга: агент понимает контракт и не нарушает его при добавлении кода вокруг.

Стратегия «ретроспеки»: фиксируем поведение перед рефактором

Перед рефакторингом модуля пишем спеку на его текущее поведение. Это инвертированный SDD: не «спека → код», а «код → спека → рефакторинг».

Процесс:

1. Читаем существующие тесты — переводим в EARS-нотацию (помните шаблон «WHEN … THE SYSTEM SHALL …» из урока про анатомию спеки).

2. Разговариваем с людьми, которые писали или поддерживали этот код, — вытаскиваем неявные ограничения.

3. Проверяем спеку на граничных кейсах и сверяем с реальным поведением кода.

4. Добавляем acceptance criteria для неочевидных случаев.

После этого рефакторинг идёт под защитой спеки: если новый код нарушает зафиксированное поведение — это видно сразу.

Check yourself
Чем ретроспека отличается от тестов? У вас уже 80% тест-покрытие — нужна ли ретроспека перед рефактором?
flowchart TD A[Новая задача] --> B{Новый модуль или фича?} B -- Да --> C[Стратегия острова] B -- Нет --> D{Рефакторинг существующего?} D -- Да --> E[Стратегия ретроспеки] D -- Нет --> F{Граница между модулями?} F -- Да --> G[Стратегия сейма] F -- Нет --> H[Уточните тип задачи]
flowchart TD
    A[Новая задача] --> B{Новый модуль или фича?}
    B -- Да --> C[Стратегия острова]
    B -- Нет --> D{Рефакторинг существующего?}
    D -- Да --> E[Стратегия ретроспеки]
    D -- Нет --> F{Граница между модулями?}
    F -- Да --> G[Стратегия сейма]
    F -- Нет --> H[Уточните тип задачи]
Выбор стратегии внедрения SDD в зависимости от типа задачи

Практический воркфлоу для команды

Шаг 1 — Выбираем точку входа. Берём компонент с наибольшим количеством изменений за последние 90 дней. High churn = максимальная боль от неясного поведения, поэтому здесь спека даст быструю отдачу.

Шаг 2 — Ретроспека втроём. Разработчик + тестировщик + продакт или аналитик. Каждый видит разный срез требований — только вместе они дают полную картину.

Шаг 3 — Любая новая задача в этом компоненте начинается со спеки. Не «потом дообновим»: сначала проверяем, покрывает ли существующая спека нужное поведение; если нет — дописываем до реализации.

Шаг 4 — Расширяемся на соседние компоненты. Начинаем с тех, которые чаще всего взаимодействуют с первым.

К пятому спринту SDD становится нормой для нового кода, ретроспека постепенно покрывает высокорисковые легаси-области.

Частые ловушки

«Исторический контекст» как неявное требование. В легаси часто встречается логика, смысл которой непонятен без пятилетней истории: «здесь умножаем на 0.97, потому что в 2019-м был специфический договор с клиентом». При ретроспеке такие места должны стать явными — как минимум в виде комментария, как максимум в виде requirement с контекстом.

Конфликт между спекой и тестами. Иногда тесты проверяют поведение, которое было ошибкой, а не фичей. Ретроспека вскрывает это противоречие. Не спешите «исправлять» спеку под тесты — сначала выясните, какое поведение является правильным.

Scope creep при ретроспекинге. Начали с одного модуля, заметили связанный, захотели описать и его — через неделю пишете спеку на половину системы. Ограничивайте ретроспеку жёстко по периметру: один модуль, один контракт за раз.

Check yourself
Во время ретроспекинга модуля вы обнаружили, что несколько тестов проверяют поведение, которое команда считает ошибочным. Ваши действия?
Quick recall
Почему исторический контекст, спрятанный в коде, представляет проблему при ретроспеке?

Признаки, что SDD приживается

  • PR на новую фичу всё чаще содержат requirements.md без напоминания
  • Ревьюеры спрашивают «а где это в спеке?» вместо «а зачем это вообще нужно?»
  • Онбординг новых разработчиков на старые модули идёт быстрее — спека даёт контекст, который раньше передавался только устно
  • Агент реже делает «неожиданные» допущения в новом коде

На кодовую базу с двухлетней историей уйдёт 3–6 месяцев, чтобы SDD стал нормой для нового кода. Но начало — одна спека, один компонент — даёт результат уже в первый спринт.

Где и как хранить спеки в легаси-проекте

Вопрос не риторический: если спеки лежат в Confluence, через полгода они расходятся с кодом, и никто об этом не узнает до очередного инцидента. Если в Google Docs — ещё хуже, потому что там нет diff и review. Единственная рабочая схема на практике: спеки живут в репозитории рядом с кодом.

Co-location — основной принцип

Спека модуля хранится в той же директории, что и его код. Это означает, что она попадает в тот же PR, ревьюится теми же людьми и версионируется вместе с реализацией. Если кто-то меняет поведение — изменение в спеке видно в diff автоматически.

Для большого монорепо или проекта с несколькими сервисами структура выглядит примерно так:

repository/
├── services/
│   ├── payment/
│   │   ├── src/
│   │   │   └── PaymentService.ts
│   │   ├── tests/
│   │   └── specs/                        # <-- спеки рядом с сервисом
│   │       ├── requirements.md           # WHAT: бизнес-требования в EARS-нотации
│   │       ├── design.md                 # HOW: архитектурные решения
│   │       ├── contracts/
│   │       │   ├── processPayment.md     # контракт на каждый публичный метод
│   │       │   └── refundPayment.md
│   │       └── decisions/
│   │           └── 001-idempotency.md    # ADR: почему выбрали именно этот подход
│   └── notifications/
│       ├── src/
│       └── specs/
│           ├── requirements.md
│           └── contracts/
│               └── sendEmail.md
├── shared/
│   └── specs/                            # межсервисные контракты и глобальные инварианты
│       ├── error-codes.md
│       └── auth-contract.md
└── docs/
    └── sdd-guide.md                      # как команда ведёт спеки (один файл, не wiki)

Что кладём в каждый файл

requirements.md — только бизнес-требования. Без деталей реализации. Сюда пишем EARS-шаблоны и acceptance criteria. Этот файл читается продактом — если он требует знания кода, чтобы понять его, что-то пошло не так.

design.md — архитектурные решения: какие паттерны использованы, почему именно они, какие альтернативы отброшены. Не «как работает код», а «почему он устроен именно так».

contracts/ — по одному файлу на каждый публичный интерфейс или HTTP-эндпоинт. Именно здесь живут контракты в стиле «сейма» из предыдущего раздела. Удобно, когда нужно быстро найти спеку конкретного метода, не читая весь requirements.md.

decisions/ — Architecture Decision Records (ADR). Нумерованные файлы: 001-..., 002-.... Здесь фиксируем «исторический контекст» — те самые умножить на 0.97 из-за договора 2019 года. Формат простой: контекст → рассмотренные варианты → принятое решение → последствия.

Naming conventions и дисциплина

Несколько правил, которые реально работают:

  • Файлы спек — только Markdown. Никаких .docx, которые нельзя нормально сравнить в PR.
  • Директория всегда называется specs/, не spec/, не documentation/, не docs/ внутри сервиса. Единообразие важнее личных предпочтений.
  • Контракты называются по методу или эндпоинту: processPayment.md, POST-orders.md. Легко искать.
  • ADR только добавляются, никогда не редактируются. Если решение изменилось — пишем новый ADR со ссылкой на старый.

Линтинг и CI

Чтобы спеки не протухали молча, можно добавить в CI простую проверку: если изменился файл в src/ без изменений в specs/ — пайплайн выдаёт предупреждение (не ошибку на старте, иначе команда начнёт обходить). Пример для GitHub Actions:

- name: Check spec coverage
  run: |
    changed_src=$(git diff --name-only origin/main | grep 'src/')
    changed_specs=$(git diff --name-only origin/main | grep 'specs/')
    if [ -n "$changed_src" ] && [ -z "$changed_specs" ]; then
      echo "::warning::Изменён код в src/, но specs/ не обновлены. Проверьте, нужна ли обновлённая спека."
    fi

Это не блокирует merge, но создаёт видимость: ревьюер видит предупреждение и задаёт вопрос.

Confluence и другие wiki — когда они уместны

Совсем отказываться от wiki не нужно. Confluence хорошо подходит для: онбординг-материалов, высокоуровневых описаний домена, диаграмм, которые активно обсуждаются. Но эти документы должны ссылаться на спеки в репозитории, а не дублировать их. Правило простое: если документ меняется вместе с кодом — он в репо. Если он меняется по другим причинам (бизнес-контекст, процессы команды) — он в wiki.