Klepa AI
Установить
Часть 3 · Работа по плану · Урок 5

Сначала спецификация, потом код: требования, сценарии и жизненный цикл изменения

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

~50 минут 4 интерактива 2 песочницы с проверками Примеры: JS · TS · PHP · Python

После урока вы сможете

  • объяснить, чем разработка по спецификации отличается от «просто попросить агента»;
  • писать требования со словами SHALL/MUST и сценарии WHEN/THEN;
  • провести изменение по циклу: предложение, дельта спецификации, дизайн, задачи, архив;
  • превращать сценарии в каркасы тестов на своём языке;
  • находить размытые требования до того, как их прочитает агент.
1 Аналогия

Стройка по чертежу и без него

Представьте

Прорабу говорят: «Построй хороший дом». Каждая бригада решает сама, где окна и сколько этажей. С чертежом все строят одно и то же, а приёмка сверяет дом с чертежом по пунктам.

Пять бригад, один дом

анимация
Без чертежа
По чертежу
Чертёж= спецификация (spec.md)
Акт приёмки по пунктам= сценарии, из которых делают тесты
План работ= список задач (tasks.md)
Новая бригада каждое утро= новая сессия агента без памяти
Журнал изменений проекта= папка изменения и её архив

Главная мысль: спецификация — это договор, который переживает сессии. Агент может забыть разговор. Файл со спецификацией он прочитает заново, а тесты из сценариев не дадут отступить от него.

2 Объяснение

От простого к сложному

Уровень 1 · Суть

Сначала договориться, потом писать код

Попросите агента: «Сделай ограничение попыток входа». Ему придётся решить за вас: сколько попыток, на какой срок блокировать, считать по IP или по логину, что ответить пользователю. Он решит. Завтра в новой сессии он решит ещё раз, и, возможно, иначе. Такой подход называют вайб-кодингом.

Spec-Driven Development (разработка от спецификации) меняет порядок. Сначала вы записываете решения в файл. Потом агент реализует ровно их. Минимальная единица — требование и сценарий его проверки:

### Requirement: Ограничение попыток входа
Система SHALL блокировать вход по логину на 15 минут
после 5 неудачных попыток подряд.

#### Scenario: Шестая попытка заблокирована
- **WHEN** пользователь 5 раз подряд ввёл неверный пароль
- **THEN** шестая попытка получает ответ 429
- **AND** вход по этому логину снова доступен через 15 минут
Что важноТребование говорит, что система обязана делать. Сценарий говорит, как это проверить. Требование без сценария — пожелание.
Уровень 2 · Детали

Как устроена спецификация и цикл изменения

Слова SHALL и MUST. Это слова обязательности из стандарта RFC 2119. MUST и SHALL значат «обязательно». SHOULD — «желательно». Слова «желательно, по возможности, быстро» агент прочитает как разрешение сделать как угодно.

Сценарии. Формат пришёл из BDD: GIVEN (дано, исходное состояние), WHEN (когда, действие), THEN (тогда, ожидаемый результат), AND (и ещё). Каждый сценарий — готовый тест.

Нефункциональные требования. Скорость, безопасность, ограничения. Тоже с числами: «ответ SHALL приходить быстрее 300 мс для 95% запросов», а не «должно работать быстро».

Цикл изменения. Возьмём инструмент OpenSpec. Текущая правда о системе лежит в openspec/specs/. Каждое изменение — отдельная папка в openspec/changes/<имя>/:

  1. proposal.md — зачем меняем и что именно.
  2. specs/<возможность>/spec.md — дельта: какие требования добавлены (ADDED), изменены (MODIFIED), удалены (REMOVED).
  3. design.md — как будем делать технически, если это не очевидно.
  4. tasks.md — шаги-чекбоксы для реализации.
  5. Реализация и проверка: агент идёт по задачам, тесты из сценариев зелёные.
  6. Архив: дельта вливается в 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 Ручная проверка на стенде
Уровень 3 · Усложнение

Исполняемая спецификация и связь с 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);
});
3 Интерактив

Проведите изменение от идеи до архива

Сквозной пример: ограничение попыток входа. Пять неудачных попыток — блокировка на 15 минут.

Жизненный цикл изменения

интерактив 1 из 4
Предложениеproposal.md
Дельтаspec.md
Дизайнdesign.md
Задачиtasks.md
Кодреализация
Проверкатесты
Архивв specs/
    Нажмите «Дальше»
    Изменение начинается с вопроса «зачем».

    Шаг 0

    Линтер требований

    интерактив 2 из 4

    Напишите требование или выберите пример. Линтер ищет то, что агент прочитает двусмысленно. Такой же линтер вы напишете в песочнице 1.

    Как его увидит агент
    Качество: —

      От сценария к тесту

      интерактив 3 из 4

      Выберите сценарий и язык. Каркас теста собирается по шаблону: имя из названия сценария, шаги — комментарии, проверки — из строк THEN.

      
                
      
              

      Две команды, пять сессий

      интерактив 4 из 4

      Одна и та же задача, пять сессий агента подряд. Между сессиями память чата пропадает. Слева есть только переписка, справа — спецификация в репозитории.

      Только чат

        Спецификация в репозитории

          Сессия 0 из 5
          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 минут
          1. Возьмите задачу из своего трекера, где есть хоть одно решение: число, срок, правило.
          2. Напишите proposal.md в три строки: зачем, что меняем, что затронуто.
          3. Напишите одно требование с SHALL и два сценария: обычный и граничный случай.
          4. Прогоните требование через линтер из интерактива 2. Уберите размытые слова.
          5. Разбейте работу на 4–6 задач в tasks.md. Дайте агенту задачу «реализуй по tasks.md, начни с тестов по сценариям». Сравните с тем, как он делал это без спецификации.
          5 Чек-лист

          Отметьте, что усвоили

          • Понимаю, почему без спецификации агент каждый раз решает по-новому.
          • Пишу требования со словами SHALL или MUST и с числами.
          • Пишу сценарии WHEN/THEN и знаю, что каждый сценарий — будущий тест.
          • Знаю цикл изменения: предложение, дельта, дизайн, задачи, реализация, проверка, архив.
          • Понимаю, где лежит текущая правда (specs) и где изменения (changes).
          • Узнаю ловушки: размытые слова, требования без сценариев, спецификация разошлась с кодом.
          • Решил песочницы: линтер требований и разбор сценариев.
          • Знаю, когда спецификация лишняя: опечатки, константы, однострочные фиксы.
          Урок пройденАгент теперь строит по чертежу. Следующий шаг: часть кода по чертежу можно строить вообще без модели, генератором.