BDD для AI-агентов

Почему практика 2006 года про разговоры между PM, QA и инженером оказалась подходящим форматом для работы с агентом

9 мая 2026

Справочник

BDD помогает участникам разработки договориться об ожидаемом поведении системы на конкретных примерах. Сценарии Gherkin можно использовать и как контекст для агента, и как основу автоматизированных проверок. В статье разбираю discovery, formulation и automation, устройство сценариев и проверку соответствия реализации договоренностям.

Тезис: BDD - формат, в котором PM, инженер и агент читают одно и то же

BDD появился задолго до AI-агентов. Дэн Норт в статье 2006 года «Introducing BDD» переписал TDD в терминах поведения, потому что слово «тест» сбивало команды с толку: люди думали, что пишут тесты, на самом деле они описывали ожидаемое поведение системы. Cucumber и Gherkin закрепили формат: текст, который читает stakeholder, и тот же текст, который исполняет тестовый раннер.

Сам формат намеренно прост. В Gherkin около десяти ключевых слов, и они образуют декларативную грамматику с фиксированным порядком:

Ключевое слово Назначение
Feature:Заголовок документа, одна на файл
Background:Общие предусловия, выполняются перед каждым сценарием в файле
Rule:Группировка сценариев под общим бизнес-правилом (Gherkin 6+)
Scenario: или Example:Один тест-кейс
Scenario Outline: + Examples:Параметризованный сценарий с таблицей данных
GivenИсходное состояние («система в таком-то состоянии»)
WhenСобытие или действие («пользователь делает X»)
ThenОжидаемое наблюдаемое последствие
And, ButПродолжение цепочки шагов того же типа
*Звездочка как замена любому шаговому ключевому слову (для bullet-стиля)
@-тегиМетаданные сценария (приоритет, scope, owner, контрибутирующий спек)
# ...Комментарии
"""...""" или ```...```Doc strings - multi-line аргумент шага (str)
| a | b |Data tables - табличный аргумент шага

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

При этом грамматика составная. Background + Scenario Outline + Examples + теги + doc strings + data tables в одном файле дают достаточную выразительность для любого слоя acceptance-тестирования: pure-unit-стиль (один Given, один When, один Then), интеграционный стиль (multi-step Given для setup, табличные Examples для матрицы кейсов), end-to-end стиль (Background для общего контекста + цепочка When/Then через нескольких акторов или этапов - спорная практика, см. ниже).

Канонический минимальный сценарий выглядит так:

Feature: Magic-link sign-in

  Scenario: User signs in with a fresh magic link
    Given a registered user "alice@example.com"
    When the user requests a magic link
     And the user opens the link within 15 minutes
    Then the user is signed in

В большом проекте структура раскладывается на несколько уровней. Один .feature-файл соответствует одному «slice» функциональности (например, magic-link sign-in или csv-export). Один каталог - одной capability (auth, agents, tools, ...). Все капабилити лежат в общем top-level дереве features/. Внутри файла:

Полный пример - закрытая регистрация (invite-only mode) с шапкой, feature-level тегами и тремя сценариями:

# Capability: auth
# Contributing specs:
#   - 008-close-registration            (introduces the kill-switch)
#   - reference/auth-flows              (cross-cutting auth invariants)

@capability:auth @spec:008 @spec:reference-auth-flows
Feature: Closed registration (waitlist mode)
  As a product team operating in invite-only mode
  I want public signup blocked with a clear waitlist response
  So that we can flip the registration kill-switch in under 5 minutes

  Background:
    Given a clean tenant for this scenario

  @spec:008 @user-story:008-1 @smoke @p1
  Scenario: Landing CTA shows waitlist label when registration is closed
    Given the registration kill-switch is on
    When an anonymous visitor opens the landing page
    Then the primary CTA reads "Join the waitlist"

  @spec:008 @user-story:008-3 @integration @p1
  Scenario: New email signup returns 503 with registration_closed error
    Given the registration kill-switch is on
    When a new email signup is attempted with email "bob@example.com"
    Then the response is 503 with body field error="registration_closed"
     And no magic-link token is created
     And no email is sent

  @spec:008 @user-story:008-2 @smoke @p1
  Scenario: Existing user's magic link still works when registration is closed
    Given the registration kill-switch is on
     And a registered user "alice@example.com"
    When the user requests a magic link
    Then the user receives an email with a one-time link
     And the user can sign in with that link

Те же ключевые слова работают на любом уровне - от одного inline-теста до 40-сценарийной capability с feature-level Background. Это и есть «композиция»: формат не растет сложнее по мере роста проекта; добавляются только количество файлов, тегов и капабилити.

Один нюанс, на котором BDD-сообщество расходится. Канонический Gherkin предписывает один When на сценарий: «Describe an initial context (Given steps), describe an event (When steps), describe an expected outcome (Then steps)». Жесткая интерпретация - «one scenario, one behavior» - отстаивается, например, Andrew Knight (Automation Panda); по его терминологии это «cardinal rule of BDD», а длинные When/Then цепочки в imperative UI-сценариях («open browser - click - check - click - check») - явный антипаттерн.

