Featured image of post Spec-Driven Development: контроль AI-кодогенерации

Spec-Driven Development: контроль AI-кодогенерации

10 спецификаций, 20 минут, 85% воспроизводимости. Как формализация требований превращает AI-кодогенерацию в контролируемый процесс.

В этой статье


Проблема

Большие MR

4000 строк в одном MR. Три часа на ревью, 12 замечаний, исправления - ещё 800 строк. На четвёртом заходе я закрыл вкладку и понял: проблема не в коде, а в том, что никто не знал, что именно нужно было написать.

Если ты работаешь с большими кодовыми базами, ситуация знакомая. Большие MR - симптом. Когда непонятно, что именно нужно сделать, разработчик пишет больше кода, чем требуется. Добавляет на всякий случай. Покрывает сценарии, которые никто не просил. MR растёт не потому что задача большая, а потому что границы размыты.

Другая причина - иллюзия, что проще сделать всё в одной задаче, чем декомпозировать. Кажется, что разбиение создаёт лишнюю работу. На практике монолитный MR на 4000 строк никто не может нормально проверить, и баги просачиваются в продакшн.

AI-кодогенерация

С AI эта проблема становится критичнее. Агент пишет код быстро, но если требования размыты - генерирует то же самое: много кода на всякий случай.

Я пользуюсь AI ежедневно. Claude Code - основной инструмент, в который можно подключить разные модели: Anthropic, DeepSeek, GPT-семейство, локальные через Ollama. И в какой-то момент заметил закономерность: чем точнее формулирую задачу, тем лучше результат. Промпты становились всё более структурированными - простые инструкции, потом шаблоны, потом что-то похожее на техническое задание.

Где проблема? Сначала я думал, что ленюсь давать AI детальные инструкции. Потом решил, что AI Agent собирает недостаточный контекст - нужен RAG или что-то подобное. В итоге понял, что проблема в обоих местах: создавать полные инструкции кажется оверкилом, а как помочь агенту собрать нужный контекст - непонятно.

Но главное - нечего проверять. Нет артефакта, на который можно указать и сказать: здесь написано одно, а сделано другое.

Идея

Идея не новая - в индустрии давно говорят о spec-first подходе. Я долго хотел попробовать и наконец решил проверить.

Если формализовать требования в виде спецификации до начала кодирования, то:

  1. AI Agent будет генерировать более предсказуемый код
  2. Результат можно будет валидировать против спецификации
  3. Архитектура останется контролируемой

Когда AI Agent генерирует сотни строк кода за минуту, единственный способ контролировать результат - иметь формальное описание того, что должно получиться, и инструмент, который проверит соответствие реализации спецификации. О втором - в отдельной статье.

Метод

Я решил проверить гипотезу на реальном проекте - инструменте для построения архитектурных графов из Go кода. Правило простое: ни одной строчки кода без спецификации.

Первая спецификация - создать пустой проект на Go со стандартным layout. Вторая - модель графа. Третья - анализатор Go кода. За несколько дней накопилось 10 завершённых спецификаций.

Структура хранения

Kanban-подобная организация через файловую систему:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
specs/
├── todo/           # очередь задач
│   ├── 0010-feature-x.md
│   └── 0020-feature-y.md
├── inprogress/     # в работе (максимум одна - WIP limit)
│   └── 0005-current.md
└── done/           # выполнено
    ├── 0001-init-project.md
    ├── 0003-data-model.md
    └── 0004-go-analyzer.md

Приоритизация через числовой префикс: меньше число - выше приоритет. Переход между состояниями - перемещение файла между директориями.

graph LR TODO["todo/"] --> INPROGRESS["inprogress/"] INPROGRESS --> DONE["done/"]

Размеры спецификаций

Классификация по времени на написание спецификации (T-shirt sizing):

  • S (Small) - до 10 минут
  • M (Medium) - 10-20 минут
  • L (Large) - больше 20 минут

Размер определяет глубину проработки. S-задача: Problem Statement и 5 Acceptance Criteria. L-задача: полные UML/C4 диаграммы, детальные Requirements, 15+ критериев приёмки. Корреляция между размером спецификации и предсказуемостью результата - прямая.

Если начинаешь с нуля - попробуй с S-спецификации. Это минимальные затраты на эксперимент.

Пример S-спецификации: инициализация проекта


# Spec 0001: Initialize Standard Golang Project Layout

**Metadata:**
- Priority: 0001
- Status: Done
- Effort: S

## Overview
### Problem Statement
Необходимо создать базовую структуру Go проекта для инструмента
archlint согласно стандартным практикам разработки.

### Solution Summary
Инициализировать Go module и создать минимальную структуру проекта.

## Requirements
### R1: Go Module Initialization
- Инициализировать Go module с именем github.com/mshogin/archlint

### R2: Minimal Directory Structure
- Создать cmd/archlint/ для точки входа
- Создать internal/ для приватного кода
- Создать pkg/ для публичных библиотек

