Перейти к содержимому
Программа SDDSDD.2 Как писать спецификации

Требования и критерии приёмки

Из чего состоит функциональная спецификация — контекст, пользовательские истории, требования с идентификаторами, сценарии «Дано / Когда / Тогда», нефункциональные требования, границы и открытые вопросы; шаблоны EARS, признаки плохого требования, маркеры неясности и цикл уточнений с агентом — на примере записи на потоки онлайн-школы.

УРОКsdd/sdd-2-specs/01-requirements.mdx2 чУрок 1 из 3
После урока вы сможете
  • Составить спецификацию функции по шаблону курса — контекст, истории, требования REQ, сценарии SC, «вне рамок», открытые вопросы
  • Формулировать требования по шаблонам EARS, а критерии приёмки — как сценарии «Дано / Когда / Тогда» с конкретными данными и границами
  • Находить плохие требования — размытые, составные, «решение вместо проблемы», непроверяемые — и переписывать их
  • Вести цикл уточнений с агентом через маркеры [УТОЧНИТЬ] и проверять спецификацию собственным линтером

В уроке SDD.1.2 мы видели: чего нет в тексте, то агент додумает. Самая важная часть спецификации — требования и критерии приёмки: первые агент реализует, по вторым проверяется результат. Плохое требование агент выполнит «как-нибудь», а без критерия приёмки вы не узнаете, как именно.

Требования писали задолго до ИИ, и у этого ремесла есть проверенные приёмы: идентификаторы, шаблоны фраз, сценарии с конкретными данными, стоп-список размытых слов. В этом уроке мы соберём их в формат, которым будем пользоваться всю программу, разберём признаки плохих требований, научимся вести цикл уточнений с агентом и напишем линтер, который сам находит проблемы в спецификации. Пример — запись на потоки онлайн-школы, правила которой вы знаете по уроку DDD.1.2.

Из чего состоит функциональная спецификация

Заголовок раздела «Из чего состоит функциональная спецификация»

Функциональная спецификация отвечает на вопросы «что» и «зачем» и ничего не говорит о «как». OpenSpec формулирует это так: спецификация — контракт поведения, а не план реализации; если реализацию можно поменять, не меняя видимого снаружи поведения, этой детали в спецификации не место.

В курсе у файла spec.md шесть разделов. Тот же формат проверяет домашняя работа sdd-hw01-spec, которую выдаёт урок SDD.2.3:

РазделЧто в нёмПример для записи на поток
Контекстцель, пользователи, ссылка на глоссарий, важные определениястудент и менеджер набора; «действующая запись» — со статусом «ожидает оплаты» или «подтверждена», только она занимает место
Пользовательские историикто и зачем, с приоритетом«Как студент, я хочу записаться на поток, чтобы занять место до старта»
Требованияпроверяемые утверждения с ID REQ-001: функциональные и нефункциональные«Если студент записывается в момент старта потока или позже, то система должна отказать…»
Сценариикритерии приёмки «Дано / Когда / Тогда» с ID SC-001 и ссылкой на требованияГлеб записывается 2 ноября в 10:00 — отказ EnrollmentClosed
Вне рамокчто сознательно не делаемоплата, лист ожидания, уведомления
Открытые вопросынеясности с маркером [УТОЧНИТЬ] и принятые решения со ссылкойпоток перенесли на другую дату — что с правилом 7 суток?
flowchart LR
G["Цель<br/>и пользователи"] --> U["Истории<br/>US-1, US-2"]
U --> R["Требования<br/>REQ-001…"]
R --> S["Сценарии<br/>SC-001…"]
S --> T["Приёмочные тесты<br/>test_sc_001_…"]
T --> C["Код"]
R -.->|"задачи ссылаются<br/>на требования"| K["Задачи<br/>T-001…"]
K -.-> C
Цепочка трассируемости: от цели к требованиям, от требований к сценариям и тестам. Каждое звено ссылается на предыдущее по ID

Идентификаторы — не формальность. По ним сценарий ссылается на требование, задача — на требования, тест — на сценарий (test_sc_005_...), а вы — разговариваете с агентом: «REQ-004 нарушен, перечитай его». Эта цепочка называется трассируемостью; к ней мы вернёмся в уроках о задачах и о проверке.

Контекст — несколько предложений о цели и пользователях плюс определения, без которых требования двусмысленны. Слова — из глоссария домена; определение «действующей записи» лучше дать один раз здесь, чем повторять в каждом требовании.