Liz Keogh, одна из учеников Дэна Норта и активных BDD-практиков, явно опровергает жесткое правило: бывают сценарии, в которых смысл - именно в последовательности нескольких событий или взаимодействии нескольких акторов. Ее каноничный пример из «On multiple Givens, Whens, and Thens»:

Given Clare is editing trade 12345
When Stephen edits and saves that trade
 And Clare tries to save that trade
Then she should be told that Stephen has already edited it.

Здесь два разных When отражают двух акторов; смысл сценария - в их временном чередовании, а не в одном изолированном событии. Сводя такой сценарий к одному When, теряется контракт - именно конкуренция и порядок действий проверяются.

Безопасный baseline: один When на сценарий, как рекомендует Cucumber-документация и большинство руководств по «better Gherkin». Цепочка When/Then используется как осознанное исключение в трех контекстах - end-to-end-сценарии (по терминологии Knight это допустимое исключение), race-condition-проверки (Liz Keogh), API-flow с несколькими цепными вызовами. Если сценарий распадается на «click → check → click → check → click → check» - это сигнал, что его стоит разбить на несколько отдельных Scenario:.

Те же восемь-десять строк сценария работают как PRD для PM, контракт для QA и исполняемый тест для CI. Когда между PM и кодом появляется AI-агент, файл начинает работать еще в одном качестве - как контекст, который определяет границы сгенерированного кода.

Причина - в том, как агент работает с контекстом. Агент не помнит вчерашнего разговора между PM и инженером. Он не догадывается, что фраза «онбординг» в задаче означает «регистрация + welcome-email + первичная настройка профиля». Он работает с тем, что записано в репозитории. Если запись «как должно быть» (PRD в Confluence) и запись «как проверяется» (тесты в репо) - два разных файла на двух разных платформах, агент будет держать в контексте одну из них и игнорировать другую. Дрейф между ними агент не увидит, потому что он не делает Confluence cross-check на каждый запрос.

Gherkin-файл, лежащий в репозитории, читаемый PM и исполняемый pytest-bdd, делает эту проблему ненаблюдаемой по построению: один источник, одна проверка, один артефакт в Pull Request.

Три практики BDD по cucumber.io: discovery, formulation, automation

Cucumber-документация разбирает BDD на три практики:

Когда в команде появляется агент, эти три практики не отменяются - они перераспределяются.

Discovery остается за людьми

Discovery - это набор практик про разговор между бизнесом и инженерами на конкретных примерах. Cucumber-проект и BDD-канон называют три устоявшихся формата:

  1. Three amigos meeting - встреча трех ролей: PM (или business analyst), инженер, QA. Каждая роль приносит свой угол: PM знает intent, инженер знает constraints, QA знает граничные случаи. Цель - найти расхождения в понимании до того, как они станут багами. Длительность 30-60 минут на одну user story.
  2. Example mapping (Burton, 2015; canonical Cucumber-практика) - структурированная сессия на 25-30 минут вокруг одной user story. Используются карточки четырех цветов:
    • желтая - сама user story сверху карты;
    • синие - acceptance criteria (бизнес-правила, которые должны выполняться);
    • зеленые - конкретные примеры, иллюстрирующие каждое правило;
    • красные - открытые вопросы, на которые в этой сессии нет ответа.
    Сессия заканчивается, когда группа удовлетворена scope-ом story, либо когда красных карточек становится больше зеленых - в этом случае story считается недостаточно проработанной для имплементации и возвращается в backlog для дополнительного research.
  3. Specification by example (Adzic, 2011) - общее название для практики, в которой acceptance criteria формулируются через конкретные примеры, а не абстрактные требования. Example mapping - одна из конкретных техник под этим зонтиком.

Ожидаемый артефакт discovery-сессии - не Gherkin (это уже formulation, следующий шаг). Артефакт - это структурированная заметка с тремя секциями:

В большинстве команд этот артефакт фиксируется в Notion / Miro / Confluence-странице или в issue-трекере как комментарии к story. Формальный шаблон не критичен; критичны три секции - правила, примеры, вопросы.

Этот артефакт затем превращается в Gherkin на стадии formulation: каждое бизнес-правило (синяя карточка) часто становится группой сценариев в .feature-файле; каждый конкретный пример (зеленая карточка) - отдельным Scenario: или строкой в Examples: у Scenario Outline.

Агент в discovery не участник:

На этапе discovery участники уточняют задачу и ожидаемое поведение. Агент может записывать договоренности, задавать вопросы и предлагать варианты сценариев. Проверять бизнес-правила и принимать решения должны люди, которые отвечают за продукт.

Formulation становится human-AI циклом

После discovery остается структурированная заметка: rules + examples + open questions. Formulation превращает ее в Gherkin - бизнес-правила группируют сценарии, конкретные примеры становятся отдельными Scenario: или строками в Examples:-таблице у Scenario Outline. Это формальная переработка артефакта в исполняемый контракт.