## Acceptance Criteria
- [x] AC1: go.mod создан с module path github.com/mshogin/archlint
- [x] AC2: cmd/archlint/main.go существует и компилируется
- [x] AC3: Директории internal/ и pkg/ существуют

Минимум деталей, максимум конкретики. AI получает чёткие границы задачи.

Пример M-спецификации: команда collect

Средние задачи требуют диаграмм. Я экспериментировал с Sequence-диаграммами - отправлял их агенту вместе с требованиями. Заметил, что по ним AI Agent выдаёт в целом то, что ожидается.


# Spec 0006: Implement Collect Command

**Metadata:**
- Priority: 0006
- Status: Done
- Effort: M

## Overview
### Problem Statement
Необходимо реализовать команду collect для сбора архитектуры
из исходного кода и сохранения графа в YAML файл.

### Solution Summary
Создать подкоманду collect, которая использует GoAnalyzer
для анализа кода и сохраняет результат в YAML формате.

## Architecture
### Sequence Flow (PlantUML)

@startuml
title Sequence: Collect Command

actor User
participant "collectCmd" as CC
participant "GoAnalyzer" as GA
participant "saveGraph" as SG

User -> CC: archlint collect . -o arch.yaml
CC -> GA: Analyze(dir)
GA --> CC: *Graph
CC -> SG: saveGraph(graph)
SG --> CC: nil
CC --> User: "Graph saved to arch.yaml"
@enduml

## Requirements
### R1: Command Definition
var collectCmd = &cobra.Command{
    Use:   "collect [директория]",
    Short: "Сбор архитектуры из исходного кода",
    Args:  cobra.ExactArgs(1),
    RunE:  runCollect,
}

### R2: Flags
-o, --output: выходной YAML файл (default: architecture.yaml)
-l, --language: язык программирования (default: go)

## Acceptance Criteria
- [x] AC1: Команда принимает директорию как аргумент
- [x] AC2: Флаги -o и -l работают
- [x] AC3: Результат сохраняется в YAML
- [x] AC4: Выводится статистика по компонентам

Sequence-диаграмма определяет порядок вызовов. AI следует ей буквально.

Пример L-спецификации: анализатор Go кода

Большие задачи требуют нескольких диаграмм - Data Model и Sequence:


# Spec 0004: Implement Go Code Analyzer

**Metadata:**
- Priority: 0004
- Status: Done
- Effort: L

## Overview
### Problem Statement
Необходимо реализовать анализатор Go кода, который парсит исходный код
с помощью AST и строит граф зависимостей между компонентами.

### Solution Summary
Создать GoAnalyzer в пакете internal/analyzer, который использует
go/ast и go/parser для анализа Go файлов и построения графа.

## Architecture
### Data Model

@startuml
class GoAnalyzer {
  -packages: map[string]*PackageInfo
  -types: map[string]*TypeInfo
  -functions: map[string]*FunctionInfo
  -nodes: []model.Node
  -edges: []model.Edge
  --
  +NewGoAnalyzer() *GoAnalyzer
  +Analyze(dir string) (*model.Graph, error)
  -parseFile(filename string) error
  -buildGraph()
}

class PackageInfo {
  +Name: string
  +Path: string
  +Imports: []string
}

class TypeInfo {
  +Name: string
  +Package: string
  +Kind: string
  +Fields: []FieldInfo
}

GoAnalyzer "1" *-- "*" PackageInfo
GoAnalyzer "1" *-- "*" TypeInfo
@enduml

### Sequence Diagram

@startuml
title Sequence: Code Analysis

actor User
participant "GoAnalyzer" as GA
participant "go/parser" as GP
participant "buildGraph" as BG

User -> GA: Analyze(dir)
loop For each .go file
  GA -> GP: ParseFile(filename)
  GP --> GA: *ast.File
  GA -> GA: Extract packages, types, functions
end
GA -> BG: buildGraph()
BG --> GA: *Graph
GA --> User: *Graph, nil
@enduml

## Requirements
### R1: AST Parsing
- Парсить все .go файлы в директории
- Извлекать packages, types, functions, methods

### R2: Graph Building
- Создавать Node для каждого компонента
- Создавать Edge для каждой связи (import, calls, uses)

### R3: External Dependencies
- Идентифицировать внешние зависимости
- Помечать их как entity: external

## Acceptance Criteria
- [x] AC1: Анализатор корректно парсит Go код
- [x] AC2: Все типы компонентов извлекаются
- [x] AC3: Все типы связей определяются
- [x] AC4: Внешние зависимости идентифицируются
- [x] AC5: Граф сериализуется в YAML

Для L-задач несколько диаграмм - необходимость. Data Model, Sequence, Component - вместе они задают архитектуру приложения и управляют зависимостями между компонентами.

Что делает AI