Пользовательская историяпользовательская историяEN user storyКороткое описание потребности от лица пользователя: «Как <роль>, я хочу <действие>, чтобы <ценность>». Отвечает на вопросы «кто» и «зачем»; детали — в требованиях и сценариях. отвечает на вопросы «кто» и «зачем» в формате «Как роль, я хочу действие, чтобы ценность»:

  • US-1 (P1): Как студент, я хочу записаться на поток, чтобы занять место до старта.
  • US-2 (P1): Как менеджер набора, я хочу, чтобы неоплаченные записи освобождали места, чтобы поток заполнялся теми, кто готов платить.
  • US-3 (P2): Как студент, я хочу отменить запись и понимать, сколько денег мне вернут.

Приоритеты P1, P2 — приём из шаблона Spec Kit: каждая история должна давать ценность сама по себе, и агент может реализовывать их по очереди. Историю не проверяют напрямую — проверяют требования и сценарии, которые из неё выросли.

Одно требование — одно проверяемое обязательство

Заголовок раздела «Одно требование — одно проверяемое обязательство»

ТребованиетребованиеEN requirementОдно проверяемое утверждение о том, что система должна делать (функциональное) или каким свойством обладать (нефункциональное). В спецификации у требования есть идентификатор, например REQ-001. — одно проверяемое утверждение о поведении системы. В курсе требование записывается одной строкой списка в разделе «Требования», с ID и словом «должна»:

## Требования
### Функциональные
- **REQ-001**: Когда студент записывается на поток со свободными местами до старта потока, система должна создать запись на поток со статусом «ожидает оплаты».
- **REQ-002**: Если число действующих записей равно вместимости потока, то система должна отказать в записи с ошибкой NoFreeSeats.
### Нефункциональные
- **REQ-009**: Система должна хранить суммы денег целым числом копеек вместе с валютой.

Слово «должна» — русский аналог MUST и SHALL из RFC 2119, на который опираются OpenSpec («The system SHALL…») и Spec Kit («System MUST…»). «Следует» (SHOULD) и «может» (MAY) в требованиях лучше не использовать: «система следует отправить письмо» нельзя ни выполнить наполовину, ни проверить.

В 2009 году Алистер Мавин и соавторы из Rolls-Royce опубликовали на конференции по инженерии требований (IEEE RE’09) статью «Easy Approach to Requirements Syntax» — EARSEARS (шаблоны формулировок требований)EN Easy Approach to Requirements SyntaxНесколько шаблонов предложения для требования: «Система должна …», «Когда <событие>, система должна …», «Пока <состояние>, …», «Если <нежелательное событие>, то …». Предложены Алистером Мавином и соавторами (Rolls-Royce, 2009).. Они разбирали требования к системам управления авиадвигателями и заметили, что почти любое хорошее требование укладывается в несколько шаблонов предложения: условие или триггер, затем система, затем её реакция. Шаблон заставляет назвать, когда правило срабатывает, — то есть ровно то, что агент иначе додумает.

ШаблонКлючевое словоФормулировкаПример для школы
Повсеместное—Система должна …Система должна хранить суммы денег целым числом копеек вместе с валютой.
По событиюWhenКогда событие, система должна …Когда студент отменяет запись за 7 суток или более до старта потока, система должна вернуть ему полную стоимость.
По состояниюWhileПока состояние, система должна …Пока студенту из листа ожидания предложено место, система должна держать это место закреплённым за ним.
Нежелательное поведениеIf … thenЕсли условие, то система должна …Если свободных мест на потоке нет, то система должна отказать в записи с ошибкой NoFreeSeats.
Опциональная функцияWhereПри наличии функции система должна …При включённом листе ожидания система должна предлагать освободившееся место первому студенту в листе ожидания.
КомплексноесочетаниеПока …, когда …, система должна …Пока поток не стартовал, когда запись на поток отменяется, система должна опубликовать событие МестоОсвободилось (SeatReleased).

EARS используют и в инструментах SDD. Например, в Kiro, по описанию в его документации, требования спецификации записываются как истории с критериями приёмки в нотации EARS («WHEN … THE SYSTEM SHALL …»); открытый проект cc-sdd, совместимый со спецификациями Kiro, прямо пишет в README, что его requirements.md — «требования в формате EARS с критериями приёмки». Проверяйте актуальное состояние по документации: инструменты меняются быстро.

Учтите совпадение слов: «Когда» в EARS — триггер в формулировке правила, а «Когда» в Gherkin — действие в примере. Требование — общее правило, сценарий — конкретный случай с датами и именами.