Шаги цикла, когда в команде есть агент:

  1. Человек инициирует scaffolding. В spec-driven workflow это команда вроде /speckit-specify "users can sign in with their Apple ID". Скилл пишет spec.md на основе discovery-артефакта, сканирует его на capability cues («sign in» → auth), выбирает целевой путь features/auth/apple-signin.feature, вызывает scaffolder.
  2. Scaffolder создает скелет. На выходе - .feature-файл с обязательной шапкой (# Capability:, # Contributing specs:), feature-level тегами, Background:, и одним Scenario: на каждый acceptance summary из spec.md. Шаги пока шаблонные: Given [initial state], When [action], Then [expected outcome] - это явные TODO, которые нужно заполнить.
  3. Агент достраивает черновик. Имея на руках discovery-артефакт (rules + examples) и шаблон сценария, агент заполняет конкретные Given/When/Then через бизнес-язык. Здесь агент эффективен по трем причинам: формальный язык с фиксированной грамматикой, правила именования (placeholder в кавычках, lowercase бизнес-термины), переиспользуемые шаги из существующих файлов в той же капабилити (агент сканирует tests/bdd/steps/auth_steps.py и предлагает шаги, которые уже забиндены).
  4. Человек ревьюит контракт. Это core-контрольная точка фазы. Что проверяется:
    • Уровень абстракции. Шаги пишутся бизнес-языком (When the user requests a magic link), а не именами внутренних API (When messaging_dispatcher.handle returns ok). Если агент сполз в имплементационные детали - возвращается на доработку.
    • Покрытие правил. Каждое синее правило из discovery-артефакта должно быть представлено хотя бы одним сценарием. Если агент сгенерировал 5 сценариев на 7 правил - проверить, какие два пропущены и почему. Часто пропускают правила, которые сформулированы как negative requirement («система не должна X»).
    • Пограничные случаи. В сценариях нужно проверить ошибки, истекшие токены, недоступность зависимостей и одновременные запросы. Агент может помочь составить список, но его нужно сверить с рисками конкретной задачи и договоренностями discovery.
    • Теги. Каждый сценарий несет scope-тег (@smoke/@integration/@e2e), priority-тег (@p1/@p2/@p3), @spec:<id> и @user-story:<id>-<n>. Если scope или priority не очевидны из контекста - агент часто проставляет дефолт @smoke @p1; человек корректирует, ориентируясь на нагрузочный профиль (требует ли сценарий DB / Redis - @integration; полный pipeline - @e2e).
    • End-user текст verbatim. Если в продукте есть user-facing strings (bot-сообщения, email subjects, UI labels), они должны попадать в шаги дословно, в кавычках, как данные. Агент иногда переводит / перефразирует / транслитерирует их - возвращается на дословное цитирование.
    • Структура When/Then. Если агент сгенерировал длинную When → Then → When → Then цепочку - проверить, действительно ли смысл сценария в этой последовательности (как у Liz Keogh с двумя акторами), или это императивный UI-скрипт, замаскированный под BDD-сценарий, и его надо разбить на несколько отдельных Scenario:.
  5. Итерация. Если ревью нашел проблемы, человек указывает агенту конкретные правки: «у Scenario 3 шаг 2 - на уровне реализации, переформулируй»; «добавь сценарий для concurrent edit (rule 4)»; «помечай @e2e вместо @smoke, требуется DB и Redis». Агент применяет, человек ревьюит снова. На capability средней сложности 1-3 итерации обычно достаточно.
  6. Финал: ## Feature files touched. После approve скилл дописывает в spec.md секцию со ссылкой на features/auth/apple-signin.feature и закрывает фазу formulation. Спек переходит на /speckit-plan с прогоном validate_drift --spec NNN.

Что отличает этот цикл от прямой генерации Gherkin агентом без human-контрольная точка - именно review-стадия. Discovery-артефакт - источник правды, против которого ревьюер сверяет агентский draft. Без него ревьюер сверяет «звучит ли разумно», и LLM, оптимизированный под звучать разумно, проходит проверку с любым контентом.

Automation - территория агента

Step bindings - это Python-функции с декораторами @given / @when / @then. Они работают как обычный pytest-код: используют существующие service-фикстуры (db_session, identity_app, recorded_llm), вызывают production-код, проверяют результаты.

Чтобы заполнять bindings, нужно знать, какие шаги уже забиндены, а какие появились в новом сценарии и требуют новой функции. Эта информация извлекается двумя способами:

  1. Pytest collection. pytest --collect-only для BDD-сьюта проходит все .feature-файлы и пытается зарезолвить каждый шаг в существующий binding. Когда binding не найден, pytest-bdd бросает StepDefinitionNotFoundError с указанием конкретного шага и файла. Это динамический механизм - он отвечает на вопрос «что не забиндено прямо сейчас» точно, без эвристики.
  2. Static AST scan. Скрипт обходит tests/bdd/steps/*.py, собирает все вызовы @given(...) / @when(...) / @then(...) через AST-парсер, извлекает паттерны декораторов (plain string или parsers.parse argument). На выходе - список «вот все pattern-ы, к которым уже привязан код, и в каком файле / функции». Это статический механизм - он отвечает на вопрос «что у нас в принципе есть» без запуска тестов.

Комбинация этих двух механизмов дает полную картину: список бинденных шагов (AST-скан) минус шаги в новом сценарии (collection failure) = шаги, которые надо написать.

На основе этой картины spec-driven скилл (например, /speckit-tasks) формирует задачи. Для каждого нового сценария он эмитит одну [BDD] wiring-task, в которой каждый шаг помечен либо «reuse» (если найден в AST-скане - с указанием конкретного файла и функции), либо «NEW» (если не найден):

- [ ] T010 [US1] [BDD] Wire scenario "User signs in with valid Apple ID"
  in features/auth/apple-signin.feature (@spec:017 @user-story:017-1).
  Steps to bind:
  - Given an Apple ID with email "{email}" → NEW: add to tests/bdd/steps/auth_steps.py
  - And a clean tenant → reuse tests/bdd/steps/tenant_steps.py:given_clean_tenant
  - When the user clicks "Sign in with Apple" → NEW: add to tests/bdd/steps/auth_steps.py
  - Then a JWT is issued with the user's email claim → NEW: add to tests/bdd/steps/auth_steps.py
  Acceptance: pytest tests/bdd/features/test_auth.py -k "User signs in with valid Apple ID" is green.

Этот формат - готовый промт для агента. Reuse-шаги дают сигнатуру существующего binding-а, который агент должен вызывать (или подсмотреть в нем паттерн). NEW-шаги дают конкретный файл, в который надо добавить новую функцию. Агент пишет код, прогоняет pytest -k "...", видит зеленый сценарий - закрывает task. Discovery остался за человеком, formulation прошел human review, automation идет по детерминированному pipeline.

Drift как корневая проблема, которую BDD решает заранее

Один из дизайн-выборов BDD: вынести спецификацию и проверку в один файл вместо нескольких артефактов в разных системах. Это инженерное решение - не drama про «дрейфующие источники правды», а просто способ построить процесс так, чтобы дрейф между PRD, тестами и кодом не возникал по построению. Если acceptance criteria и acceptance test - один файл, между ними нет промежуточного шага «синхронизировать», на котором все ломается.

Это дает три полезных свойства:

  1. Stakeholder читает контракт без перевода. PM, спрашивающий «что продукт делает сегодня», открывает features/<capability>/<name>.feature и читает прозу. Не нужно ходить в репозиторий за тестами и не нужно звать инженера для перевода.
  2. Спецификация и тест меняются одним PR. Когда поведение меняется, меняется один файл. Сценарий не становится «историческим документом» - он лежит там, где идут изменения.
  3. Acceptance scenarios не дублируются. Не нужно писать отдельный набор Python-тестов, которые перекодируют acceptance scenarios из spec.md в test-language - сценарий уже исполняемый.

В работе с агентом это особенно полезно. Агент работает в цикле «прочитал контекст - сгенерировал patch - запустил тесты». Если контекст и тесты - один файл, цикл замкнут: «зеленые тесты» и «контракт выполняется» означают одно и то же. Если контекст лежит в Confluence, а тесты в репозитории, цикл размыкается - агент будет генерировать patch, который проходит тесты, но не обязательно соответствует контракту в Confluence, и не заметит этого.

Insight: один артефакт, два потребителя

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

«Физически одним» - не метафора. Один файл features/auth/closed-registration.feature лежит в репозитории. PM открывает его на GitHub и читает прозу. CI запускает pytest, который через pytest-bdd парсит тот же файл и исполняет шаги. PR-ревьюер видит diff на тот же файл, когда меняется поведение. Других мест нет.

Это накладывает ограничения на авторов:

Что это меняет для агента:

Это не отменяет человеческое ревью - discovery остается за людьми, как обсуждалось выше. Но все, что между discovery и production-кодом, сводится в один файл.

Опорная архитектура BDD-стека

На уровне дерева репозитория Python-проекта типичный BDD-стек выглядит так:

features/                     # current-state контракты, top-level
├── auth/
│   ├── magic-link.feature
│   ├── webauthn.feature
│   ├── jwt-issuance.feature
│   └── closed-registration.feature
├── agents/
│   ├── agent-loop.feature
│   └── agent-tuning.feature
├── tools/
│   ├── doc-spec-matching.feature
│   ├── csv-export.feature
│   └── ...
├── platform/
│   ├── state-engine.feature
│   └── ...
└── ...

tests/bdd/                    # step bindings + execution glue
├── conftest.py               # shared fixtures, tag → marker, V-TAG enforcement
├── steps/
│   ├── world.py              # World dataclass + per-scenario fixture
│   ├── tenant_steps.py
│   ├── agent_steps.py
│   ├── fsm_steps.py
│   ├── tool_steps.py
│   ├── channel_steps.py
│   ├── llm_steps.py
│   └── bdd_meta_steps.py
└── features/
    ├── conftest.py
    ├── test_auth.py          # 5 строк: scenarios("auth")
    ├── test_agents.py
    └── ...

tools/bdd/                    # production-grade тулинг
├── parser.py                 # Gherkin AST → typed dataclasses
├── render.py                 # walks features/ → manifest.json + markdown
├── scaffold.py               # creates / amends .feature files
└── validate_drift.py         # three-direction drift validator

specs/                        # decision history (immutable once shipped)
├── 001-initial-feature/
│   ├── spec.md               # contains "Feature files touched" section
│   ├── plan.md
│   └── tasks.md
└── ...

В этом примере features/ физически не лежит под specs/. Это два разных дерева с разной семантикой:

Дерево Роль Изменчивость Аудитория
specs/ Decision history - почему изменение сделано, когда, что решено Append-mostly; spec-и не меняют после shipping Инженеры, читающие историю проекта
features/ Actual current state - что система делает сегодня, по capability Меняется в том же PR, что и поведение PM, QA, инженеры

Жесткое правило (V-LOCATION): .feature-файл не имеет права лежать где-либо под specs/. Связь двусторонняя plain-text:

Развязка важна, потому что capability живет дольше любого отдельного спека. Спек 008 «закрытая регистрация» добавил сценарии в features/auth/closed-registration.feature. Спек 014 через год добавит в тот же файл сценарии про rate-limiting. Capability auth - константа. Спеки - timeline.

Для агента это означает: когда задача приходит в контексте «исправь баг в waitlist», агент идет в features/auth/closed-registration.feature, а не пытается восстановить актуальное поведение по chronological order спеков 008-014.

Что писать в .feature: канонический Gherkin + операционные расширения

Cucumber-документация определяет минимальный синтаксис: Feature (заголовок документа), Scenario (один тест-кейс), Given (исходное состояние), When (действие или событие), Then (наблюдаемое последствие), And / But (продолжение цепочки), Background (общий контекст для всех сценариев в файле), Scenario Outline + Examples (один сценарий, прогнанный по таблице данных), теги (@-префикс для метаданных).

Минимальный пример:

Feature: Magic-link sign-in

  Background:
    Given a clean tenant for this scenario

  @smoke @p1
  Scenario: User signs in with a fresh magic link
    Given a registered user "alice@example.com"
    When the user requests a magic link
    Then the user receives an email with a one-time link
    When the user opens the link within 15 minutes
    Then the user is signed in
     And a JWT is issued with the user's email claim

Поверх канонического Gherkin полезно зафиксировать несколько операционных правил, которые превращают .feature-файл из теста в контракт.

Капабилити-первая организация, не сервис-первая

Капабилити - то, как продукт-менеджер называет область продукта, а не то, как инженеры разбили код на сервисы. auth/ (капабилити) грубо соответствует services/identity/ (сервис), но это не одно и то же: invariants под features/auth/ описывают user-facing поведение, не внутреннюю архитектуру. Если завтра identity-сервис разрежут на два, features/auth/ не двинется.

Заголовок файла - часть контракта

Полезный паттерн - обязательная шапка файла:

# Capability: auth
# Contributing specs:
#   - 008-close-registration            (introduces the kill-switch)
#   - reference/auth-flows              (cross-cutting auth invariants)
# Living docs: docs/bdd/per-file/auth-closed-registration.md

@capability:auth @spec:008 @spec:reference-auth-flows
Feature: Closed registration (waitlist mode)
  ...

# Capability: и # Contributing specs: - не комментарии, а данные для drift-валидатора. Их формат проверяется парсером.

Whitelist тегов

Каждый сценарий обязан нести минимум: scope-тег (@smoke / @integration / @e2e), priority-тег (@p1 / @p2 / @p3), хотя бы один @spec:<id>, и @user-story:<NNN>-<N>, если контрибутирующий спек имеет user stories. Whitelist:

Тег Форма Назначение
@smoke bare Быстрый PR-чек (default)
@integration bare Нужны DB / Redis
@e2e bare Полный pipeline
@slow bare Исключается из PR fast path
@flaky, @known-regression bare Требуют # tracking: коммента
@p1, @p2, @p3 bare Приоритет
@capability:<slug> keyed Capability-маркер на уровне Feature
@spec:<id> keyed Контрибутирующий спек, валидируется
@user-story:<NNN>-<N> keyed Линк на User Story из spec.md
@owner:<team> keyed Информационный, фильтр в living-doc

Неизвестный тег ломает pytest-collection. Это ловит опечатки (@intgration) до того, как тест запустится.

Механика «той стороны» Gherkin - как шаги превращаются в исполняемые тесты (декораторы и parsers, target_fixture, World fixture, теги-маркеры, async, doc strings и data tables), трассируемость scenario → step → код и Ruby-эквиваленты (Cucumber, Turnip) - вынесена в отдельный reference: Gherkin и pytest-bdd: как .feature превращается в исполняемые тесты.

Three-direction drift validator как операционный gate

Часть инфраструктуры, который превращает features/ в живой артефакт - валидатор дрейфа. Он смотрит в три направления и срабатывает в трех местах: pre-commit hook, CI, автономный PR-reviewer-агент.

Direction A: spec → features

Когда spec.md меняется (добавили User Story, переписали Acceptance Scenarios), но листинг feature-файлов не меняется - спек описывает поведение, которое контрактом не зафиксировано.

Правило Что ловит
V-SPEC-LIST В spec.md нет секции ## Feature files touched
V-SPEC-LIST-EXISTS Путь, перечисленный в этой секции, не существует
V-USER-STORY ### User Story N в спеке без матчащего @user-story:<NNN>-<N> тега ни в одном feature-файле

Direction B: features → spec

Когда .feature-файл указывает на несуществующий спек или вовсе не имеет контрибутирующих спеков.

Правило Что ловит
V-LINK # Contributing specs: ссылается на id, которого нет в specs/
V-ORPHAN .feature без контрибутирующих спеков (warning)

Direction C: code → features

Когда production-код меняет externally-observable поведение, но ни один feature-файл не тронут.

Правило Что ловит
V-CODE-DRIFT PR трогает behaviour-affecting пути под services/*/src/, libs/*/src/, apps/*/src/ без изменения .feature-файлов и без лейбла bdd-skip:no-behaviour-change

Direction C обычно делегируют автономному PR-reviewer-агенту, который читает diff: программная классификация «behaviour-affecting vs refactor» нетривиальна, а агент с доступом к diff справляется. Reviewer пишет в комментарий PR-а структурированный отчет:

## BDD Spec Coverage

- ✅ No .feature files under specs/ (location rule honoured)
- ✅ specs/017-apple-signin/spec.md lists "Feature files touched"
- ✅ features/auth/apple-signin.feature has @spec:017 in its header
- ✅ Scenario "User signs in with valid Apple ID" tagged @smoke @p1 @spec:017 @user-story:017-1
- ⚠️ Direction C: production code change without features/ update
  - Modified: services/orchestration/src/.../doc_matcher.py
  - No .feature change in the PR
  - Resolve: add a scenario, or apply bdd-skip:no-behaviour-change
- ✅ Coverage gate at 100%

Verdict: REQUEST CHANGES

Кросс-катящие правила

Эти правила срабатывают независимо от направления drift-а - они проверяют целостность самого формата и метаданных, а не связи между деревьями.

V-LOCATION - .feature-файл где-либо под specs/. Это hard-rule из дизайна стека: spec-и - decision history, features - current state, и они не должны смешиваться. Если кто-то по ошибке создал specs/008-close-registration/features/closed-registration.feature, валидатор это ловит и блокирует merge. Без этого правила при росте проекта неизбежно появятся два дерева feature-файлов, между которыми поедет drift.

V-PII - реальный email, телефон или bot-token в шагах сценария. .feature-файлы лежат в публичном (или хотя бы общедоступном для всей компании) репозитории; PII в них - regulatory-проблема (GDPR / 152-ФЗ / CCPA). Реализация - regex-сканирование шагов на паттерны вроде \d{10,15} для телефонов, [A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,} для email-ов кроме whitelisted доменов (@example.com, @test.local), \d{6,}:[A-Za-z0-9_-]{30,} для Telegram bot-token-ов. Cost фалшивого срабатывания - переименовать значение в плейсхолдер; cost пропуска - regulatory finding.

V-TAG - тег не в проектном whitelist. Whitelist живет в одном месте (например, tests/bdd/conftest.py + tools/bdd/validate_drift.py, оба должны быть синхронизированы) и фиксирует допустимые scope, priority, и keyed-теги. Это ловит опечатки (@intgration вместо @integration) и неавторизованные нововведения (один разработчик решил завести @critical - валидатор просит сначала добавить в whitelist).

V-SPEC-TAG - @spec:<id> не резолвится в реальный spec dir. Тег @spec:008 предполагает, что в specs/ есть директория 008-*. Если ее нет (опечатка, спек был удален, или ссылка на еще-не-созданный спек) - валидатор ругается. Это симметрично к V-LINK в направлении B (Direction B ловит ссылки в шапке файла, V-SPEC-TAG - в feature-level и scenario-level тегах).

V-LANG - # language: <X> с X != "en". Если команда выбрала английские Gherkin keywords как стандарт (см. антипаттерны), это правило enforce-ит выбор валидатором. Конкретный X может быть ru, de, fr - V-LANG срабатывает на любой не-en. Команды, выбравшие русский Gherkin, инвертируют правило.

PARSE-ERROR - .feature не парсится Gherkin-парсером. Это catch-all для случаев, когда файл синтаксически сломан (отсутствует Feature:, кривое отступление в Scenario Outline, незакрытая doc string). Срабатывает раньше всех остальных правил - если файл не парсится, проверять его содержимое бессмысленно.

Где это срабатывает

Pre-commit hook лучше делать advisory: инженер видит дрейф до push, но не оказывается заблокирован настолько, чтобы хотеть выключить hooks вообще. CI блокирует - exit code 1 на любой error-level finding. PR-reviewer-агент пишет вердикт в PR-комментарий.

Для агента это означает три независимых места, где он не пропустит обновление контракта: pre-commit покажет ему diff drift до push; CI скажет ему то же самое после push; PR-reviewer сделает финальную проверку с человеком в петле.

End-to-end жизненный цикл с агентом

Сшивание выглядит так на примере новой фичи «sign in with Apple ID» в spec-driven workflow.

Шаг 1: /speckit-specify

/speckit-specify "users can sign in with their Apple ID". Скилл пишет specs/017-apple-signin/spec.md с user stories, scope, requirements. Затем сканирует описание на capability cues (cue «sign in» → auth), предлагает целевой путь features/auth/apple-signin.feature, вызывает scaffolder. Scaffolder создает файл с шапкой, feature-level тегами, Background и одним Scenario: на каждый acceptance summary, отмеченным @spec:017 @user-story:017-N @smoke @p1. После этого скилл дописывает в spec.md секцию ## Feature files touched со ссылкой на новый файл.

Шаг 2: /speckit-plan

Constitution Check запускает validate_drift --spec 017. Проверяет:

Если чисто - план READY. Если нет - DRAFT, и инженер (или агент) возвращается в /speckit-specify.

Шаг 3: /speckit-tasks

Генерирует список задач. Для BDD - одна creation-task на новый файл и одна [BDD] wiring-task на каждый acceptance scenario, со статическим AST-сканом существующих tests/bdd/steps/ для подсказок реюза:

- [ ] T010 [US1] [BDD] Wire scenario "User signs in with valid Apple ID"
  in features/auth/apple-signin.feature (@spec:017 @user-story:017-1).
  Steps to bind:
  - Given an Apple ID with email "{email}" → NEW: add to tests/bdd/steps/auth_steps.py
  - When the user clicks "Sign in with Apple" → NEW: add to tests/bdd/steps/auth_steps.py
  - Then a JWT is issued with the user's email claim → NEW: add to tests/bdd/steps/auth_steps.py
  Acceptance: pytest tests/bdd/features/test_auth.py -k "User signs in with valid Apple ID" is green.

Шаг 4: имплементация

Production-код под services/identity/src/.../auth/apple_oauth.py. Step bindings под tests/bdd/steps/auth_steps.py:

from pytest_bdd import given, when, then, parsers

@given(parsers.parse('an Apple ID with email "{email}"'),
       target_fixture="apple_id")
def given_apple_id(email: str) -> AppleID:
    return AppleID(email=email)

@when('the user clicks "Sign in with Apple"', target_fixture="auth_response")
async def when_apple_signin_clicked(apple_id, identity_app):
    return await identity_app.post(
        "/auth/apple/init", json={"email": apple_id.email}
    )

@then("a JWT is issued with the user's email claim")
def then_jwt_with_email(auth_response, apple_id):
    claims = decode_jwt(auth_response.json()["access_token"])
    assert claims["email"] == apple_id.email

Эту работу часто делает агент - код прозрачный, паттерны повторяются, fixtures (identity_app, db_session) уже описаны в tests/bdd/conftest.py.

Шаг 5: /speckit-implement

Completion gate запускает:

Если все зеленое - спек закрыт. Если нет - gate репортит green / red / pending breakdown и точку отказа.

Шаг 6: PR

CI прогоняет полный pytest + validate_drift. Автономный PR-reviewer-агент работает в isolated context (worktree), читает testing-rules, конституцию, контракты BDD-слоя, выдает «BDD Spec Coverage» секцию с severity matrix. Reviewer не пишет код - только репортит. Решение принимает человек.

Каждый шаг - программный gate с конкретным выходом: либо пайплайн зеленый и контракт в целости, либо красный с конкретной точкой отказа.

Скип-паттерн как backfill-стратегия

Распространенная прагматичная ситуация в момент внедрения BDD-слоя. Чтобы выполнить функциональное требование «every scenario is executable», step bindings нужны для всех сценариев в момент шиппинга. Реальные bindings требуют integration-фикстур (db_session, identity_app, recorded_llm), которые на этом этапе еще не подключены к conftest. Поставлять пустые fixtures ради скорости - значит получать красные тесты, которые никто не починит.

Решение - универсальный single-Given skip-pattern:

@spec:008 @user-story:008-3 @integration @p1
Scenario: New email signup attempt returns 503 with a registration_closed error
  Given the new-email-blocked assertions are deferred to Phase 5 auth-domain follow-up

Один Given, который матчится универсальным binding-ом в bdd_meta_steps.py:

@given(parsers.parse("the {workflow} assertions are deferred to {target}"))
def given_deferred(workflow: str, target: str) -> None:
    pytest.skip(
        f"workflow-level scenario for '{workflow}' is verified manually "
        f"per quickstart.md; programmatic binding lands during {target}."
    )

При прогоне сценарий рапортуется как skipped с сообщением, в котором есть phase pointer. Pytest-bdd дальше шагов не идет.

Что это удовлетворяет:

Без принуждения иметь:

Trade-off: скипнутые сценарии не проверяют поведение. Они фиксируют contract surface - что система обещает делать - и показывают, где должны появиться реальные bindings. По мере того, как domain owners пишут реальный код, сценарии переходят из skipped в green.

В контексте AI-агентов это особенно полезно: агент часто формулирует контракт быстрее, чем команда успевает завести integration-фикстуры. Skip-pattern закрывает разрыв: контракт зафиксирован, тесты зеленые (как skip), реальные bindings ставятся отдельным PR-ом - желательно тем же агентом, по указателю на phase pointer в сообщении skip-а.

Что это дает агенту в качестве context

Если перечислить, что меняется в работе агента, когда features/-дерево становится первоклассным контрактом:

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

Антипаттерны и ограничения

  1. Gherkin не везде уместен. Low-level invariants - индексы БД, internal API contracts, garbage collection - в .feature-файле выглядят инородно. Их естественное место - обычные интеграционные или unit-тесты. BDD-слой - additive, не replacement: existing pytest-suites остаются на своем месте, features/ добавляется сверху как acceptance-контракт.
  2. Happy-path-only сценарии бесполезны. Без edge cases, failure modes, unhappy paths контракт описывает идеальный мир, в котором баги не случаются. Cucumber-документация по example mapping подчеркивает это: зеленые карточки - это конкретные примеры, включая граничные. Полезное правило: для каждого acceptance criteria - хотя бы один happy и один edge сценарий, и явные @flaky / @known-regression теги для зон, где система ведет себя неустойчиво.
  3. Авторинг сценариев без human review. Formulation - часть discovery practice, она требует stakeholder-знания, которого у агента нет. Даже отлично написанный Gherkin не означает правильной модели предметной области. Рабочий pipeline: агент скаффолдит первый драфт, человек проходится перед /speckit-plan, dispute случаются на этом этапе, не на ревью PR-а.
  4. «У нас есть DDD - этого хватит». Bounded context auth-сервиса содержит регистрацию, login, password reset, MFA, SSO, session management - это разные user-facing capability с разным lifecycle, разными pain points, разными owner-ами. DDD структурирует код, но не functional surface. Подробнее это разобрано в статье «Функциональность как first-class artifact».
  5. Russian Gherkin keywords. Cucumber поддерживает # language: ru и весь набор русских ключевых слов (Функция, Сценарий, Допустим, Когда, Тогда). Для проекта с русскоязычной командой это выглядит соблазнительно, но лучше выбирать один стиль и применять его последовательно. Английские keywords сочетаются с большинством IDE-плагинов, GitHub-подсветки и tooling-cookbooks без дополнительных настроек; русский end-user текст и так попадает дословно в step arguments как данные.
  6. Перенос всего PRD в Gherkin. Gherkin - формат для acceptance criteria, не для market analysis, business cases, ARC-design rationale. Spec-документ остается (это decision history), .feature - его исполняемая часть. Если PM пишет «фича позволит увеличить retention на 15% по гипотезе X» - это не Given/When/Then, это motivation в spec.md.
  7. Drift validator без owner. Правила уровня error без человека, который реагирует на тревоги, превращаются в noise. V-PII или V-LOCATION блокирует merge - кто-то должен быть готов это разрулить. V-ORPHAN - warning, который кто-то периодически разгребает. Без этой ответственности валидатор отключают через две недели.

Минимальная рабочая система

Чтобы попробовать BDD-as-default в проекте с AI-агентами, не обязательно строить весь стек сразу. Минимальная конфигурация.

1. Один capability, не все сразу

Выбрать одну critical capability с понятным actor и outcome - типа onboarding, billing-checkout, primary search. Не размазываться на все области.

2. Top-level features/<capability>/

Не под спеками, не под тестами - отдельное дерево. Один файл на slice (например, features/auth/magic-link.feature).

3. pytest-bdd как execution layer

Установить pytest-bdd, в pyproject.toml положить:

[tool.pytest.ini_options]
asyncio_mode = "auto"
bdd_features_base_dir = "features"

4. Один conftest для tag whitelist

В tests/bdd/conftest.py - pytest_collection_modifyitems, который ловит unknown теги:

_BARE_TAG_WHITELIST = frozenset({
    "smoke", "integration", "e2e", "p1", "p2", "p3",
})
_KEYED_TAG_WHITELIST = frozenset({"capability", "spec", "user-story"})

def pytest_collection_modifyitems(config, items):
    unknown = []
    for item in items:
        for marker in list(item.iter_markers()):
            name = marker.name
            if ":" in name:
                if name.split(":", 1)[0] in _KEYED_TAG_WHITELIST:
                    continue
            elif name in _BARE_TAG_WHITELIST:
                continue
            elif name in {"asyncio", "parametrize", "skip", "skipif", "xfail"}:
                continue
            unknown.append((item.nodeid, name))
    if unknown:
        formatted = "\n".join(f"  - {nid}: @{tag}" for nid, tag in unknown)
        raise pytest.UsageError(f"Unknown tags:\n{formatted}")

5. Per-capability glue в 5 строк

# tests/bdd/features/test_auth.py
from pytest_bdd import scenarios
from tests.bdd.steps.world import world  # noqa: F401
from tests.bdd.steps.auth_steps import *  # noqa: F403

scenarios("auth")

6. World-фикстура для per-scenario state

# tests/bdd/steps/world.py
from dataclasses import dataclass, field
from typing import Any
import pytest

@dataclass(slots=True)
class World:
    last_response: Any = None
    extra: dict[str, Any] = field(default_factory=dict)

@pytest.fixture
def world() -> World:
    return World()

7. Простейший drift-чек как pre-commit advisory

Скрипт, который:

Hard gate - в CI: тот же скрипт, но exit 1 при ошибках.

8. Опционально: интеграция со spec-driven workflow

Если используется spec-kit, GitHub Spec Kit, OpenSpec или собственный аналог - добавить вызов scaffold + добавление ## Feature files touched секции в spec-документ. Без этого агент будет писать .feature-файлы вручную, что работает, но не имеет автоматического gate-а.

Что не нужно на старте: HTML living-doc renderer, full three-direction validator, кастомный .pre-commit-config.yaml, кастомный PR-reviewer-агент. Это все имеет смысл, когда capabilities дойдет до 5-10. Через месяц решать: используется ли .feature-дерево как input context для агента (PM ссылается на сценарии, агент в /specify-промтах ставит линки на features/..., тесты падают на каждом дрейфе) - расширять. Если файлы лежат и никто их не открывает - формат не подошел, проблема в process, а не в инфраструктуре.

Источники

Theory and practice

Industry artifacts

Смежные статьи на сайте