AI ускоряет работу, но архитектуру я не делегирую: решения фиксируются в спецификации, проходят ревью и проверяются валидаторами. Однако решения редко рождаются из воздуха: я приношу первичные варианты и ограничения, агент предлагает альтернативы и подсвечивает слепые зоны. Это влияет на ход мысли, и я это осознаю. Финальное “да/нет” и ответственность лежит на мне.

У агента две зоны ответственности.

  1. Specification Editor Я диктую голосом сырой поток мыслей (диктую быстрее, чем печатаю). Агент приводит его к моему шаблону спецификации: раскладывает по секциям, уточняет недосказанное, формулирует требования и критерии приемки так, чтобы их можно было проверить. После этого я ревьюю и фиксирую спецификацию как исходный контракт.

  2. Implementation Executor Когда спецификация согласована, я отдаю её агенту на реализацию. Агент пишет код по спецификации, а я проверяю результат: ревью, валидация, итерации до тех пор, пока архитектура не станет чистой и предсказуемой.

graph LR A["Идеи/варианты (я)"] --> B["AI дополняет и формализует"] B --> C["Спецификация"] C --> D["Ревью и решения (я)"] D --> E["Реализация (AI)"]

Эксперимент

За несколько дней - 10 завершённых спецификаций и работающий проект. Код соответствует архитектуре из диаграмм.

Чтобы проверить, насколько спецификации самодостаточны, я провёл эксперимент: дал Claude Code пустую директорию и 10 спецификаций из archlint - без доступа к исходному коду. Задача: воссоздать проект с нуля.

Результат за 20 минут:

  • 85.5% успешность воспроизведения
  • 100% структурная идентичность (директории, файлы, типы)
  • 23 мутации в деталях реализации

Структура проекта воспроизведена полностью. Все acceptance criteria из спецификаций выполнены. Проект компилируется и проходит тесты.

Мутации возникли там, где спецификации описывали что делать, но не как. Критический пример: алгоритм построения sequence-диаграммы был реализован иначе - функционально эквивалентно, но с другой логикой обхода стека вызовов. Ещё одна категория мутаций - стилистические: язык комментариев, порядок функций в файлах, именование переменных.

Вывод для улучшения спецификаций: для критических алгоритмов нужен псевдокод или конкретные примеры входов/выходов. Спецификация что + как даёт более точное воспроизведение, чем только что.

Полный отчёт с каталогом мутаций: github.com/mshogin/archlint-reproduction

Идемпотентность спецификаций

Чтобы спецификации оставались воспроизводимыми, все изменения должны проходить через них. Никаких доработок в режиме copilot, никаких чатов с “поправь вот это”. Каждое изменение - обновление спецификации, затем реализация.

Это основной вызов. Хочется быстро поправить баг в диалоге, а не возвращаться к спеке. Но каждая такая правка - потеря воспроизводимости.

Trade-off очевиден:

  • Нужен результат здесь и сейчас - режим copilot быстрее
  • Нужна воспроизводимость - только через спецификации

Выбор зависит от контекста. Прототип или эксперимент - copilot. Продуктовый код с долгим жизненным циклом - спецификации.

Ограничения

Подход не решает проблемы автоматически. Он не заменяет понимание предметной области, не придумывает требования и не гарантирует хороший дизайн. Он делает ошибки видимыми раньше и заставляет держать архитектуру в рамке.

Цена: время на написание спецификаций, их ревью и итерации после валидации. Если относиться к спекам формально, всё скатывается обратно в хаос. Честно спроси себя: готов ли ты тратить 10-30 минут на спецификацию, чтобы агент реализовал её за 5-20 минут?

Детали реализации варьируются. Эксперимент с воспроизводимостью показал 23 мутации - алгоритмы интерпретируются по-разному, стилистика кода отличается. Для критических участков нужен псевдокод, а не только описание.

Я думаю, что подход работает хорошо там, где есть поставленный, сформированный и рабочий процесс. В процессах с фокусом на дисциплину, на понятные зоны ответственности, ревью, критерии готовности. Можно смотреть на этот процесс как на конвейер, доставляющий ПО 24/7.

Итоги

Гипотеза подтвердилась:

  1. AI генерирует более предсказуемый код - да, при наличии диаграмм
  2. Результат можно валидировать - да, 85.5% воспроизводимость
  3. Архитектура остаётся контролируемой - да, 100% структурная идентичность

Суть простая: без спецификации нечего проверять, со спецификацией - есть артефакт для валидации. Не нужен идеальный AI или идеальный промпт.

Эксперимент продолжается.


Шаблоны и примеры: github.com/mshogin/archlint

Если пробуешь spec-driven подход или уже используешь - расскажи в комментариях, что работает, а что нет. Пишу о практике AI-кодогенерации и архитектуре в Telegram: @MikeShogin

Создано при помощи Hugo
Тема Stack, дизайн Jimmy