Нефункциональные требованиянефункциональное требованиеEN non-functional requirementТребование к свойствам системы, а не к её поведению: время ответа, доступность, безопасность, формат хранения данных. Записывается с измеримым порогом: не «быстро», а «95% запросов за 300 мс». описывают свойства системы: время ответа, доступность, безопасность, формат хранения данных. Главное правило — измеримый порог. Числа ниже условные:

РазмытоИзмеримо
система должна работать быстросистема должна отвечать на запрос записи на поток не дольше 500 мс для 95% запросов при нагрузке до 20 запросов в секунду
деньги должны считаться надёжносистема должна хранить суммы денег целым числом копеек вместе с валютой
запись должна быть удобнойот страницы курса до созданной записи — не больше двух действий студента

Требования к устройству кода — «время в модели только параметром now», «без новых зависимостей», «сначала тест» — это не поведение системы, а правила разработки. Им место в плане и в конституции проекта (уроки SDD.2.2 и SDD.2.3) или в файле памяти агента.

Раздел «Вне рамок»вне рамок (что не делаем)EN out of scope, non-goalsРаздел спецификации с тем, что сознательно не входит в задачу. Защищает от «полезных» дополнений агента и от споров на приёмке. кажется лишним, пока не поработаешь с агентом. Агенты любят быть полезными: попросите запись на поток — и получите заодно отправку писем, лист ожидания и скидки. Каждое такое дополнение — непроверенный код и решения о продукте, которых никто не принимал. Явный список «что не делаем» снимает это и заодно споры на приёмке. Хорошая запись говорит не только «нет», но и «где»: «письма — контекст «Уведомления»», «лист ожидания — отдельная функция».

Критерии приёмки: сценарии «Дано / Когда / Тогда»

Заголовок раздела «Критерии приёмки: сценарии «Дано / Когда / Тогда»»

Критерий приёмкикритерий приёмкиEN acceptance criterionПроверяемое условие, при котором требование или история считаются выполненными. Удобная форма — сценарий «Дано / Когда / Тогда» с конкретными данными: из него прямо пишется приёмочный тест. — проверяемое условие, при котором требование считается выполненным. Лучшая его форма — сценарий «Дано / Когда / Тогда»сценарий «Дано / Когда / Тогда»EN Given / When / Then (Gherkin)Структура примера поведения: исходное состояние (Дано), действие или событие (Когда), ожидаемый результат (Тогда). Пришла из BDD; язык Gherkin инструмента Cucumber понимает и русские ключевые слова: Сценарий, Дано, Когда, Тогда, И, Но.: исходное состояние, действие или событие, наблюдаемый результат. Форма пришла из разработка через поведениеразработка через поведение (BDD)EN Behavior-Driven DevelopmentПрактика, которую предложил Дэн Норт в середине 2000-х: описывать поведение системы примерами «Дано / Когда / Тогда» на языке бизнеса и превращать их в автоматические тесты (инструмент Cucumber, язык Gherkin). (Дэн Норт) и стала языком Gherkin инструмента Cucumber. Gherkin понимает русские ключевые слова — это видно в его словаре gherkin-languages.json:

# language: ru
Функция: Запись на поток
Сценарий: Отмена ровно за 7 суток до старта — полный возврат
Дано поток «Python для аналитиков» со стартом 2 ноября 2026 года в 10:00 и ценой 30 000 руб.
И у Анны запись на поток со статусом «подтверждена»
Когда Анна отменяет запись 26 октября 2026 года в 10:00
Тогда Анне возвращается 30 000 руб.
И запись Анны получает статус «отменена»

В спецификации курса мы пишем сценарии в markdown — так их читают и люди, и агент, и наш линтер. Каждый сценарий — заголовок ### SC-…, строка «Требования:» со ссылками и шаги списком:

## Сценарии
### SC-006: Отмена ровно за 7 суток — полный возврат
Требования: REQ-006, REQ-009
- **Дано** у Анны запись на поток со статусом «подтверждена»
- **Когда** Анна отменяет запись 26 октября в 10:00
- **Тогда** Анне возвращается 3 000 000 копеек в валюте RUB

