Методология
Этот документ описывает, как организована спецификация сервиса ЭНЗС, где что лежит и как с ней работать — разработчику, ИИ-ассистенту и любому, кто читает спеку впервые.
Спека — источник правды для устройства системы. Цель — не подробное руководство, а такой набор фактов, который не противоречит коду, не устаревает быстро и собирается в одно место для понимания.
1. Spec first
Спецификация — источник правды. Изменение поведения системы начинается с изменения спеки, не с правки кода. Реализация догоняет спеку, не наоборот.
2. AI first + optimized
ИИ-агент — первоклассный читатель спеки наравне с человеком. Формат, размеры порций, способ ссылаться — выбираются так, чтобы агент мог их потреблять без потерь. Оптимизация под агента — это compile-safe идентификаторы, плоские ссылки nameof(), отсутствие неявных соглашений «из контекста».
3. C# compilable
Спецификация оформлена как компилируемый C#-проект (sdaid/Rugrid.Sdaid.Epas/). Это даёт линтер: ссылка на несуществующий артефакт ломает сборку, переименование через F2 правит все упоминания, find-references показывает обратные связи без отдельных индексов.
4. Human readable
C#-формат не отнимает у человека читаемости. Генератор превращает классы в навигируемый HTML-сайт с прозой, таблицами, диаграммами и кликабельными ссылками между артефактами. Любой раздел доступен по стабильному URL и пригоден для копи-пейста в чат, обсуждение, ревью.
5. Материализация прозы
Любой кусок прозы, на который команда уже ссылается («FR-301», «модель потребления», «энергопрофиль», «отчёт MVER-R01») — кандидат на материализацию в отдельный класс-артефакт. До материализации это текст; после — адресуемая сущность с собственной страницей, секциями, обратными ссылками. Vision-таблицы — пример прогрессивной материализации: текст «FR-301» в ячейке становится ссылкой на IFunctionalRequirementDocument-класс ровно тогда, когда требование берётся в проработку.
6. Правило одного факта
Каждый факт описан ровно в одном месте. Если он нужен в двух — второе место это ссылка (typed-reference, не копия).
Из этого правила следует много частностей:
- Глоссарий — один на проект; vision/use-case/handler ссылаются на термины через
nameof(), не пересказывают определения. - Domain-сущность — в одном доменном модуле; другие места ссылаются через
nameof(). - Описание операции — в handler-spec; endpoint, service, use-case ссылаются на него, не дублируют narrative.
- Acceptance criteria — на одном уровне (эпик или use-case); не дублировать на оба.
- Open question (OQ-N) — в одном месте (документ эпика); use-case/page/handler ссылаются.
- Status, version, comments — в самом артефакте; никаких «параллельных трекеров».
🔍 Признак нарушения: если поиск по точной фразе в проекте даёт ≥2 result-а в разных артефактах — это дрейф. Нужно: оставить ровно один источник правды, остальные превратить в typed-ссылку (
nameof,SdaidAnchors.RefTo<T>(text),GlossaryAnchors.RefTo<T>(text)).
7. Что куда писать / структура папок
Где живёт vision, глоссарий, требования, эпики, доменные сущности, архитектурные решения — фиксируется явно. Цель: и человек, и ИИ-агент однозначно знают, куда положить новый артефакт и где искать существующий — без догадок.
7.1. Принципы (зачем именно так)
Один домен — ntr. Вся предметная область сервиса ЭНЗС — единый домен. Чтобы группировать сущности, домен поделён на доменные модули (50_Domain/<Module>/). Это группировка по смыслу, а не отдельные Bounded Contexts. Поэтому контракт API и UI-спека живут на корне спеки (45_Api, 65_Ui), а не внутри доменных модулей.
Слои — горизонтальные, на корне. Каждый слой живёт в своём top-level разделе: контракт API — 45_Api, домен — 50_Domain, поведение бэка — 55_Backend, эпики — 60_Epics, UI-спека — 65_Ui. Внутри раздела — группировка по области/эпику (Endpoints/<Area>/, Handlers/<Area>/, Pages/<Area>/).
Эпик — тонкая нить. 60_Epics/<Epic>/ собирает сквозной контекст (use-case-ы, локальные решения, AC, state-machine-ы) и ссылается на артефакты слоёв через nameof(). Реализационные артефакты use-case-а (endpoint, handler, страница) лежат в своих слоях, не в подпапках use-case-а.
Реализация — 80_Subsystems, по необходимости. Особенности конкретной реализации деплой-единицы (ADR подсистемы, нюансы стека) — в 80_Subsystems/. Если особенностей нет — раздел не заводится. Инфраструктурные контракты (file/MQ/интеграции — например, приём данных из АИС) — в 70_Infrastructure/, тоже по необходимости.
7.2. Структура (верхнеуровневые разделы)
00_Methodology/ как работать со спекой
00_Migration/ инструкция миграции md→SDAID (временная, пока идёт перенос)
10_Vision/ слабоструктурированный вход от заказчика
20_Glossary/ общий проектный словарь
30_Requirements/ FR — материализуются по мере проработки
35_ChangeRequests/ батчи замечаний и пожеланий
40_Arch/ системные кросс-cutting решения
ADR/ общесистемные ADR
Tech/CSharp/ техно-ADR backend
Tech/Angular/ техно-ADR frontend
45_Api/ контракт API
Based/ общие базовые типы (BasedCommand, BasedQuery, …)
Endpoints/<Area>/ endpoint + Command/Query + Result
50_Domain/ домен ntr, поделён на доменные модули
<Module>/
BC_<Module>.cs overview модуля (IBoundedContextModuleDocument)
<Entity>.cs сущности и агрегаты
55_Backend/ поведение бэка
Handlers/<Area>/ canonical narrative операций (IHandlerDocument)
Services/<Area>/ доменные / прикладные сервисы
60_Epics/ бизнес-направления (тонкая нить)
<Epic>/
EPIC_<Epic>.cs цель, акторы, scope, открытые вопросы, AC
UseCases/<UC>/ use-case-документы (Given/When/Then), state-machine-ы
65_Ui/ UI-спека
Pages/<Area>/ страницы (IPageDocument)
Componetns/, Layout/ компоненты и layout-ы
html/ общий shared.css для HTML-прототипов
70_Infrastructure/ [при необходимости] инфраструктурные контракты (file/MQ/интеграции)
80_Subsystems/ [при необходимости] особенности реализации деплой-единиц
7.3. Папка → kind-маркер (что внутри какой папки)
| Папка | Kind-маркер класса | Что внутри |
|---|---|---|
00_Methodology/ |
ISdaidDocument (без подтипа) |
Документ методологии проекта (этот файл) |
00_Migration/ |
ISdaidDocument (без подтипа) |
HowTo миграции md→SDAID (временно) |
10_Vision/ |
IVisionDocument |
Vision-документ |
20_Glossary/Terms/ |
IGlossaryTerm (отдельный контракт, не ISdaidDocument) |
Термины проекта, по одному классу на термин |
30_Requirements/ |
IFunctionalRequirementDocument |
FR-документы (материализуются по мере проработки) |
35_ChangeRequests/ |
IChangeRequestDocument |
Батчи замечаний и пожеланий |
40_Arch/ADR/ |
IAdrDocument |
Общесистемные ADR |
40_Arch/Tech/<Stack>/ |
IAdrDocument |
Техно-ADR (CSharp, Angular) |
45_Api/Endpoints/<Area>/ |
IApiContractDocument (+ IFolder<ApiFolder>) |
Endpoint + Command/Query + Result |
45_Api/Based/ |
ISdaidDocument |
Общие базовые типы команд/запросов |
50_Domain/<Module>/ (overview) |
IBoundedContextModuleDocument |
Описание доменного модуля |
50_Domain/<Module>/<Entity>.cs |
IDomainEntityDocument |
Сущности и агрегаты (агрегаты пока без отдельного маркера) |
55_Backend/Handlers/<Area>/ |
IHandlerDocument (+ IFolder<BackendFolder>) |
Canonical narrative операции |
55_Backend/Services/<Area>/ |
IApplicationServiceDocument / IDomainServiceDocument (+ IFolder<BackendFolder>) |
Сервисы реализации |
60_Epics/<E>/ (overview) |
IEpicDocument |
Цель эпика, акторы, scope, открытые вопросы, AC |
60_Epics/<E>/UseCases/<UC>/ |
IUseCaseDocument (с PrimaryActor) |
Given/When/Then narrative |
60_Epics/<E>/UseCases/<UC>/StateMachines/ |
IStateMachineDocument |
State machine с mermaid в Body |
65_Ui/Pages/<Area>/ |
IPageDocument (+ IFolder<UiFolder>) |
Спека страницы |
65_Ui/Componetns/, Layout/ |
IUiComponentDocument / IUiSpecDocument (+ IFolder<UiFolder>) |
Компоненты, layout-ы |
70_Infrastructure/<Capability>/ |
IInfrastructureDocument |
Контракты на file/MQ/интеграции (по необходимости) |
80_Subsystems/<S>/ |
IApplicationDocument, IAdrDocument и др. (+ IFolder<SubsystemsFolder>) |
Особенности реализации подсистемы (по необходимости) |
7.4. Что куда — детально
| Что за артефакт | Где живёт |
|---|---|
| Доменная сущность / агрегат | 50_Domain/<Module>/<Entity>.cs |
| Endpoint + Command/Query + Result | 45_Api/Endpoints/<Area>/ |
| Handler (canonical narrative) | 55_Backend/Handlers/<Area>/ |
| Доменный / прикладной сервис | 55_Backend/Services/<Area>/ |
| Инфраструктурный контракт | 70_Infrastructure/<Capability>/ (по необходимости) |
| Страница / компонент UI | 65_Ui/Pages/<Area>/, 65_Ui/Componetns/ |
| Use-case (Given/When/Then) | 60_Epics/<E>/UseCases/<UC>/ |
| State machine | 60_Epics/<E>/UseCases/<UC>/StateMachines/ |
| Системный ADR | 40_Arch/ADR/ |
| Tech ADR (Angular/EF/CSharp) | 40_Arch/Tech/<Stack>/ |
| Особенности реализации подсистемы | 80_Subsystems/<S>/ (по необходимости) |
7.5. Эпик ↔ доменный модуль ↔ слой
Разные сущности, связанные через ссылки:
- Эпик (
60_Epics/<E>/) — бизнес-направление, единица планирования; тонкая нить со сквозным контекстом. - Доменный модуль (
50_Domain/<Module>/) — группа сущностей одного домена ntr. - Слой (
45_Api,55_Backend,65_Ui) — артефакты реализации по типу.
Один эпик затрагивает ≥1 доменный модуль и артефакты нескольких слоёв; модуль/слой обслуживают ≥1 эпик. Связи трассируются через nameof() в обе стороны.
8. Границы ответственности, контракты и соглашения
Каждый модуль (домен, API, backend, UI) отвечает за свою часть знаний и не лезет в чужую. На границах модулей фиксируются явные контракты (что один модуль обещает другому) и соглашения (как они общаются: имена, формат данных, ошибки, идемпотентность). Изменение контракта — событие, требующее согласования; изменение внутреннего устройства модуля — внутреннее дело модуля.
9. Эпики и слои
Эпик — это бизнес-цель, реализация которой проходит через несколько слоёв (домен, API, backend, UI).
Артефакты слоёв первичны — сущность, страница, endpoint существуют сами по себе и в принципе могут участвовать в нескольких эпиках. Эпик их не «забирает себе», а собирает вместе и добавляет cross-cutting контекст (use-case, state-machine, AC, локальные решения).
Артефакт физически группируется в подпапку с именем области/эпика, если он относится только к нему:
45_Api/Endpoints/<Area>/— endpoints области;55_Backend/Handlers/<Area>/— хендлеры области;55_Backend/Services/<Area>/— сервисы и алгоритмы;65_Ui/Pages/<Area>/— страницы;50_Domain/<Module>/— сущности живут в доменных модулях (часто переиспользуются между эпиками).
Если артефакт начинает использоваться в нескольких эпиках — он поднимается на верхний уровень слоя (без эпик-подпапки) и его эпик-специфичные ссылки выносятся в epic-уровень.
60_Epics/<Epic>/ — тонкая нить: эпик-документ, use-cases, state-machines, локальные решения, AC. Содержит только то, что характерно для пересечений слоёв; не дублирует их.
9.1. Ссылки артефактов слоя на эпик
Артефакт, физически лежащий в подпапке своего эпика, может ссылаться на локальные документы эпика (OQ-N в эпик-документе, R-N в локальных ADR, AC-*). Это естественный контекст: пока артефакт привязан к эпику, его комментарии живут в эпик-словаре.
Если артефакт перестаёт быть эпик-специфичным — все ссылки на эпик надо вычистить до перемещения наверх. Артефакт без эпика не должен ссылаться на конкретный эпик-документ.
10. Структурные связи между документами
Зависимости между документами фиксируются явно — типизированными структурными связями
(IHasStructuralLinks + Rel.Serves<T>() / Rel.Realizes<T>() / Rel.Uses<T>()), а не
только ссылками в прозе. Это отдельный курируемый слой: генератор рендерит его блоком
«Структурные связи» на странице документа и сводит в страницу «Граф связей» (по одной
диаграмме на эпик). Объявлять связи — обязательная часть спеки, а не опция: каждый
документ, зависящий от другого, должен нести эту зависимость явно.
10.1. Правило направления — от вариативного к стабильному
Связь объявляется на более вариативной стороне и указывает на более стабильную. Стрелки всегда идут «сверху вниз» по шкале вариативности ниже (от того, что меняется часто, к тому, что меняется редко) — так одно правило объясняет направление всех зависимостей разом. Обратную подпись («используется» / «реализуется» / «обслуживается») генератор строит сам на целевой странице.
Шкала вариативности (по убыванию; чем выше — тем чаще меняется):
| Понятие | Вариативность | Что гонит изменчивость |
|---|---|---|
| page | 9 | UI: редизайны, перекладка полей и флоу — постоянно |
| use_case | 8 | продуктовый сценарий, меняется вместе с требованиями |
| epic | 7 | стратегическая группировка, тасуется с роадмапом |
| endpoint | 7 | контрактная поверхность; плодится версиями (но опубликованное — липкое) |
| read_model | 6 | форма диктуется запросами и UI |
| consumed_contract | 6 | чужой контракт, меняется вне твоего контроля |
| message (Command/Query) | 5 | внутренний DTO, следует за входами операции |
| domain_event | 5 | публикуется наружу → как контракт, липкий |
| handler | 4 | оркестрация; сама операция стабильна |
| policy | 4 | бизнес-правила — самая подвижная часть домена |
| domain_service | 3 | доменная координация, стабильна вместе с моделью |
| aggregate | 3 | граница консистентности, ядро инвариантов |
| entity | 2 | живёт внутри агрегата |
| value_object | 1 | примитивы-бедрок (Money, период, единица измерения) |
| bounded_context | 1 | сама граница; перечерчивается крайне редко |
10.2. Типовые связи (источник → цель, вниз по шкале)
- page → use_case / epic —
Rel.Realizes<…>()(страница реализует сценарий; если use-case-документа нет — реализует эпик напрямую). - use_case / epic → endpoint —
Rel.Uses<…>()(сценарий/эпик использует endpoint-ы). - endpoint → handler —
Rel.Uses<…>()(контракт опирается на обслуживающий хендлер).
Rel.Serves (подпись «обслуживает») семантически закреплён за направлением handler →
endpoint. Так как handler стабильнее endpoint-а, в этом проекте мы объявляем связь на
вариативной стороне — endpoint → handler через Rel.Uses (а обратная подпись
«используется» появляется на странице хендлера автоматически).
11. Проверки качества документа (обязательны при каждом касании)
При создании или правке любого документа спеки проверять пять пунктов — и озвучивать замечание, если что-то не так (не молча):
Структурные связи проставлены. Документ, зависящий от другого, обязан нести связь явно (
IHasStructuralLinks, см. раздел «Структурные связи»). Забыли проставить — напомнить и доставить.Проза, а не структурное описание. Тело документа пишется связной прозой (по материализации прозы из секций), а не «структурным конспектом»/списком ради списка. Встретили документ, написанный структурным описанием вместо прозы — по возможности переписываем в прозу; как минимум — напоминаем.
Нет документов-сирот. У документа должна быть хотя бы одна связь — исходящая или входящая (на него кто-то ссылается, либо он ссылается). Встретили полностью изолированный документ (никто не ссылается и он ни на кого) — напомнить: либо вписать его в граф, либо это сигнал, что документ лишний/недописан.
Vision — точка входа. Vision не просто неструктурированный вход от заказчика (
10_Vision/) — вся спецификация разбивается на разделы, достижимые от Vision: от Vision до любого пункта спецификации должен существовать путь по ссылкам (RefTo/nameof, структурные связи), пусть и через несколько документов. Новый документ, до которого от Vision нет пути — не только документ-сирота (п.3) в узком смысле «нет связей вообще»; это пробел именно в навигации Vision — отследить и дописать путь, а не только факт наличия произвольной связи.Раздел — только по теме заголовка. Текст под заголовком раздела/секции относится только к этому разделу. Факт, принадлежащий другой теме (другой процедуре, ролевой модели внутри описания загрузки, плану правок самого документа внутри описания целевого состояния и т. п.), не смешивается в прозу «заодно» — выносится в свой раздел, даже если по смыслу он рядом.
12. Запросы на изменение (Change Request)
Замечания после тестирования и пожелания пользователей оформляются батчами в документы
kind IChangeRequestDocument (папка «Запросы на изменение», 35_ChangeRequests/). Цель —
трассируемость: по системе видно, почему принято то или иное решение.
Один батч = один документ. Имя — дата + тема: DOC_CR_<гггг>_<мм>_<дд>_<Тема>. Новые
замечания другой даты — новый документ (старый не правим задним числом, кроме раздела «Итог»).
Структура CR-документа:
- Источник (как есть) — дословный текст замечаний/пожеланий (verbatim: язык и опечатки сохраняем, как при миграции).
- Разбор — что меняем по каждому пункту, детали, открытые вопросы.
- Blast radius — на что влияет: спеки (страницы/компоненты/endpoint/use-case), backend,
UI, миграции/swagger. Затронутые документы объявляются структурными связями
(
Rel.Uses<…>) — тогда влияние видно на «Графе связей», а на затронутых страницах появляется обратная ссылка на CR. - План — шаги (спека → реализация), чек-лист.
- Итог — что фактически сделано (со ссылками) + статус по каждому пункту:
выполнено/отложено/в работе.
Ссылки в прозе. Кроме раздела «Источник (как есть)» (он verbatim), в прозе CR на каждую
упоминаемую страницу/компонент/endpoint/use-case/термин ставим навигационную ссылку
(SdaidAnchors.RefTo<…>() в interpolated-строке, либо markdown-ссылка), чтобы из CR можно
было перейти к затронутому документу. Затронутые документы дополнительно объявляем
структурной связью Rel.Uses<…> (blast radius на графе).
UI-изменения — сначала макет. Если CR меняет внешний вид страницы, сперва обновляем (или создаём) её wireframe-мокап, показываем пользователю и получаем «ок», и только затем идём в реализацию. Не начинаем кодить визуальную часть, не показав, что получится.
Процесс: оформили CR из «как есть» → обсудили разбор/blast radius/план → идём по нему (спека + мокап для UI → согласование → реализация) → фиксируем «Итог» и откладываем CR (статус). Так батчи замечаний не перемешиваются и каждое решение прослеживается до источника.
13. Сервисы и алгоритмы
В backend бывают абстракции, которые не являются ни хендлерами, ни сущностями: сервисы (например, оркестрация приёма данных из АИС, расчёт по модели потребления, отправка уведомлений) и алгоритмы (парсеры выгрузок, экстракторы полей, валидаторы энергопаспорта). Их тоже надо специфицировать.
13.1. Расположение
Сервисы и алгоритмы живут в слое 55_Backend (см. M_S6_FolderStructure):
- Доменный / прикладной сервис — в
55_Backend/Services/<Area>/<Name>Service.cs. - Инфраструктурный контракт (файловое хранилище, MQ, внешние интеграции — контракт на работу с внешней капабилити) — в
70_Infrastructure/<Capability>/, по необходимости. - Алгоритмы (чистая трансформация данных без побочных эффектов) — рядом с сервисом, который их использует. Отдельный
Algorithms/-уровень не вводим, пока их немного.
13.2. Что в C#-файле сервиса
- Класс (НЕ интерфейс) — чтобы методы могли иметь тело. Класс — spec-only, не используется в runtime, не реализует интерфейсы и не содержит реальной логики.
- XML-doc на классе — назначение сервиса и архитектурная привязка (кто оркестратор, как сервис связан с state machine).
- XML-doc на каждом методе — короткий контракт: что делает (1-3 предложения), что возвращает, что НЕ трогает.
- Тело каждого метода:
var spec = $@" КАК выполнить: 1. ... 2. ... ЧЕГО НЕ ДЕЛАТЬ: - ... "; throw new NotImplementedException(spec);- Строка
spec— детальные шаги «КАК» (нумерованным списком), затем «ЧЕГО НЕ ДЕЛАТЬ», инварианты (например, оптимистическая блокировка). throw new NotImplementedException(spec)— гарантирует, что (а)specне unused (без warning'а), (б) при ошибочном вызове в реальной сборке текст spec попадёт в exception message.
- Строка
- Result-классы для результатов методов — отдельные публичные типы рядом, в том же файле.
13.3. Почему класс, а не интерфейс
Конвенция «комментарий говорит ЧТО, тело говорит КАК» — естественная и ценная. Интерфейс не позволяет описать «КАК должен работать метод», только сигнатуру и контракт. Если запихать «КАК» в XML-doc, получается смешение «что» и «как» в одном комментарии — теряется смысловое разделение.
Класс с пустыми телами решает это естественно: XML-doc метода = «что», тело со spec = «как». Реализация в src/ — отдельная задача и отдельный класс.
Spec-классы:
- живут в
Rugrid.Sdaid.Epas.Ntr.csproj, не подтягиваются в runtime-solution; - не реализуют никаких интерфейсов (это забота реализации);
- не имеют конкретной логики — только
spec-строки иthrow NotImplementedException(spec).
13.4. Когда всё-таки interface
Если у спеки нет «как» (только контракт типа маркера или dispatch-точки) — допустим интерфейс. Но когда в спеке есть существенный «КАК» — это сигнал использовать класс.
13.5. Что НЕ делать
- Не реализовывать тела методов. Это спека, не прототип.
- Не описывать в XML-doc высокоуровневые сценарии — это в use-case или state machine, со ссылкой.
- Не дублировать содержимое state machine в спеке сервиса. Сервис описывает что делает шаг, state machine — когда какой шаг разрешён.
13.6. Связка сервис ↔ хендлер ↔ state machine
Типичная картина для сервисов с состоянием:
- state machine — какие статусы есть, какие переходы разрешены, когда какой переход.
- сервис — что конкретно происходит на конкретном шаге (verify, process batch); сервис не знает о state machine и о конкретном вызывающем — может переиспользоваться.
- хендлер — каноническое описание «что произойдёт при вызове endpoint». Проводит читателя сквозь всю цепочку, интегрирует state machine и сервис в цельный рассказ.
Все три ссылаются друг на друга через nameof(). Хендлер не дублирует содержимое state machine и XML-doc сервиса, но интегрирует их: где сервис делает «что», state machine задаёт «когда какой переход», а хендлер связывает их в «при вызове endpoint происходит вот это».
14. Каноническое описание операции
Когда читатель приходит с вопросом «что произойдёт, если дёрнуть этот endpoint», он должен в одном месте получить полную картину — без необходимости стыковать обрывки из 4-х файлов.
Этим местом является handler-spec — string spec в <Name>Handler.cs.
14.1. Распределение описания
| Файл / артефакт | Где живёт | Что описывает |
|---|---|---|
Endpoint (<Name>Endpoint.cs) |
45_Api/Endpoints/<Area>/ |
Назначение операции в 1-3 предложения + указатель на handler через <see cref>. Не описывает поля — XML-doc каждого поля живёт на самом поле в Query/Command/Result. |
Handler (<Name>Handler.cs) |
55_Backend/Handlers/<Area>/ |
Каноническая narrative — проводит сквозь всю цепочку: валидация входа → загрузка данных → ветвление по состоянию → вызовы методов сервиса (с краткими 1-2-предложными summary) → применение результата → сохранение → ответ. Реакции на ошибки и не-happy-path тоже здесь. |
Service (<Name>Service.cs) |
55_Backend/Services/<Area>/ |
Что конкретно делает один шаг — без оркестрационного контекста и без ссылок на конкретного вызывающего (сервис может переиспользоваться). Класс, не интерфейс (см. соответствующую секцию). |
State machine (<Aggregate>StateMachine.cs) |
60_Epics/<E>/UseCases/<UC>/StateMachines/ |
Какие статусы и переходы разрешены, при каких условиях. Не содержит «как работает конкретный handler». Описывается секциями с mermaid в Body. |
| Use case | 60_Epics/<E>/UseCases/<UC>/ (класс use-case-а как IUseCaseDocument + Given/When/Then секциями) |
Сквозной сценарий пользователя через несколько endpoint-ов. Если операция ограничена одним endpoint — use-case не нужен. |
14.2. Что НЕ делать
- Не делать handler «самым тонким». Тонкий handler делает читателя несчастным: full story рассыпается. Handler не дублирует детали сервиса/state-machine, но интегрирует их.
- Не плодить overview-документы. Если возникает соблазн написать отдельный
<Operation>.overview— это плохой признак: содержание должно жить в handler-spec, а не рядом. - Не дублировать narrative в endpoint-XML-doc. Endpoint описывает только назначение (1-3 предложения) и указывает на handler. Полное описание — в handler-spec.
- Не описывать поля endpoint в endpoint-summary. Каждое поле имеет своё XML-doc на самом себе — там и хранится описание (тип, default, диапазон, связанные правила). Дубликат в summary endpoint-класса гарантирует drift: при изменении поля никто не вспомнит поправить summary. Если нужна компайл-сейфти ссылка —
<see cref>на тип поля в endpoint summary, а не пересказ. - Не описывать в сервисе оркестрационный контекст. Сервис рассказывает «что я делаю на одном вызове моего метода», без знания о том, кто и в каком порядке его вызывает. Иначе он перестаёт быть переиспользуемым.
14.3. Длина handler-spec
- Для query-эндпоинтов с простой логикой — 3-7 шагов, 10-20 строк.
- Для command-эндпоинтов с workflow / state machine — 30-60 строк, и это нормально: handler остаётся единственным местом «что произойдёт».
14.4. Ссылки в spec-строках — inline nameof / RefTo
nameof(...) и SdaidAnchors.RefTo<...>() ставятся inline в spec-строках.
Выносить их в class-level const-алиасы не требуется: спека читается в сгенерированном
HTML (не в исходнике), а алиасы создают лишний шум при чтении кода (в т.ч. для
ИИ-агента). При необходимости подключаем namespace через using.
15. Страницы и формы (page / modal / drawer)
Страница описывается прозой — какие возможности от неё ожидаются, а не структурой
компонентов. Это IPageDocument (в 65_Ui/Pages/<Area>/) с:
Description— одно предложение: что это за страница;- один-два прозовых блока (
public static string), например «Возможности» и «Карточка (drawer)» — флоу-текст: что показывает список (грид, поля, поиск, подгрузка), что можно открыть, создать, отредактировать, удалить; какие защиты и какой доступ.
Ссылки — типизированные (SdaidAnchors.RefTo<...>()): на endpoints-источники данных и
команды, на доменные сущности, на другие страницы. Клик ведёт на их страницы,
переименование ловит компилятор. Голую прозу-имя без ссылки не оставляем.
Список и его карточка/форма (drawer/modal) живут в одном документе страницы, пока
близки по смыслу. Если форма вырастает в модалку или отдельную страницу редактирования —
выделяется в отдельный IPageDocument.
15.1. Что НЕ пишем
- Структуру компонентов — никаких именованных свойств-кнопок, блоков колонок,
dynamic-полей, таблиц «Name/Required/Sends». Это шум; нужное говорится прозой. - Размещение в меню/навигации — решение sitemap-а, не свойство страницы.
- Точную вёрстку — это HTML-прототип и реализация, не спека-описание.
15.2. Доступ
Доступ к странице и действиям — через UI-claims; серверные операции дополнительно проверяются на backend. В прозе указываем ожидаемый доступ.
15.3. HTML-прототип
Кликабельный макет кладётся в html/ рядом со страницей и встраивается в спеку через
SdaidHtml.IframeOf<TPage>(...) (+ SdaidHtml.LinkTo<TPage>(...) — открыть в новой вкладке).
Прототип берёт стили из единого 65_Ui/html/shared.css — не инлайнит свой CSS,
чтобы прототипы не «плыли» друг от друга. Разметка прототипа самодостаточна, общий стиль —
один на все.
16. Когда нужен use-case, а когда нет
Use-case-документ создаётся только если выполнено хотя бы одно:
- сценарий проходит через 2+ страницы;
- запускает фоновый процесс или интеграцию;
- задевает 2+ доменных модуля;
- имеет состояния, сохраняемые между шагами;
- содержит cross-cutting NFR.
Иначе — описание живёт в page-спеке. «Просмотр одного грида + одного query» — не use-case, это функциональность одной страницы.
Use-case — карта пересечений: цель, актор, шаги Given/When/Then на бизнес-уровне (без слов «контроллер», «компонент»), таблица «шаг → page → endpoint → handler → entity», локальные NFR, открытые вопросы. Пересказ page-спеки в use-case — запрещён.
17. State machines
State-machine состоит из трёх частей, и попытка положить всё в один формат теряет одну из них:
- Структура (статусы, допустимые переходы) — таблица.
- Поведение (что делается на переходе) — проза.
- Архитектурная привязка (где исполняется) — короткий блок.
State-machine — это C#-класс с маркером IStateMachineDocument, в Body секций которого:
- Перечень статусов берётся из enum (
IDomainEntityDocumentна enum-сущности в50_Domain/<Module>/) — форма уже зафиксирована в коде. - Mermaid
stateDiagram-v2для визуализации (в Body секции). - Таблица допустимых переходов с триггерами (в Body секции).
- Проза по поведению на каждом переходе (отдельная секция или абзац).
- В конце — где и как исполняется (оркестратор, сервисы).
Файл размещается рядом с use-case-ом, который владеет машиной: 60_Epics/<E>/UseCases/<UC>/StateMachines/<Aggregate>StateMachine.cs. Имена статусов компайл-сейфтятся через nameof() на enum.
18. Именование
- Все классы — PascalCase:
EnergyProfilesListPage.cs,CreateImportJobUseCase.cs. - Документ-обёртка над артефактом реализации — с префиксом
DOC_:DOC_<Entity>,DOC_<Name>Endpoint,DOC_<Name>Handler. - Эпик —
EPIC_<Epic>.cs; доменный модуль —BC_<Module>.cs. - Use-case — PascalCase, имя класса говорит о действии:
CreateImportJob,ApproveEnergyPassport. - State-machine — PascalCase с суффиксом
StateMachine(илиLifecycleдля не-статусных машин). - Страницы —
<Name>Page.cs, дроверы/модалки —<Name>Drawer.cs/<Name>Modal.cs; HTML-прототип —<Name>.htmlвhtml/рядом со спекой. - Имена не дублируют контекст папки: внутри
EnergyProfile/пишемProfilesListPage, а неEnergyProfileProfilesListPage. - Реализация (
src/) ссылается на спеку, не наоборот. Имена файлов в коде могут отличаться от имён файлов спеки.
19. ADR vs локальные решения эпика
40_Arch/ADR/— сквозные решения проекта со строгой нумерацией:ADR_<NNN>_<Name>.cs(класс сIAdrDocument). Сюда попадает то, что переиспользуется или влияет на проект целиком.40_Arch/Tech/<Stack>/— техно-практики (Angular, .NET, EF). ТожеIAdrDocument-классы, но привязаны к стеку.60_Epics/<E>/Adr/— локальные решения этого эпика. Классы видаR_N_<Name>.cs(всё ещёIAdrDocument, нумерация R-1, R-2 в пределах папки).80_Subsystems/<S>/Adr/— ADR деплой-единицы.
Если решение в эпик-локальном Adr начинает повторяться в других эпиках — поднимаем его в 40_Arch/ADR/ отдельной задачей.
20. Открытые вопросы
Все нерешённые вопросы эпика живут в одном месте — секция «Открытые вопросы» в IEpicDocument-классе эпика. Use-cases, UI-spec-и, handler-spec-и и state-machine-ы на эти вопросы ссылаются через nameof() на конкретные секции (OQ-секции эпика — каждая отдельный класс-секция, тогда auto-linkify работает).
Это запрещает спеке выглядеть «готовой», когда в ней дыры.
21. Acceptance criteria
AC живут на двух уровнях:
60_Epics/<E>/Acceptance.cs(или секции внутри эпик-документа) — cross-cutting AC всего эпика (системно-важные).60_Epics/<E>/UseCases/<UC>/Acceptance.cs— локальные AC use-case-а.
Главное представление — сводный список всех AC по проекту со статусами (генерится автоматически по kind-index-у или через faceted view).
Правила:
- Только пользователь-наблюдаемое поведение. Внутренние инварианты бэка (защита запрещённых переходов state-machine, оптимистические блокировки, конкурентность) описаны в state-machine и реализуются автоматически — не выносить в AC.
- Измеримость. Каждое AC — потенциальный e2e-тест или ручной чеклист.
- Префикс по группе для удобства ссылок: AC-C* (создание), AC-L* (список), AC-D* (детали), AC-S* (lifecycle, наблюдаемая часть), AC-X* (consistency данных), AC-N* (NFR). Состав групп — на усмотрение эпика.
- AC, заблокированные открытыми вопросами, помечаются ссылкой на соответствующий OQ через
nameof().
22. Карта реализации (traceability)
Traceability автоматическая — строится генератором из nameof()-упоминаний в прозе и из структурных связей. Не нужно вручную поддерживать таблицу «Артефакт → Где»; mention-graph её собирает.
Use-case остаётся самостоятельным документом (IUseCaseDocument) потому что:
- Given/When/Then narrative нельзя «выжать» из ссылок на артефакты;
- локальные NFR и AC use-case-а — их собственные секции;
- открытые вопросы UC ссылаются на конкретные шаги.
На странице эпика и use-case-а генератор автоматически рендерит блок «Связанные артефакты» (все классы, упомянутые через nameof()) и «Где используется» (обратные ссылки).
23. Как работать со спекой
23.1. Разработчику
- При работе с реализацией в
src/Ntr.*илиsrc/Ntr.Web— открой соответствующий артефакт спеки в рендеренном сайте; навигация от него к связанным (handler ↔ service ↔ state machine ↔ use-case) делается через mention-graph. - При работе с эпиком — начинай с
60_Epics/<E>/EPIC_<E>.cs, оттуда — по auto-linkified ссылкам. - При расхождении spec ↔ impl — фиксируй вопрос явно. Не приводи код к спеке и не правь спеку под код молча.
- Изменил реализацию → проверь спеку соответствующего use-case-а/handler-а/service-а. Не уверен — добавь OQ-секцию в эпике.
23.2. ИИ-ассистенту
- Перед редактированием кода в зоне эпика — загрузи
IEpicDocument-документ эпика, связанныеIUseCaseDocument/IHandlerDocument/IDomainServiceDocument/IStateMachineDocument. - Перед редактированием спеки — проверь существующую реализацию в
src/. - Не проводить тихую синхронизацию spec ↔ impl: при расхождении явно сообщить пользователю, спросить, что — источник правды.
- При работе с HTML-прототипами UI: cs-спека важнее прототипа; прототип после реализации компонента не синхронизировать автоматически.
23.3. Сборка спеки
dotnet build sdaid/Rugrid.Sdaid.Epas/Rugrid.Sdaid.Epas.csproj
Проект — это сама SDAID-спека на C#. Компилируется как линтер: переименование сущности через F2 правит все её упоминания, find-references показывает зависимости, ссылки на несуществующие классы (через nameof(), RefTo<T> и т. п.) ломают сборку.
После любого обновления методологии (и спеки в целом) — обязательно перегенерировать HTML-сайт вызовом sdaid\build-html.cmd. Сборка проверяет компайл-сейфти, рендер — что секции выглядят как ожидалось; одно без другого оставляет рассинхрон.
24. Общие антипаттерны
Базовый список того, что не нужно делать при ведении спеки. Не привязан к структуре папок — антипаттерны процесса/мышления.
- Не плодить use-case-ы, когда хватает page-спеки. Если сценарий = «открыл одну страницу, нажал кнопку, увидел результат» — это поведение страницы, не use-case. Use-case оправдан только при cross-page потоке, фоновом процессе, взаимодействии нескольких доменных модулей или сохранённом состоянии между шагами.
- Не выносить внутренние инварианты бэка в acceptance. AC — это наблюдаемое пользователем поведение. Защита запрещённых переходов state-machine, оптимистическая блокировка, конкурентность — описаны в state-machine и реализуются автоматически. В AC они дублируют контракты.
- Не нумеровать локальные решения эпика в общем ADR-индексе. Локальные R-N эпика — в
60_Epics/<E>/Adr/, со своей нумерацией. Если решение начало повторяться в других эпиках — поднимаем в общий40_Arch/ADR/отдельной задачей. - Не редактировать вручную сгенерированные файлы. Если такие появятся (rendered HTML, генерируемый код) — правка идёт через источник, не через output. Иначе drift гарантирован.
25. Журнал изменений методологии
Принятые изменения самой методологии — для тех, кто читал её раньше и хочет увидеть, что
поменялось, не перечитывая документ целиком. Новые записи — сверху; дата — когда решение
принято. Это журнал решений по методике (не путать с CHANGELOG нотации
Rugrid.Sdaid.Notation, который описывает API авторской нотации).
25.1. 2026-11-05
- Добавлены проверки качества 4 и 5 в «Проверки качества документа»: Vision — точка входа в спецификацию (путь от Vision до любого пункта спеки должен существовать); раздел/секция содержит только текст по теме своего заголовка, без подмешивания фактов другой темы.
25.2. 2026-08-14
- Заведено. Методология перенесена в проект ЭНЗС как основа для второго этапа: структура
папок, правило одного факта, структурные связи, каноническое описание операции, страницы,
state machines, AC, change requests. Исторические разделы прежнего md-гибрида (слои
«по скорости устаревания»,
_Meta.cs-атрибуты, парные.cs+.md) не переносились — спека ведётся сразу в C#-форме.