Сначала спецификация, потом код: требования, сценарии и жизненный цикл изменения
Если не записать решения заранее, агент придумывает их на каждом шаге. И каждый раз немного по-другому. Спецификация фиксирует, что должно получиться, до первой строки кода, и превращается в тесты.
После урока вы сможете
- объяснить, чем разработка по спецификации отличается от «просто попросить агента»;
- писать требования со словами SHALL/MUST и сценарии WHEN/THEN;
- провести изменение по циклу: предложение, дельта спецификации, дизайн, задачи, архив;
- превращать сценарии в каркасы тестов на своём языке;
- находить размытые требования до того, как их прочитает агент.
Стройка по чертежу и без него
Прорабу говорят: «Построй хороший дом». Каждая бригада решает сама, где окна и сколько этажей. С чертежом все строят одно и то же, а приёмка сверяет дом с чертежом по пунктам.
Пять бригад, один дом
анимацияГлавная мысль: спецификация — это договор, который переживает сессии. Агент может забыть разговор. Файл со спецификацией он прочитает заново, а тесты из сценариев не дадут отступить от него.
От простого к сложному
Сначала договориться, потом писать код
Попросите агента: «Сделай ограничение попыток входа». Ему придётся решить за вас: сколько попыток, на какой срок блокировать, считать по IP или по логину, что ответить пользователю. Он решит. Завтра в новой сессии он решит ещё раз, и, возможно, иначе. Такой подход называют вайб-кодингом.
Spec-Driven Development (разработка от спецификации) меняет порядок. Сначала вы записываете решения в файл. Потом агент реализует ровно их. Минимальная единица — требование и сценарий его проверки:
### Requirement: Ограничение попыток входа Система SHALL блокировать вход по логину на 15 минут после 5 неудачных попыток подряд. #### Scenario: Шестая попытка заблокирована - **WHEN** пользователь 5 раз подряд ввёл неверный пароль - **THEN** шестая попытка получает ответ 429 - **AND** вход по этому логину снова доступен через 15 минут
Как устроена спецификация и цикл изменения
Слова SHALL и MUST. Это слова обязательности из стандарта RFC 2119. MUST и SHALL значат «обязательно». SHOULD — «желательно». Слова «желательно, по возможности, быстро» агент прочитает как разрешение сделать как угодно.
Сценарии. Формат пришёл из BDD: GIVEN (дано, исходное состояние), WHEN (когда, действие), THEN (тогда, ожидаемый результат), AND (и ещё). Каждый сценарий — готовый тест.
Нефункциональные требования. Скорость, безопасность, ограничения. Тоже с числами: «ответ SHALL приходить быстрее 300 мс для 95% запросов», а не «должно работать быстро».
Цикл изменения. Возьмём инструмент OpenSpec. Текущая правда о системе лежит в openspec/specs/. Каждое изменение — отдельная папка в openspec/changes/<имя>/:
proposal.md— зачем меняем и что именно.specs/<возможность>/spec.md— дельта: какие требования добавлены (ADDED), изменены (MODIFIED), удалены (REMOVED).design.md— как будем делать технически, если это не очевидно.tasks.md— шаги-чекбоксы для реализации.- Реализация и проверка: агент идёт по задачам, тесты из сценариев зелёные.
- Архив: дельта вливается в
openspec/specs/, папка изменения уходит в архив.
Похожий путь у GitHub Spec Kit: команды /specify, /plan, /tasks ведут от спецификации к плану и задачам. Инструменты разные, идея одна.
Пример файлов изменения целиком — proposal, дельта, tasks
# openspec/changes/add-login-rate-limit/proposal.md ## Why Ботами перебирают пароли. За неделю 12 000 неудачных входов с 40 логинов. ## What Changes - Блокировка входа по логину после 5 неудачных попыток на 15 минут. - Ответ 429 с понятным текстом и временем до разблокировки. ## Impact - Затронуто: auth (вход), redis (счётчики).
# openspec/changes/add-login-rate-limit/specs/auth/spec.md ## ADDED Requirements ### Requirement: Ограничение попыток входа Система SHALL блокировать вход по логину на 15 минут после 5 неудачных попыток подряд. #### Scenario: Шестая попытка заблокирована - **WHEN** пользователь 5 раз подряд ввёл неверный пароль - **THEN** шестая попытка получает ответ 429 #### Scenario: Успешный вход сбрасывает счётчик - **GIVEN** у логина 4 неудачные попытки - **WHEN** пользователь ввёл верный пароль - **THEN** счётчик неудачных попыток равен 0
# openspec/changes/add-login-rate-limit/tasks.md ## 1. Реализация - [ ] 1.1 Счётчик неудачных попыток в redis с TTL 15 минут - [ ] 1.2 Проверка лимита перед проверкой пароля, ответ 429 - [ ] 1.3 Сброс счётчика при успешном входе ## 2. Проверка - [ ] 2.1 Тест на каждый сценарий из spec.md - [ ] 2.2 Ручная проверка на стенде
Исполняемая спецификация и связь с harness
Спецификация как часть процесса. Она не лежит мёртвым документом. Harness встраивает её в работу агента:
- скил «реализуй изменение» читает
tasks.mdи берёт первую незакрытую задачу; - после каждой задачи агент отмечает чекбокс, так прогресс переживает новую сессию;
- из каждого сценария получается тест, и тест падает, пока поведение не совпало с договором;
- проверяющий агент или скрипт сверяет: у каждого требования есть сценарий, у каждого сценария есть тест;
- несколько агентов (урок 10) делят задачи из одного
tasks.mdи не спорят о целях.
Три ловушки.
- Размытые слова. «Быстро», «удобно», «корректно», «по возможности». Агент трактует их как хочет. Замените числом или сценарием.
- Требование без сценария. Нельзя проверить — значит, нельзя доказать, что сделано.
- Спецификация разошлась с кодом. Код поменяли руками, а спецификацию нет. Через месяц агент «починит» код обратно по старой спецификации. Лечится архивом изменений и правилом: поведение меняем только через изменение спецификации.
Тест по сценарию на четырёх языках — «Шестая попытка заблокирована»
<?php
// tests/Auth/LoginRateLimitTest.php — PHPUnit
final class LoginRateLimitTest extends WebTestCase
{
/** Scenario: Шестая попытка заблокирована */
public function testSixthAttemptIsBlocked(): void
{
$client = static::createClient();
// WHEN пользователь 5 раз подряд ввёл неверный пароль
for ($i = 0; $i < 5; $i++) {
$client->jsonRequest('POST', '/api/login', ['login' => 'anna', 'password' => 'wrong']);
self::assertResponseStatusCodeSame(401);
}
// THEN шестая попытка получает ответ 429
$client->jsonRequest('POST', '/api/login', ['login' => 'anna', 'password' => 'wrong']);
self::assertResponseStatusCodeSame(429);
}
}
# tests/test_login_rate_limit.py — pytest
def test_sixth_attempt_is_blocked(client):
"""Scenario: Шестая попытка заблокирована"""
# WHEN пользователь 5 раз подряд ввёл неверный пароль
for _ in range(5):
r = client.post("/api/login", json={"login": "anna", "password": "wrong"})
assert r.status_code == 401
# THEN шестая попытка получает ответ 429
r = client.post("/api/login", json={"login": "anna", "password": "wrong"})
assert r.status_code == 429
// test/login-rate-limit.test.js — node:test
import { test } from "node:test";
import assert from "node:assert/strict";
import { login } from "./helpers.js";
test("Scenario: Шестая попытка заблокирована", async () => {
// WHEN пользователь 5 раз подряд ввёл неверный пароль
for (let i = 0; i < 5; i++) {
assert.equal((await login("anna", "wrong")).status, 401);
}
// THEN шестая попытка получает ответ 429
assert.equal((await login("anna", "wrong")).status, 429);
});
// test/login-rate-limit.test.ts — node:test + TypeScript
import { test } from "node:test";
import assert from "node:assert/strict";
import { login } from "./helpers";
test("Scenario: Шестая попытка заблокирована", async () => {
// WHEN пользователь 5 раз подряд ввёл неверный пароль
for (let i = 0; i < 5; i++) {
const res: Response = await login("anna", "wrong");
assert.equal(res.status, 401);
}
// THEN шестая попытка получает ответ 429
assert.equal((await login("anna", "wrong")).status, 429);
});
Проведите изменение от идеи до архива
Сквозной пример: ограничение попыток входа. Пять неудачных попыток — блокировка на 15 минут.
Жизненный цикл изменения
интерактив 1 из 4Изменение начинается с вопроса «зачем».
Линтер требований
интерактив 2 из 4Напишите требование или выберите пример. Линтер ищет то, что агент прочитает двусмысленно. Такой же линтер вы напишете в песочнице 1.
От сценария к тесту
интерактив 3 из 4Выберите сценарий и язык. Каркас теста собирается по шаблону: имя из названия сценария, шаги — комментарии, проверки — из строк THEN.
Две команды, пять сессий
интерактив 4 из 4Одна и та же задача, пять сессий агента подряд. Между сессиями память чата пропадает. Слева есть только переписка, справа — спецификация в репозитории.
Только чат
Спецификация в репозитории
Песочницы с автопроверкой
Код выполняется прямо в браузере. Нажмите «Запустить» или Ctrl+Enter.
Песочница 1. Линтер требований
JSНапишите lintRequirement(text). Вернуть массив кодов проблем без повторов и в таком порядке:
NO_MODAL— нет словаSHALLилиMUST(заглавными, отдельным словом);VAGUE_WORD— есть хоть одно слово из массиваVAGUE(регистр не важен);NO_SCENARIO— нет строки, начинающейся с#### Scenario:;NO_THEN— сценарий есть, но нет строки сTHEN: она может начинаться с-и быть обёрнута в**.
Песочница 2. Разбор сценариев
JSНапишите parseScenarios(md). Каждый блок начинается строкой #### Scenario: имя. Строки шагов могут начинаться с - , ключевое слово может быть в **…**. GIVEN, WHEN, THEN кладут текст в свой массив. AND добавляет в последний использованный массив. Остальные строки пропускаем. Результат: [{ name, given: [], when: [], then: [] }].
Зачем это harness-у — откройте после решения
Разобранные сценарии превращаются в каркасы тестов, как в интерактиве 3. Ещё их используют для проверки: в каждом изменении у каждого требования есть хотя бы один сценарий, у каждого сценария — THEN. Такая проверка в CI или хуке (урок 4) ловит размытую спецификацию до того, как агент начнёт писать код.
Проверьте себя
Чем требование отличается от сценария?
Требование — правило. Сценарий — его проверяемый случай. Из сценария получается тест.
Какое требование агент поймёт однозначно?
Числа, ключ блокировки, срок и проверяемый результат. В первых двух агенту придётся решать за вас.
Что происходит при архивации изменения?
После архива openspec/specs/ снова описывает систему как она есть. Следующее изменение начнётся от актуальной правды.
В новой сессии агент поставил лимит 10 попыток вместо договорённых 5. Как исправить надёжно?
Чат пропадёт вместе с сессией. Файл в репозитории и красный тест — нет.
Задание на своём проекте — 40 минут
- Возьмите задачу из своего трекера, где есть хоть одно решение: число, срок, правило.
- Напишите
proposal.mdв три строки: зачем, что меняем, что затронуто. - Напишите одно требование с SHALL и два сценария: обычный и граничный случай.
- Прогоните требование через линтер из интерактива 2. Уберите размытые слова.
- Разбейте работу на 4–6 задач в
tasks.md. Дайте агенту задачу «реализуй по tasks.md, начни с тестов по сценариям». Сравните с тем, как он делал это без спецификации.
Отметьте, что усвоили
- Понимаю, почему без спецификации агент каждый раз решает по-новому.
- Пишу требования со словами SHALL или MUST и с числами.
- Пишу сценарии WHEN/THEN и знаю, что каждый сценарий — будущий тест.
- Знаю цикл изменения: предложение, дельта, дизайн, задачи, реализация, проверка, архив.
- Понимаю, где лежит текущая правда (specs) и где изменения (changes).
- Узнаю ловушки: размытые слова, требования без сценариев, спецификация разошлась с кодом.
- Решил песочницы: линтер требований и разбор сценариев.
- Знаю, когда спецификация лишняя: опечатки, константы, однострочные фиксы.
Что почитать и где попробовать
Материалы на русском отмечены зелёной меткой. Все ссылки проверены 18 октября 2026 года.
Почитать
- Что такое OpenSpec? Полный гайд по Spec-Driven Developmentтеория SDD и устройство изменения в OpenSpec. рус.
- Внутри Spec-Driven Development: на что способен Spec Kitрабочий процесс GitHub Spec Kit по этапам и реалистичные ожидания. рус., перевод
- От vibe coding к Spec-Driven Developmentпример от X5: specification.md → plan.md → tasks.md для магазина на FastAPI. рус.
- Введение в BDDот TDD к BDD, сценарии Given/When/Then с примерами на python-behave. рус.
- Given When Thenкороткая заметка Фаулера о формате сценариев. англ.
Песочницы по теме
- JupyterLitePython прямо в браузере: записать сценарий Given/When/Then как тест-функцию и прогнать.
- Mermaid Live Editorнарисовать диаграмму состояний к спецификации до написания кода.
Запустить код из урока
- TypeScript PlaygroundJS и TS из примеров урока: вставить и запустить кнопкой Run.
- 3v4l.orgPHP-примеры урока на любой версии PHP.
- JupyterLitePython-примеры урока прямо в браузере, без установки.