Что отличает хороший сценарий:

  • Конкретные данные. Имена, даты, суммы, вместимость. «Студент отменяет запись заранее» — не сценарий.
  • Одно поведение. Если в сценарии два «Когда», это два сценария.
  • Границы. Для правила с числом нужен случай ровно на границе и случай сразу за ней: отмена в 10:00 (ровно 7 суток — возврат) и в 10:01 (уже нет). Именно на границе агент чаще всего пишет > вместо >=.
  • Отрицательные случаи. Мест нет, запись закрыта, повторная запись — ошибки тоже поведение.
  • Наблюдаемый результат в «Тогда». Статус, сумма, ошибка, событие — то, что можно проверить снаружи, а не «вызван метод _recalc».
  • ID в имени теста. Сценарий SC-006 станет тестом test_sc_006_full_refund_exactly_7_days; так по отчёту pytest видно, какое правило нарушено (подробно — в уроке SDD.4.2).

Шаблон Spec Kit тоже содержит сценарии Given / When / Then внутри каждой истории. Не путайте нумерацию: в Spec Kit функциональные требования — FR-001, а SC-001 — измеримые критерии успеха (Success Criteria). В нашем курсе требования — REQ-001, а SC-001 — сценарии.

ПризнакПримерЧем плохоКак исправить
размытые слова«Система должна быстро и удобно записывать студентов»проверить нельзя; агент выберет «быстро» самизмеримый порог или отдельное поведение
составное«…должна отменить запись, вернуть деньги и уведомить студента»выполнено наполовину — это выполнено? сценарий проверит только частьразбить на REQ по одному обязательству
решение вместо проблемы«Система должна хранить записи в таблице enrollments в PostgreSQL»это «как», оно связывает руки плану и не говорит, зачем«Когда запись отменяется, система должна сохранить её со статусом «отменена», не удаляя»
нет числа или границы«Неоплаченные записи должны отменяться через некоторое время»агент подставит любое «разумное» число«…48 часов с момента записи» и сценарий ровно на 48-м часу
чужие слова«Если группа заполнена, заказ отклоняется»синонимы вместо глоссария — агент назовёт классы Group и Orderслова глоссария: поток, запись на поток
запрет без поведения«Система не должна допускать ошибок при оплате»непонятно, что делать, чтобы выполнитьчто именно происходит при ошибке оплаты

Для первого признака удобен стоп-список. В курсе в нём: «быстро», «удобно», «интуитивно», «оптимально», «эффективно», «гибко», «своевременно» и «вовремя», «современный», «надёжно», а также выражения «по возможности», «при необходимости», «в разумные сроки», «как правило», «и т. д.», «и др.». Каждое из них — место, где агент примет решение за вас. Стоп-список домашней работы sdd-hw01-spec задан в её README.

Составные требования ловятся союзами «и», «или», «и/или» после слова «должна» и двумя «должна» в одной фразе. Это эвристика: «должна сохранить запись и её историю» — скорее одно обязательство. Линтер подсвечивает кандидатов, решает человек.

Самое ценное в спецификации — не ответы, а честно записанные вопросы. Если правило неизвестно, его нельзя угадывать: ставьте маркер неясностимаркер неясностиEN clarification markerПометка прямо в спецификации, что решение ещё не принято: в курсе — [УТОЧНИТЬ: вопрос], в Spec Kit — [NEEDS CLARIFICATION: …]. Запрещает агенту додумывать и собирает вопросы для эксперта. [УТОЧНИТЬ: вопрос] прямо в текст и копию — в раздел «Открытые вопросы». Это русский аналог маркера [NEEDS CLARIFICATION: …] из шаблонов Spec Kit, которые прямо запрещают модели догадываться: «если в описании чего-то нет — пометь».

Цикл уточнений выглядит так:

  1. Вы пишете черновик — или просите агента набросать его по вашему описанию и глоссарию.
  2. Агент ищет неясности и задаёт немного вопросов с вариантами ответа. В Spec Kit этим занимается команда /speckit.clarify: до пяти точечных вопросов за проход, ответы записываются обратно в spec.md.
  3. Вы или эксперт отвечаете; агент обновляет требования и сценарии и закрывает маркеры.
  4. Повторяйте, пока маркеров не останется или оставшиеся явно не вынесены за рамки задачи.

Промпт для шага 2 в любом агенте:

Прочитай specs/enrollment/spec.md и docs/glossary.md. Код не пиши и ничего не правь.
Найди места, где требование можно понять двумя способами, где нет числа или границы,
где сценарий не покрывает требование. Задай мне не больше пяти вопросов, у каждого —
2–3 варианта ответа и твоя рекомендация. Неясное не додумывай: для вопросов, на которые
я не отвечу, предложи маркер [УТОЧНИТЬ: …] и место в спецификации.

