| name | write-product-spec |
| description | Написание PRODUCT.md для значимой пользовательской фичи — детальное поведение как нумерованные проверяемые инварианты. Использовать, когда нужна продуктовая спека/PRD, нужно зафиксировать поведение до реализации, или фича достаточно крупная/неоднозначная, чтобы письменная спека улучшила реализацию и ревью. |
| vibeVersion | 1.1.0 |
write-product-spec
Написать PRODUCT.md для значимой фичи.
Суть
Продуктовая спека делает желаемое поведение настолько однозначным, что агент реализует его корректно и без регрессий. Описывать фичу строго с позиции пользователя — что он видит, делает и переживает, и какие инварианты для него обязаны выполняться. Без деталей реализации: внутренних типов, раскладки состояния, границ модулей, потоков данных, алгоритмов.
«Пользователь» — не только конечный пользователь приложения, а потребитель проектируемой поверхности:
- UI/UX-фича → человек в приложении;
- модель данных → код, который её читает и пишет;
- API/протокол/библиотека → вызывающие: другие сервисы, клиентский код, плагины, агенты;
- CLI или developer-facing поверхность → разработчик, который её вызывает.
Спека описывает поведение с позиции этого потребителя: форма поверхности, доступные операции, что он получает в ответ, инварианты, на которые может опираться, edge cases, которые обязан обработать — без предписания внутренней реализации.
Реализация, валидация и план тестирования живут в парном TECH.md (скилл write-tech-spec). Продуктовая спека — обычно первый шаг двухшагового процесса: после согласования PRODUCT.md вызывается write-tech-spec (или пользователю явно сообщается, что это ожидаемый следующий шаг). Писать так, чтобы техспека выводилась из продуктовой напрямую.
Путь: specs/<id>/PRODUCT.md. Правила выбора <id> и работы с трекером — в скилле spec-driven-implementation.
Перед написанием
Собрать только необходимый контекст: id каталога, краткое описание фичи, целевые пользователи, ключевые поведения, edge cases. Недостающее — спросить у пользователя, а не угадывать.
Дизайн-моки
Если у фичи есть UI или интерактивный дизайн — до черновика секции Behavior спросить, существует ли мок (Figma, Sketch, скриншот). Мок — самый надёжный источник правды о визуальных состояниях, отступах и краевых раскладках; не спросить — значит дать Behavior угадывать намерения, которые дизайнер уже решил.
- Ссылка дана → короткая секция
## Design (или строка в начале Behavior): Design: <link>.
- Мока нет, подтверждено → явная пометка
Design: none provided.
- Фича чисто backend (модель данных, API, CLI без визуала) → вопрос пропустить, секцию опустить.
Не ронять дизайн-контекст молча: явный «none» лучше отсутствия упоминания там, где дизайн обычно ожидается.
Структура
Обязательные секции:
- Summary — 1–3 предложения: фича и желаемый результат.
- Behavior — мясо спеки: исчерпывающее описание работы фичи нумерованными проверяемыми инвариантами. Вся длина спеки зарабатывается здесь; остальное остаётся тонким, чтобы не дублировать.
Опциональные — только когда добавляют сигнал сверх ядра; пустую секцию не писать (никаких «None»):
- Problem — только если мотивация неочевидна из Summary.
- Goals / Non-goals — когда рамки неоднозначны или оспаривались.
- Design — см. «Дизайн-моки» выше; для невизуальных фич опустить целиком.
- Open questions — предпочитать инлайновые
**Open question:** … рядом с соответствующим поведением; отдельная секция — только если нерешённых вопросов несколько и их стоит собрать.
Не включать секции Validation, Success criteria, Testing — валидация и тест-план живут в TECH.md. Behavior пишется нумерованными инвариантами, проверяемыми сами по себе, — техспека ссылается на них напрямую.
Секция Behavior
Behavior — это и есть спека. Всё остальное — обрамление.
Цель — полное описание работы фичи, достаточно детальное, чтобы техспеку можно было написать прямо из него, не угадывая и не выводя продуктовое намерение заново. Если читатель заканчивает Behavior с вопросами «а что фича делает в ситуации X» — секция не готова.
Описать как минимум:
- поведение по умолчанию и happy-path сценарий;
- каждое видимое пользователю состояние и переходы между ними;
- все входы пользователя и реакцию фичи на них;
- пустые состояния, ошибки, loading/pending, отмену;
- edge cases, о которых разумный исполнитель не догадается спросить: permission denied, оффлайн, таймауты, гонки между изменениями состояния, несколько параллельных инстансов, протухшие/отсутствующие данные, потеря фокуса посреди взаимодействия, взаимодействие со смежными фичами;
- клавиатуру, доступность и фокус, где релевантно;
- инварианты, обязанные выполняться всегда, и поведения, которые не должны регрессировать.
Длина Behavior — по фиче: тривиальной хватит горстки инвариантов, сложной нужны десятки с подсекциями по сценариям. Сомневаешься — перечисли на один edge case больше, а не меньше.
Эвристика длины
Behavior — столько, сколько требует фича; edge cases ради лимита строк не резать. Эвристика — для обрамления вокруг Behavior:
- тривиальный фикс или узкая UI-правка: спека не нужна;
- малая фича (один модуль, мало edge cases): всего ~30–60 строк;
- средняя (кросс-модульная, несколько состояний): ~80–150 строк;
- крупная/поведенчески богатая: длиннее — нормально, и почти вся длина должна жить в Behavior.
Одна и та же мысль повторяется в Summary, Problem, Goals и Behavior → схлопывать обрамление, не контент Behavior.
Принципы письма
- Конкретное наблюдаемое поведение вместо лозунгов.
- Behavior — списком инвариантов, а не прозой, где возможно.
- Фиксировать инварианты-нерегрессии и легко упускаемые edge cases.
- Детали реализации — только если неизбежны для UX.
- Секция либо отрабатывает место, либо удаляется.
Актуальность спеки
Поддержание PRODUCT.md синхронным с реализацией (тот же PR, обновление при изменении поведения/UX) — по каноническим правилам скилла implement-specs.
Связанные скиллы
implement-specs, write-tech-spec, spec-driven-implementation
Пример секции Behavior
Образец для гипотетической фичи — рендеринг GFM-таблиц в блочном списке чата/терминала. Демонстрирует ожидаемую форму: нумерованные, проверяемые инварианты с позиции пользователя — умолчания, edge cases, битый ввод, стриминг, выделение/копирование, поиск, шаринг, темизация, кросс-поверхностная консистентность, один инлайновый открытый вопрос.
## Behavior
1. Когда блок вывода содержит GFM-таблицу (строка заголовка, строка-разделитель из сегментов `---` и одна или более строк тела, всё разделено `|`), она рендерится как визуально оформленная таблица, а не как сырой pipe-текст.
2. Таблица рендерится с: визуально выделенной строкой заголовка; выравниванием колонок по разделителю (`|:---|` влево, `|:---:|` по центру, `|---:|` вправо; `|---|` без двоеточий — дефолт: текст влево, числовые значения вправо); видимыми разделителями строк в соответствии с активной темой.
3. Инлайновый markdown внутри ячейки рендерится инлайново: жирный, курсив, инлайн-код, зачёркивание и ссылки — как в окружающем блоке. Переносы внутри ячейки (`<br>` или экранированный `\n`) — как внутриячеечные переносы.
4. Ширины колонок подбираются под естественное содержимое, пока таблица помещается в блок. Очень длинная ячейка переносит текст внутри своей колонки, а не растягивает колонку до неразумной ширины.
- **Open question:** если перенесённая ячейка делает строку неразумно высокой — клипуем с аффордансом «развернуть» или даём строке расти неограниченно?
5. Горизонтальный скролл: общая ширина превышает блок (много колонок или несжимаемо широкие) → таблица скроллится горизонтально внутри блока, открывая офф-скриновые колонки без клиппинга. Вертикальный скролл блока работает независимо.
6. При ресайзе блока (окно, сплит панели, открытие сайдбара) таблица перевёрстывается под новую ширину без потери порядка строк и колонок.
7. Пустые ячейки рендерятся видимо пустыми (та же высота строки, без плейсхолдеров). Строка из одних пустых ячеек всё равно рендерится строкой.
8. Таблица из заголовка и разделителя без строк тела — как header-only таблица, не как сырой текст.
9. Одноколоночная таблица — одноколоночной таблицей (не схлопывается в список).
10. Битые таблицы деградируют аккуратно: нет разделителя → preformatted-текст, не таблица; рваные строки (меньше/больше ячеек, чем в заголовке) → недостающие пустыми, лишние показываются, заголовок по возможности визуально расширяется — блок никогда молча не теряет данные; незакрытая таблица (последняя строка обрезана стримом) → частичная таблица, см. (11).
11. Стриминг: пока команда выдаёт строки, таблица рендерится инкрементально — новые строки дописываются по мере прихода. Заголовок фиксируется по получении строки-разделителя; строки до разделителя рендерятся обычным текстом, пока таблица не распознана.
12. Выделение и копирование: выделение мышью/клавиатурой по ячейкам выбирает видимый текст; копирование выделения по умолчанию даёт tab-separated текст (строка на строку, ячейки через tab); аффорданс (контекстное меню, шорткат) копирует исходный markdown; копирование блока целиком сохраняет исходный markdown дословно.
13. Поиск внутри блока матчится по тексту ячеек; совпадения подсвечиваются на месте в отрендеренной ячейке; навигация по совпадениям доскролливает таблицу, включая горизонтально к офф-скриновой колонке.
14. Шаринг/экспорт блока сохраняет исходный markdown, не отрендеренную форму.
15. Темизация: рамки, фон заголовка, чередование строк (если есть), стили ссылок/кода — из активной темы приложения. Без хардкода цветов.
16. GFM-таблицы рендерятся консистентно везде, где блочный markdown уже рендерится: одинаковый вход — одинаковая таблица на каждой поверхности.
17. Не-табличный pipe-контент не рендерится таблицей по ошибке: текст с `|`, но без валидной пары «заголовок + разделитель» остаётся обычным текстом, даже если визуально похож на таблицу.