Как это выглядит на спецификации записи на поток:

Агент: 1. REQ-005: 48 часов отсчитываются от момента записи или от выставления счёта?
А) от записи (рекомендую: так в глоссарии); Б) от счёта.
2. REQ-005: ровно через 48 часов запись ещё действует или уже отменена?
А) отменена (записалась 1 октября в 10:00 — 3 октября в 10:00 отменена); Б) ещё действует.
3. Что делать с платежом, который пришёл после автоматической отмены записи?
А) вернуть деньги; Б) восстановить запись, если место свободно.
4. Что, если поток перенесли на другую дату: записи сохраняются? От какой даты считать 7 суток?
Вы: 1 — А, 2 — А. Третий решили эксперты на Event Storming (урок DDD.2.3): деньги вернуть
полностью, запись не восстанавливать; это делает контекст «Оплата», у нас — вне рамок,
запиши решение со ссылкой. Четвёртый эксперты пока не решили: поставь [УТОЧНИТЬ]
в «Открытые вопросы» и вынеси перенос потока за рамки этой функции.

Обратите внимание на третий и четвёртый ответы. Это вопросы о правилах предметной области, и ни агент, ни разработчик не вправе закрыть их догадкой. Третий уже решён с экспертами — в спецификацию попадает решение и ссылка на него. Четвёртый не решён — он остаётся открытым, а решение «пока вне рамок» тоже принято явно.

Ещё один полезный приём Spec Kit — команда /speckit.checklist, которую документация называет «юнит-тестами для требований»: чек-лист вопросов о полноте и однозначности спецификации («для каждого ли статуса описано поведение?»). Часть таких проверок механическая — их можно автоматизировать, что мы и сделаем в практике.

Соберём всё вместе. Это полная спецификация в формате курса; её проходит без замечаний линтер из практики.

# Спецификация: запись на поток
## Контекст
Контекст «Набор» онлайн-школы. Студент записывается на поток курса до его старта; запись ожидает оплаты
и после оплаты подтверждается. Пользователи: студент и менеджер набора. Термины — из docs/glossary.md.
Действующая запись — запись на поток со статусом «ожидает оплаты» или «подтверждена»; только действующие записи занимают места.
Числа (48 часов, 7 суток) — условия учебного примера.
## Пользовательские истории
- **US-1 (P1)**: Как студент, я хочу записаться на поток, чтобы занять место до старта.
- **US-2 (P1)**: Как менеджер набора, я хочу, чтобы неоплаченные записи освобождали места, чтобы поток заполнялся теми, кто готов платить.
- **US-3 (P2)**: Как студент, я хочу отменить запись и понимать, сколько денег мне вернут.
## Требования
### Функциональные
- **REQ-001**: Когда студент записывается на поток со свободными местами до старта потока, система должна создать запись на поток со статусом «ожидает оплаты».
- **REQ-002**: Если число действующих записей равно вместимости потока, то система должна отказать в записи с ошибкой NoFreeSeats.
- **REQ-003**: Если у студента уже есть действующая запись на этот поток, то система должна отказать в повторной записи с ошибкой AlreadyEnrolled.
- **REQ-004**: Если студент записывается на поток в момент старта потока или позже, то система должна отказать в записи с ошибкой EnrollmentClosed.
- **REQ-005**: Если запись на поток остаётся в статусе «ожидает оплаты» 48 часов с момента записи, то система должна перевести её в статус «отменена».
- **REQ-006**: Когда студент отменяет запись за 7 суток (7 × 24 часа) или более до старта потока, система должна вернуть ему полную стоимость.
- **REQ-007**: Когда студент отменяет запись позже, чем за 7 суток до старта потока, система должна перевести запись в статус «отменена» без возврата денег.
- **REQ-008**: Когда запись на поток переходит в статус «отменена», система должна опубликовать событие ЗаписьОтменена (EnrollmentCancelled) с причиной отмены.
### Нефункциональные
- **REQ-009**: Система должна хранить суммы денег целым числом копеек вместе с валютой.
- **REQ-010**: Система должна отвечать на запрос записи на поток не дольше 500 мс для 95% запросов при нагрузке до 20 запросов в секунду.
## Сценарии
Общие данные: поток «Python для аналитиков», старт 2 ноября 2026 года в 10:00, вместимость 2, цена 30 000 руб.
### SC-001: Свободное место есть
Требования: REQ-001
- **Дано** на потоке одна запись Бориса со статусом «подтверждена»
- **Когда** Анна записывается на поток 1 октября в 10:00
- **Тогда** у Анны появляется запись со статусом «ожидает оплаты»
### SC-002: Мест нет
Требования: REQ-002
- **Дано** у Анны запись «ожидает оплаты», у Бориса — «подтверждена»
- **Когда** Вера записывается на поток
- **Тогда** система отказывает с ошибкой NoFreeSeats
### SC-003: Повторная запись
Требования: REQ-003
- **Дано** у Анны запись на поток со статусом «ожидает оплаты»
- **Когда** Анна снова записывается на этот поток
- **Тогда** система отказывает с ошибкой AlreadyEnrolled
### SC-004: Запись в момент старта
Требования: REQ-004
- **Дано** на потоке есть свободные места
- **Когда** Глеб записывается на поток 2 ноября в 10:00
- **Тогда** система отказывает с ошибкой EnrollmentClosed
### SC-005: Ровно 48 часов без оплаты
Требования: REQ-005, REQ-008
- **Дано** Анна записалась на поток 1 октября в 10:00 и не оплатила
- **Когда** наступает 3 октября 10:00
- **Тогда** запись Анны получает статус «отменена»
- **И** публикуется событие ЗаписьОтменена (EnrollmentCancelled) с причиной «истёк срок оплаты»
### SC-006: Отмена ровно за 7 суток — полный возврат
Требования: REQ-006, REQ-009
- **Дано** у Анны запись со статусом «подтверждена»
- **Когда** Анна отменяет запись 26 октября в 10:00
- **Тогда** Анне возвращается 3 000 000 копеек в валюте RUB
### SC-007: Отмена на минуту позже — без возврата
Требования: REQ-007
- **Дано** у Анны запись со статусом «подтверждена»
- **Когда** Анна отменяет запись 26 октября в 10:01
- **Тогда** запись Анны получает статус «отменена»
- **И** возврат равен 0
### SC-008: Повторная запись после отмены
Требования: REQ-001, REQ-003
- **Дано** у Анны отменённая запись на поток, свободное место есть
- **Когда** Анна снова записывается на поток 5 октября в 10:00
- **Тогда** у Анны появляется новая запись со статусом «ожидает оплаты»
- **И** старая запись остаётся в истории со статусом «отменена»
### SC-009: Нагрузка
Требования: REQ-010
- **Дано** 1000 потоков и 20 запросов записи в секунду в течение 10 минут
- **Когда** измеряем время ответа на каждый запрос
- **Тогда** 95% ответов приходят не дольше чем за 500 мс
## Вне рамок
- Оплата и выставление счёта — контекст «Оплата».
- Лист ожидания и предложение освободившегося места — отдельная функция.
- Письма и пуши — контекст «Уведомления».
- Перенос записи на другой поток.
## Открытые вопросы
- Решено (Event Storming, урок DDD.2.3): повторная запись после отмены разрешена — запрет касается только действующих записей (REQ-003, SC-008).
- Решено (там же): платёж, пришедший после автоматической отмены, возвращается полностью, запись не восстанавливается. Реализует контекст «Оплата» — вне рамок.
- [УТОЧНИТЬ] Поток перенесли на другую дату — что с записями и правилом 7 суток? До ответа экспертов — вне рамок.
- [УТОЧНИТЬ] Возможен ли частичный возврат? До ответа действует REQ-007: позже чем за 7 суток — без возврата.

Обратите внимание на несколько деталей. Каждое требование — одна строка и одно обязательство. Границы чисел покрыты сценариями ровно на границе (SC-005, SC-006) и сразу за ней (SC-007). Определение «действующей записи» дано один раз в контексте. В «Открытых вопросах» два вида записей: решения, которые эксперты уже приняли на Event Storming (урок DDD.2.3), — со ссылкой; и две горячие точки, которые там остались без ответа, — с маркером [УТОЧНИТЬ] и явным решением «пока вне рамок». Ни один вопрос не закрыт догадкой.

Напишите линтер требований — программу, которая разбирает spec.md в формате курса и находит механические проблемы: требования без идентификатора, без сценария, с размытыми словами из стоп-списка, составные требования и ссылки сценариев на несуществующие требования. Готовы стоп-список, константы правил и функция format_report, которая печатает отчёт. Вам нужно написать четыре функции:

  • parse_spec(text) — разобрать markdown: требования (ID или None, текст, номер строки) и сценарии со ссылками;
  • find_vague(text) — найти размытые слова и выражения;
  • is_compound(text) — решить, похоже ли требование на составное;
  • lint(text) — собрать список проблем в заданном порядке.

Кнопка «Запустить» печатает отчёт по учебной спецификации EXAMPLE_SPEC. Когда тесты пройдут, должно получиться так:

Требований: 4, сценариев: 2, проблем: 8
REQ-002 размытые слова: быстро, удобно
REQ-002 составное требование
REQ-002 нет сценария
строка 7 нет идентификатора: Система должна закрывать запись на поток вовремя.
строка 7 размытые слова: вовремя
REQ-003 составное требование
REQ-003 нет сценария
SC-002 ссылка на несуществующее требование: REQ-004
Линтер требований: ID, сценарии, размытые слова, составные требования8 тестов
import re

# Стоп-список размытых слов (урок SDD.2.1). Основы слов сравниваем с началом слова:
# «быстр» ловит «быстро», «быстрый», «быстрого». Буква «ё» заменена на «е» —
# текст требования перед поиском нормализуем так же.
VAGUE_STEMS = ["быстр", "удобн", "интуитивн", "оптимальн", "эффективн", "гибк",
               "своевременн", "вовремя", "современн", "надежн"]
# Размытые выражения из нескольких слов ищем как подстроку, начиная с границы слова
VAGUE_PHRASES = ["по возможности", "при необходимости", "в разумные сроки", "как правило",
                 "и т. д.", "и т.д.", "и др."]
# Модальные слова требования: «система должна …»
MODAL_WORDS = ["должен", "должна", "должно", "должны"]

RULE_NO_ID = "нет идентификатора"
RULE_VAGUE = "размытые слова"
RULE_COMPOUND = "составное требование"
RULE_NO_SCENARIO = "нет сценария"
RULE_UNKNOWN_REF = "ссылка на несуществующее требование"

REQ_ID = re.compile(r"REQ-\d{3}")
SC_ID = re.compile(r"SC-\d{3}")

EXAMPLE_SPEC = """# Спецификация: запись на поток

## Требования

- **REQ-001**: Когда студент записывается на поток со свободными местами, система должна создать запись на поток со статусом «ожидает оплаты».
- **REQ-002**: Система должна быстро и удобно показывать студенту его записи.
- Система должна закрывать запись на поток вовремя.
- **REQ-003**: Если запись остаётся в статусе «ожидает оплаты» 48 часов с момента записи, то система должна отменить запись и/или уведомить студента.

## Сценарии

### SC-001: Свободное место есть
Требования: REQ-001
- **Дано** поток с вместимостью 2 и одной записью «подтверждена»
- **Когда** Вера записывается на поток
- **Тогда** у Веры появляется запись «ожидает оплаты»

### SC-002: Повторная запись
Требования: REQ-004
- **Дано** у Анны есть запись «ожидает оплаты» на поток
- **Когда** Анна снова записывается на этот поток
- **Тогда** система отказывает: AlreadyEnrolled
"""


def _normalize(text):
    """Нижний регистр и «е» вместо «ё»: «Надёжно» → «надежно». Длина строки не меняется."""
    return text.lower().replace("ё", "е")


def parse_spec(text):
    """Разобрать спецификацию в markdown на требования и сценарии.

    Требования — пункты списка («- …» или «* …») в разделе «## Требования» (подразделы «### …»
    внутри него тоже считаются). У пункта с идентификатором текст начинается с REQ-001
    (можно жирным: **REQ-001**), дальше двоеточие или точка и формулировка.

    Сценарии — заголовки «### SC-001: …» в разделе «## Сценарии»; ссылки на требования —
    в строке «Требования: REQ-001, REQ-002» под заголовком сценария.

    Возвращает (requirements, scenarios):
      requirements — список словарей {"id": "REQ-001" или None, "text": формулировка, "line": номер строки с 1};
      scenarios — словарь {"SC-001": ["REQ-001", ...]} в порядке появления сценариев.
    """
    # TODO: ваш код
    ...


def find_vague(text):
    """Размытые слова и выражения из стоп-списка в тексте требования.

    Текст нормализуем: нижний регистр, «ё» → «е». Выражения из VAGUE_PHRASES ищем как подстроку,
    перед которой нет буквы; слово считается размытым, если начинается с основы из VAGUE_STEMS.
    Возвращает найденные фрагменты в нормализованном виде (слово целиком: «быстро», «удобного»;
    выражение — как в списке), без повторов, в порядке появления в тексте.
    """
    # TODO: ваш код
    ...


def is_compound(text):
    """Похоже ли требование на составное — несколько обязательств в одном.

    Правила (по порядку; текст нормализован, как в find_vague):
      1) в тексте есть «и/или» → да;
      2) модальных слов из MODAL_WORDS («должна» и т. п.) два и больше → да;
      3) в части после первого модального слова (если его нет — во всём тексте), выбросив фрагменты
         в кавычках «…» и выражения из VAGUE_PHRASES, есть отдельное слово «и» или «или» → да;
      иначе → нет.
    Это эвристика: она подсвечивает кандидатов, а решает человек.
    """
    # TODO: ваш код
    ...


def lint(text):
    """Проверить спецификацию и вернуть список проблем — кортежей (где, правило, подробности).

    «Где» — идентификатор требования, а для требования без него — «строка N».
    Для каждого требования по порядку проверьте правила в таком порядке:
      RULE_NO_ID       — нет идентификатора (подробности — текст требования);
      RULE_VAGUE       — есть размытые слова (подробности — find_vague через «, »);
      RULE_COMPOUND    — составное требование (подробности — пустая строка);
      RULE_NO_SCENARIO — у требования с идентификатором нет ни одного сценария (подробности — "").
    После всех требований — сценарии по порядку: каждая ссылка на требование, которого нет
    в спецификации, даёт (id сценария, RULE_UNKNOWN_REF, id требования).
    """
    # TODO: ваш код
    ...


def format_report(text):
    """Отчёт линтера для человека (готовая функция)."""
    requirements, scenarios = parse_spec(text) or ([], {})
    problems = lint(text) or []
    lines = [f"Требований: {len(requirements)}, сценариев: {len(scenarios)}, проблем: {len(problems)}"]
    for where, rule, details in problems:
        lines.append(f"  {where:10} {rule}" + (f": {details}" if details else ""))
    return "\n".join(lines)


print(format_report(EXAMPLE_SPEC))

Дополнительно. Сохраните решение как lint_spec.py и прогоните его на спецификации записи на поток из этого урока (должно быть 0 проблем), а потом на черновике, который напишет ваш агент по подсказке из блока «Как это у ИИ-агентов». Сколько проблем нашёл линтер? Какие из них настоящие, а какие — ложные срабатывания эвристики?

Проверь себя6 вопросов
1 К какому шаблону EARS относится требование «Если свободных мест на потоке нет, то система должна отказать в записи с ошибкой NoFreeSeats»?
2 Какие требования плохие? Выберите все верные варианты.Можно выбрать несколько ответов
3 Правило: «при отмене за 7 суток и более до старта — полный возврат». Агент может перепутать >= и >. Какой сценарий обязательно нужен, чтобы это поймать?
4 Агент при уточнении спецификации нашёл вопрос: «поток перенесли на другую дату — от какой даты считать 7 суток для возврата?» Эксперты ещё не решили. Как правильно?
5 В спецификации 8 требований REQ-001…REQ-008. Сценарии ссылаются на REQ-001, REQ-002, REQ-003, REQ-005, REQ-006, REQ-007 и REQ-011. Сколько замечаний «нет сценария» выдаст линтер из практики?
6 Что означает идентификатор SC-001 в шаблоне спецификации Spec Kit?
  • Спецификация функции в курсе: Контекст, Пользовательские истории, Требования (REQ-001), Сценарии (SC-001, «Дано / Когда / Тогда», строка «Требования: REQ-…»), Вне рамок, Открытые вопросы. Тот же формат — в ДЗ sdd-hw01-spec.
  • Требование — одно проверяемое обязательство со словом «должна». Шаблоны EARS (Мавин и др., 2009) заставляют назвать триггер или состояние; их используют и инструменты SDD, например Kiro. Нефункциональные требования — с измеримым порогом.
  • Критерий приёмки — сценарий с конкретными данными, границами и наблюдаемым результатом; ID сценария переходит в имя теста.
  • Плохие требования: размытые слова, составные, решение вместо проблемы, без числа, чужие слова, запрет без поведения. Механическую часть ловит линтер, смысл проверяет человек.
  • Неясное не угадывают: [УТОЧНИТЬ: вопрос], цикл из нескольких вопросов агента с вариантами ответа, решения эксперта записываются обратно в спецификацию.

Дальше: урок SDD.2.2 — технический план, модель данных и контракты: как перейти от «что» к «как», где здесь контексты и агрегаты DDD и сколько деталей давать агенту, чтобы он не додумывал и не утонул.