using System;
using Rugrid.Sdaid.Notation.Documents;
namespace Rugrid.Sdaid.Epas.Ntr.Migration
{
/// <summary>
/// HowTo: миграция markdown-спецификации сервиса ЭНЗС в SDAID-формат.
/// Документ внутри проекта Ntr (а не методологии) — спека самодостаточна, и эта инструкция
/// специфична именно для миграции ЭНЗС. Применяется на каждой повторной итерации миграции.
/// </summary>
public class HowToMigrateMdToSdaid : ISdaidDocument, IFolder<NtrMigrationFolder>
{
public string Name => "Миграция markdown-спецификации ЭНЗС в SDAID-формат";
public string Description =>
@"У команды ЭНЗС уже есть спецификация первого этапа в markdown (Obsidian-хранилище `rugrid-devdocs/service-enter/ntr-01/`, плюс отдельные спеки в `docs/` самого репозитория) — с устоявшимися формулировками, на которые ссылаются задачи, обсуждения с заказчиком, code-review. Переход на SDAID **не должен ломать этот язык**: SDAID — это структурная упаковка существующего текста, а не его «улучшение». Этот документ фиксирует, что считается структурой (зона ИИ-ассистента), что считается содержанием (зона авторов спеки), и пошаговый рецепт.
**Мигрируется только первый этап.** Спецификация второго этапа не пишется в markdown и не мигрируется: новые эпики формулируются **сразу в SDAID**, а сделанное на первом этапе дорабатывается уже в перенесённых SDAID-артефактах. Папка `ntr-02/` — не источник миграции.
Документ читается **вместе с нотацией** (контракты `ISdaidDocument`, `ISdaidDocumentSection`, `IGlossaryTerm`, `IFunctionalRequirement` и хелперы `SdaidAnchors`, `GlossaryAnchors`) — без понимания нотации рецепт ниже применить нельзя.";
public string Version => "1.0";
public string Status => "accepted";
public string[] Comments => new string[0];
}
// ═══════════════════════════════════════════════════════════════════════════════
// 0. Главный мета-принцип: план → апрув → исполнение
// ═══════════════════════════════════════════════════════════════════════════════
/// <summary>Сначала план миграции блока — апрув пользователя — потом перенос.</summary>
public class Migration_S0_PlanFirst : ISdaidDocumentSection
{
public Type Document => typeof(HowToMigrateMdToSdaid);
public Type Parent => typeof(object);
public int Order => 5;
public string Number => "";
public string Name => "Сначала план — потом перенос";
public string Body =>
"""
**Перед миграцией любого блока** (vision, эпик, домен, ADR, инфраструктурный контракт) — построить и показать пользователю **план переноса**:
- какие исходные файлы и разделы охватывает блок;
- какие SDAID-классы из них получатся (имя класса, kind-маркер, в какую папку);
- какие места не очевидно куда переносить — **явно выписать как открытые вопросы**;
- какие новые kind-маркеры / расширения нотации потребуются.
**Только после апрува пользователя** — начинать создавать файлы. Это даёт:
1. **Меньше переделок.** Лучше обсудить раскладку 5 минут и переделать 1 файл, чем построить 30 и переразложить.
2. **Видимость решений на границах.** Если для какого-то артефакта не находится подходящего kind-маркера или подходящей папки — это нормально, остановись и спроси. Молчаливый fallback на «`ISdaidDocument` без подтипа» или на ближайшую папку — антипаттерн.
3. **Сохранение «языка команды».** План показывает, какие исходные формулировки попадают в какие классы. Если что-то теряется или объединяется — пользователь это видит до того, как стало fait accompli.
### Формат плана
Минимум: markdown-таблица с колонками **Источник** | **SDAID-класс** | **Kind-маркер** | **Путь в проекте** | **Открытые вопросы**.
Для маленьких блоков (один use-case) — таблицы на 10–20 строк. Для пилотного эпика — таблица на 50–100 строк. Не страшно — это инвестиция в скорость следующих шагов.
### Где спрашивать
Любая из следующих ситуаций — **остановка + вопрос пользователю**:
- Не нашёлся подходящий kind-маркер в нотации (см. `SdaidDocumentKinds.cs` + методологию, раздел «Что куда писать / структура папок»).
- Несколько вариантов размещения (например, сервис относится и к приёму данных из АИС, и к расчёту — где?).
- Конфликт между источниками (один и тот же артефакт описан в двух md-файлах с разными формулировками — типично для `ntr-01/` и `ntr-02/`).
- Несоответствие между источником и текущей методологией.
""";
public string Version => "";
public string Status => "";
public string[] Comments => new string[0];
}
// ═══════════════════════════════════════════════════════════════════════════════
// 1. Главный принцип
// ═══════════════════════════════════════════════════════════════════════════════
/// <summary>Принцип: текст дословно, структура моя; нотация расширяется, текст — нет.</summary>
public class Migration_S1_Principle : ISdaidDocumentSection
{
public Type Document => typeof(HowToMigrateMdToSdaid);
public Type Parent => typeof(object);
public int Order => 10;
public string Number => "";
public string Name => "Главный принцип";
public string Body =>
"""
**Преобразовать структуру, не редактируя текст.** Команда и заказчик общаются формулировками из спеки. Задача в трекере говорит «реализовать FR-301 — импорт данных из АИС». Если миграция перепишет FR-301 «своими словами» (даже короче или яснее), задача потеряет связь с текстом, обсуждения станут двусмысленными.
### Что — структурное (зона ИИ-ассистента)
* Имена классов: `FR_301_AisDataImport`, `EnergyProfile`, `S_DocumentPurpose`, `BP_03_ConsumptionModel`.
* Иерархия секций через `Parent => typeof(SomeSection)`, порядок через `Order`.
* Идентификаторы и коды на самих concrete-классах: `public static string Code => "FR-301"`, `public static string Name => "Энергопрофиль"`. На интерфейсах их нет — это была бы либо `static abstract` (ломает использование интерфейса как type argument, CS8920), либо instance + `new()`/рефлексия в хелперах (мешает читать спеку).
* Типизированные ссылки — через прямое обращение к статикам concrete-класса: `SdaidAnchors.RefTo<FR_301_X>(FR_301_X.Code)`, `GlossaryAnchors.RefTo<SomeTerm>(SomeTerm.Name)`, `SomeTerm.Definition`. Класс упомянут дважды (как type argument и как owner статика) — это то, что ловит компилятор при F2-rename.
* `<summary>` XML-doc над классами — служебные, на сайте не отображаются.
* Структурные обёртки-секции (например, `S_Introduction` с пустым телом, повторяющая в дереве заголовок «1. Введение» исходника).
### Что — содержательное (зона авторов спеки)
* `Name`, `Description`, `Body`, `Definition`, `Summary`, `AcceptanceCriteria` — всё, что пользователь видит в рендере.
* Любые prose-абзацы, таблицы (контент ячеек), списки.
* **Опечатки исходника.** Оставлять как есть. Это часть исторической записи; правка — отдельное решение команды, не миграции.
* Markdown-ссылки на старые пути и Obsidian-ссылки `[[...]]` — оставлять дословно; они «битые» сейчас, починятся когда мигрируем целевой раздел и заменим на `nameof(...)`.
### Если содержание не помещается в текущую нотацию
**Обсудить с пользователем что делать. Не принимать никаких решений самостоятельно и безмолвно.** Если миграционный фрагмент не ложится на существующий контракт (нет подходящего kind-маркера, нет helper-а, нет поля в интерфейсе) — остановиться, сформулировать гэп одним абзацем и вынести пользователю. Возможные варианты решения (расширить нотацию, добавить helper, изменить генератор, пересмотреть структуру) — выбирает **пользователь**.
**Расширить нотацию или генератор, а не перефразировать источник.** Когда направление согласовано с пользователем — выполнить расширение, не подгонять текст. Соблазн «упростить» текст чтобы он «влез» в текущую нотацию — антипаттерн: один раз позволил себе перефразировать — и весь язык команды поплыл.
""";
public string Version => "";
public string Status => "";
public string[] Comments => new string[0];
}
// ═══════════════════════════════════════════════════════════════════════════════
// 2. Чтение источника перед началом
// ═══════════════════════════════════════════════════════════════════════════════
/// <summary>Перед написанием первого класса — медленно прочитать и описать источник.</summary>
public class Migration_S2_ReadFirst : ISdaidDocumentSection
{
public Type Document => typeof(HowToMigrateMdToSdaid);
public Type Parent => typeof(object);
public int Order => 20;
public string Number => "";
public string Name => "Чтение источника и инвентаризация";
public string Body =>
"""
Прежде чем создать первый класс — прочитать **весь** исходный документ или папку и составить таблицу артефактов: какие H1/H2/H3-заголовки, какие таблицы, какие ссылки между файлами. Без этой инвентаризации легко:
* Пропустить раздел (и потом восстанавливать после замечания пользователя).
* Создать дубликаты (один термин описан и в `ntr-01/domain/terms/`, и внутри спеки раздела — с разными определениями; нужно решение команды, какой канон).
* Неправильно расставить иерархию (раздел с подпунктами 4.4.1–4.4.4 — каждый адресуется отдельно).
### Где лежит источник
| Источник | Что это |
|---|---|
| `rugrid-devdocs/service-enter/ntr-01/` | спецификация первого этапа: домен, use-cases, UI, роли, отчёты, прототип, интеграция |
| `rugrid-devdocs/mver-module/tech-spec/` | тех-спека модуля MVER |
| `rugrid-ntr/docs/*.md` | точечные спеки в самом репозитории кода (импорт из АИС, модель потребления, безопасность) |
`ntr-02/` **источником не является** — второй этап специфицируется сразу в SDAID (см. описание документа).
### Что выписать на этапе чтения
* Все H1/H2/H3 с их прямым текстом — это будущие `Name`-ы секций.
* Таблицы и их колонки — будущий формат markdown-таблиц в `Body` секций.
* Идентификаторы артефактов: FR-NNN, БП-N, ADR-N — нужны для `Code` / выбора kind-маркера.
* Внешние ссылки: на другие md-файлы, на файлы исходников, на enum-ы — пометить как «будущая трассировка через `nameof`».
* Obsidian-специфика: wiki-ссылки `[[...]]`, `_assets/`-вложения, dataview-блоки — пометить, как переносим (обычно: текст дословно, ссылку чиним позже).
* Опечатки и стилистические странности — **подсветить** и оставить.
* Конфликты: один и тот же термин с разными определениями в разных файлах. Не решать в одиночку — вынести команде.
### Когда останавливаться и спрашивать
* Источник имеет неоднозначность (одно слово в двух местах с разными смыслами).
* Два файла содержат «одно и то же» с дрейфом формулировок (типично для `ntr-01/` ↔ `rugrid-ntr/docs/`, где спека уточнялась по ходу реализации).
* Источник ссылается на артефакт, которого ещё нет в новой структуре.
Лучше один вопрос пользователю до начала, чем 30 файлов с silent decisions, которые придётся пересматривать.
""";
public string Version => "";
public string Status => "";
public string[] Comments => new string[0];
}
// ═══════════════════════════════════════════════════════════════════════════════
// 3. Маппинг markdown-структуры в классы
// ═══════════════════════════════════════════════════════════════════════════════
/// <summary>Соответствие markdown-конструкций классам нотации.</summary>
public class Migration_S3_Mapping : ISdaidDocumentSection
{
public Type Document => typeof(HowToMigrateMdToSdaid);
public Type Parent => typeof(object);
public int Order => 30;
public string Number => "";
public string Name => "Маппинг markdown → SDAID-классы";
public string Body =>
"""
### Базовое соответствие
| Markdown-конструкция | SDAID-конструкция | Куда дословный текст |
|---|---|---|
| Заглавный `# H1` файла | `Name` документа (`ISdaidDocument` или подтип) | в `Name` |
| Преамбула под H1 (до первого H2) | `Description` документа | в `Description` |
| `## H2` раздел | `ISdaidDocumentSection` с `Parent => typeof(object)` | заголовок → `Name`, содержимое → `Body` |
| `### H3` подраздел | `ISdaidDocumentSection` с `Parent => typeof(ParentSection)` | то же |
| `#### H4` и глубже | секция с `Parent => typeof(H3Section)` | то же |
| Markdown-таблица | в `Body` секции, как есть | вся таблица в `Body` |
| Список (bullet/numbered) | в `Body` секции, как есть | весь список в `Body` |
| Code fence | в `Body`, как есть | весь блок |
| `> blockquote` | в `Body`, как есть | весь блок |
| Mermaid-диаграмма | в `Body`, как есть (генератор рендерит) | весь блок |
### Заголовок секции и нумерация
Markdown-источник часто содержит ручную нумерацию: `### 1.1. Назначение документа`. В `Name` секции **префикс «1.1.» не включать** — генератор нумерует секции автоматически по `Order`/иерархии. `Name => "Назначение документа"` достаточно. Это **не** перефразирование: «1.1.» — это структура (генератор её восстанавливает), а «Назначение документа» — содержание (оно сохраняется буквально).
### Обёрточные секции с пустым телом
Если в источнике под `## H2` сразу идут `### H3` без preamble (например, «## 1. Введение» → сразу «### 1.1.»), создаётся обёрточная секция с `Body => ""`. Это сохраняет иерархию и группировку, не добавляя своего текста.
### Маркеры kind документов
Для документа выбирается подтип `ISdaidDocument` по смыслу:
* Видение проекта → `IVisionDocument`
* Эпик/инициатива → `IEpicDocument`
* Use Case (сквозной сценарий пользователя) → `IUseCaseDocument`
* Архитектурное решение → `IAdrDocument`
* Функциональное требование как отдельный документ → `IFunctionalRequirementDocument`
* Страница UI → `IPageDocument`; переиспользуемый компонент → `IUiComponentDocument`
* Доменная сущность → `IDomainEntityDocument`
* Обзор доменного модуля → `IBoundedContextModuleDocument`
* API-контракт операции (endpoint + command/query + result) → `IApiContractDocument`
* Handler (canonical narrative операции) → `IHandlerDocument`
* Доменный сервис → `IDomainServiceDocument`; прикладной сервис уровня реализации → `IApplicationServiceDocument`
* State machine → `IStateMachineDocument`
* Инфраструктурный контракт → `IInfrastructureDocument`
* Батч замечаний/пожеланий → `IChangeRequestDocument`
* Обзорный документ подсистемы (карта кода, деплой-единица) → `IApplicationDocument`
**Если ни один из существующих kind-маркеров не подходит — обратиться к пользователю, не решать самостоятельно.** Описать документ-источник в один абзац (о чём он, как используется командой, на каком уровне иерархии живёт) и предложить варианты:
* Использовать `ISdaidDocument` без подтипа — если документ действительно «просто структурный» и не требует kind-специфичного рендеринга / индекса.
* Завести новый kind-маркер в нотации — если документ-категория повторяется и заслуживает собственного landing-page по типу.
Решение о направлении принимает **пользователь**. Безмолвный fallback на «`ISdaidDocument` без подтипа» — это та же ошибка, что и перефразирование текста.
### Функциональные требования: прогрессивная проработка vision
FR-таблица в исходном vision — это в большинстве случаев **просто текст**: ID, краткая формулировка, приоритет, покрытие. На старте миграции она **не материализуется** в отдельные классы-требования:
* Vision-секция группы (5.1, 5.2, …) — обычная `ISdaidDocumentSection`, в `Body` лежит **дословная markdown-таблица** из источника. Ячейка ID — пока **plain-текст** «FR-301».
* FR не существует как отдельная сущность, пока команда не решила его проработать.
**Материализация = создание класса.** Когда команда берётся за конкретный FR:
1. Создаётся отдельный `IFunctionalRequirementDocument`-класс (например, `FR_301_AisDataImport`) в `30_Requirements/`. Только теперь у требования есть собственная страница, на которой его можно расписать (секции, AC) и трассировать.
2. В соответствующей vision-таблице **plain-текст «FR-301» заменяется на ссылку** на этот документ (через typed-helper).
3. FR попадает в kind-index требований, в матрицу трассируемости, в auto-linkify.
Зачем так:
* **Vision не «рассыпается».** Чтобы прочитать что система должна уметь, читатель открывает Vision и видит ровно те же таблицы, что в исходнике.
* **Проработка видна структурно.** Глядя на таблицу, видно сразу: какие ID — текст, какие — кликабельные ссылки. Это карта зрелости проработки.
* **Создание не опережает проработку.** Класс заводится тогда, когда есть что записать в его секции.
### Антипаттерн: предварительная материализация всех FR
Создать 40 классов-FR на этапе миграции «чтобы все были адресуемые» — это **антипаттерн**. Все 40 будут пустыми, без описания, без AC, без связей. Vision загромождается, обещание SDAID «структурированная проработка» подменяется «структурированными заглушками».
""";
public string Version => "";
public string Status => "";
public string[] Comments => new string[0];
}
// ═══════════════════════════════════════════════════════════════════════════════
// 4. Один источник правды
// ═══════════════════════════════════════════════════════════════════════════════
/// <summary>Каждый артефакт живёт в одном классе; таблицы — это views поверх классов.</summary>
public class Migration_S4_SingleSourceOfTruth : ISdaidDocumentSection
{
public Type Document => typeof(HowToMigrateMdToSdaid);
public Type Parent => typeof(object);
public int Order => 40;
public string Number => "";
public string Name => "Один источник правды";
public string Body =>
""""
### Правило
Каждая единица содержания — один термин, одно требование, один эпик — живёт **в одном классе**. Если markdown-источник содержит таблицу, которая повторяет описания терминов или требований — таблица в SDAID становится **view**, ячейки которого **подтягиваются** из классов-источников через типизированные хелперы.
### Глоссарий
Каждый термин — один класс `IGlossaryTerm`:
* `Name` — каноническое отображаемое имя (дословно из источника, на родном языке)
* `Definition` — определение (дословно из источника)
* `Aliases`, `ForbiddenSynonyms` — пустые массивы, если не было в источнике
Сводная таблица в Vision-документе или в любом другом месте **не дублирует** определения — она их **рендерит**, обращаясь к статикам concrete-классов терминов напрямую:
```csharp
public string Body => $@"
| Термин | Определение |
|---|---|
| {GlossaryAnchors.RefTo<EnergyProfile>(EnergyProfile.Name)} | {EnergyProfile.Definition} |
";
```
* `Term.Name`, `Term.Summary`, `Term.Definition` — `public static string` на самом классе термина. Доступны без `new T()` и без рефлексии.
* `GlossaryAnchors.RefTo<T>(string)` — строит `[displayText](URL страницы термина T)`. Display-текст передаётся явно.
Класс термина упомянут дважды (как type argument и как owner static-а) — это сознательная избыточность: F2-rename переименовывает оба упоминания компилятором, а читатель видит, откуда приходит каждое значение.
### Функциональные требования
Каждый FR-класс несёт `public static string Code => "FR-301"` (на самом маркере `IFunctionalRequirement` ничего нет — интерфейс маркер-структурный). Сводная таблица — view, ссылается на FR прямым вызовом:
```csharp
| {SdaidAnchors.RefTo<FR_301_AisDataImport>(FR_301_AisDataImport.Code)} | <требование дословно> | MVP | <покрытие дословно> |
```
Если FR-301 переименуют в FR-401 (изменив `Code`), все таблицы перестроятся автоматически — display-текст приходит из самого класса, а не из строкового литерала, который пришлось бы искать grep-ом.
### Анти-паттерн: дублирование текста
Если строка из исходника попала в `Body` **сводной таблицы** и одновременно в `Body` соответствующей **детальной секции** — это уже дрейф потенциального источника правды. Решение: таблица показывает короткое (Code, Name) через helper; детали (текст требования) — в детальной секции. Если повторяется текст в `Name` секции и в `Body` — нормально (заголовок + описание); если повторяется текст в `Body` двух разных секций — пересмотреть.
"""";
public string Version => "";
public string Status => "";
public string[] Comments => new string[0];
}
// ═══════════════════════════════════════════════════════════════════════════════
// 5. Типизированные ссылки
// ═══════════════════════════════════════════════════════════════════════════════
/// <summary>Хелперы для ссылок: что и когда использовать.</summary>
public class Migration_S5_TypedLinks : ISdaidDocumentSection
{
public Type Document => typeof(HowToMigrateMdToSdaid);
public Type Parent => typeof(object);
public int Order => 50;
public string Number => "";
public string Name => "Типизированные ссылки";
public string Body =>
"""
### Когда какой helper
| Сценарий | Вызов | Что возвращает |
|---|---|---|
| Ссылка на термин с его каноническим именем | `GlossaryAnchors.RefTo<T>(T.Name)` | `[Термин](URL)` |
| Ссылка на термин со склонённым display-текстом | `GlossaryAnchors.RefTo<T>("термина")` | `[термина](URL)` |
| Вставить определение термина как значение | `T.Definition` | текст определения как `string` |
| Ссылка на FR-секцию с её кодом | `SdaidAnchors.RefTo<FR_301_X>(FR_301_X.Code)` | `[FR-301](#section-anchor)` |
| Ссылка на произвольную секцию того же документа | `SdaidAnchors.RefTo<T>("text")` | `[text](#section-anchor)` |
| Ссылка на другой документ / chunk | `SdaidAnchors.RefTo<T>()` / `SdaidAnchors.ChunkRefTo<TDoc>(nameof(TDoc.Chunk), "текст")` | cross-page ссылка |
| Ссылка на значение enum | `SdaidAnchors.RefToEnum(SomeEnum.Value)` | ссылка на якорь значения |
| Mermaid-click на секцию | `SdaidAnchors.MermaidClickTo<T>("node")` | директива `click ...` |
| Голый anchor-slug секции (для кастомной разметки) | `SdaidAnchors.AnchorOf<T>()` | `section-...` без `#` |
| Упоминание любого артефакта в прозе | `nameof(SomeType)` | display = TypeName, link через auto-linkify |
| Встроить исходник класса | `SdaidCode.OfClass<T>()` / `SdaidCode.OfClass(typeof(T))` | code-block в рендере |
| Встроить HTML-прототип | `SdaidHtml.IframeOf<TDoc>("file.html")` | iframe в рендере |
### Почему класс упомянут дважды
В `SdaidAnchors.RefTo<FR_301_X>(FR_301_X.Code)` имя класса встречается и как type argument, и как owner static-поля. Это **сознательная избыточность**:
* Type argument нужен генератору для построения URL/anchor.
* `FR_301_X.Code` — обращение к идентификатору на самом классе. Не строковый литерал.
F2-rename переименовывает оба упоминания одним движением. Опечатка ловится компилятором. Скрыть второе упоминание за дженериком нельзя без рефлексии или `static abstract`, оба варианта отвергнуты: рефлексия мешает ИИ читать спеку, `static abstract` блокирует использование интерфейса как type argument в `IFolderForKind<TKind>` (CS8920).
### Когда строковый display оправдан
* Склонение слова в прозе.
* Ссылка с пояснением, не равным `Name`: `SdaidAnchors.RefTo<ADR_017_X>("решение о вынесении CSS")`.
* Mermaid-node, чьё имя на диаграмме отличается от `Name` целевой секции.
### Если для нужного kind ещё нет статического идентификатора
Завести `public static string Code => "…"` или `public static string Name => "…"` на самом concrete-классе (kind-маркер на интерфейсе **не нужен**) и обращаться напрямую.
""";
public string Version => "";
public string Status => "";
public string[] Comments => new string[0];
}
// ═══════════════════════════════════════════════════════════════════════════════
// 6. Антипаттерны
// ═══════════════════════════════════════════════════════════════════════════════
/// <summary>Конкретные ошибки, отработанные на предыдущих миграциях.</summary>
public class Migration_S6_AntiPatterns : ISdaidDocumentSection
{
public Type Document => typeof(HowToMigrateMdToSdaid);
public Type Parent => typeof(object);
public int Order => 60;
public string Number => "";
public string Name => "Антипаттерны";
public string Body =>
"""
Эти ошибки реально делались при миграции md-спеки в SDAID и явно отмечались пользователем. Сохранены здесь чтобы не повторять.
### ❌ Перефразирование «для ясности»
**Симптом:** В `Body` секции написано «своими словами» вместо дословной цитаты из источника. Часто появляется как «давайте сделаем пояснее», «уберём повтор», «структурируем».
**Цена:** Команда теряет язык. Поиск по точной фразе в репо не работает. Заказчик говорит «реализовать FR-301 как написано в спеке» — а в спеке теперь другая формулировка.
**Лечение:** Каждый раз перед тем как написать prose в `Body` — открыть источник и **скопировать** соответствующий фрагмент. Если не получается скопировать буквально — что-то не так с маппингом.
### ❌ Изобретение разделов и ролей
**Симптом:** Добавление секции, которой в источнике нет, — «мне показалось логичным выделить».
**Цена:** Документ показывает читателю несуществующий контракт. Любой, кто откроет источник для сверки, не найдёт этой секции там.
**Лечение:** Если хочется добавить секцию-обёртку — спросить себя: была ли она в источнике? Если нет — не добавлять. Структура SDAID должна один-в-один отражать структуру источника.
### ❌ Тихое опускание разделов
**Симптом:** Пропустил раздел «Глоссарий», потому что термины уже мигрировал в отдельные `IGlossaryTerm`-классы и подумал «зачем дублировать».
**Цена:** Документ читается как пересказ, не как перенос. Раздел из источника просто исчезает, и читатель не знает почему.
**Лечение:** Каждый H2/H3 раздел получает SDAID-секцию. Если он содержательно перекочевал в другое место (как глоссарий → отдельные term-классы), секция всё равно создаётся — её `Body` становится **view**, подтягивающий данные из новых источников.
### ❌ Преждевременная материализация всех FR в классы
**Симптом:** На этапе миграции каждый FR-NNN из таблицы Vision стал отдельным классом-требованием. Vision наполнился десятками «фантомных» сущностей, по которым никто пока не работал.
**Цена:** Целостный взгляд на систему заменён грудой пустых страниц-обёрток; на kind-index «Все FR» — десятки пунктов без содержания.
**Лечение:** На этапе миграции **FR-таблица в Vision остаётся обычной markdown-таблицей**. Класс заводится только тогда, когда FR действительно прорабатывается.
### ❌ Row-specific хелперы вместо value-cell хелперов
**Симптом:** Завёл `private static string Row<T>()`, формирующий целую строку таблицы.
**Цена:** Хелпер привязан к конкретному формату таблицы. Захочется добавить колонку — придётся писать `Row2<T>` или менять интерфейс.
**Лечение:** Содержимое отдельных ячеек строится прямым обращением к статикам concrete-классов. Структура таблицы (количество колонок, порядок) — markdown в `Body`, не в хелпере.
### ❌ Строковый display, когда у типа есть идентификатор
**Симптом:** `SdaidAnchors.RefTo<FR_101_X>("FR-101")` — строка рядом с типом.
**Цена:** Если код FR изменится, строка рядом не обновится — drift. Опечатка в строке — компилятор пропускает.
**Лечение:** Завести на самом concrete-классе `public static string Code => "FR-101"` и использовать `SdaidAnchors.RefTo<FR_101_X>(FR_101_X.Code)`.
### ❌ «Заглушка» вместо настоящего класса термина
**Симптом:** Создал `IGlossaryTerm`-класс с `Definition`, написанным «по смыслу», потому что показалось, что сойдёт.
**Цена:** Полный набор терминов выглядит готовым, но определения не совпадают с источником ни в одном символе.
**Лечение:** Либо переносим термин с дословным определением, либо **не создаём класс**. Лучше отсутствие термина, чем фабрикация.
""";
public string Version => "";
public string Status => "";
public string[] Comments => new string[0];
}
// ═══════════════════════════════════════════════════════════════════════════════
// 7. Расширение нотации vs перефразирование
// ═══════════════════════════════════════════════════════════════════════════════
/// <summary>Когда обнаруживается разрыв нотации — расширяем, не подгоняем текст.</summary>
public class Migration_S7_ExtendDoNotRephrase : ISdaidDocumentSection
{
public Type Document => typeof(HowToMigrateMdToSdaid);
public Type Parent => typeof(object);
public int Order => 70;
public string Number => "";
public string Name => "Расширение нотации vs перефразирование";
public string Body =>
"""
Главный сигнал расширить нотацию — когда ловишь себя на мысли «давайте я тут чуть-чуть переформулирую чтобы влезло». Если есть искушение перефразировать — почти всегда правильный ход: остановиться и расширить нотацию/генератор.
### Шаблон расширения
Когда обнаружен очередной разрыв:
1. Сформулировать гэп в один абзац: что именно не получается выразить.
2. **Вынести пользователю** — описать гэп, перечислить возможные варианты решения (новое поле в интерфейсе / новый маркер / новый helper / изменение генератора / иначе), назвать trade-off-ы каждого. **Решение о направлении принимает пользователь.** Никаких молчаливых «я подумал и решил».
3. После согласования: добавить минимальное расширение в `Rugrid.Sdaid.Notation` (новое свойство интерфейса, новый маркер, новый helper).
4. Если генератор должен диспатчить по этому расширению — добавить ветку в `SpecReader` / `PageGenerator` проекта `Rugrid.Sdaid.Generator`.
5. Зафиксировать расширение в **этом** документе (список ниже) — чтобы следующая итерация миграции опиралась на накопленный опыт.
### Журнал расширений нотации
Пока пуст. Каждое согласованное расширение добавляется сюда одним блоком: **Гэп** (что не выразилось) → **Расширение** (что добавили в нотацию/генератор).
### Что уже известно из опыта донорского проекта
Нотация в её текущем виде уже вобрала пять расширений, сделанных на предыдущей миграции: собственный `SourcePagePath` у секции; маркер `IFunctionalRequirement` (чтобы FR-секция, а не только FR-документ, попадала в индекс и матрицу трассируемости); `Code`/`Name` как статики на concrete-классе вместо строкового display; `GlossaryAnchors.RefTo<T>` для cross-page ссылок на страницы терминов; разведение sidebar-папок и kind-маркеров (`IFolderForKind` требует `TKind : ISdaidDocument`, поэтому глоссарий группируется генератором напрямую, а FR-секции — через faceted kind-index).
**Открытый вопрос, унаследованный от донора:** обобщить `IFolderForKind`, чтобы он работал для произвольных маркеров (`IRequirement`, `IGlossaryTerm`), либо завести параллельную концепцию `IFolderForMarker`.
""";
public string Version => "";
public string Status => "";
public string[] Comments => new string[0];
}
// ═══════════════════════════════════════════════════════════════════════════════
// 8. Фазы миграции
// ═══════════════════════════════════════════════════════════════════════════════
/// <summary>Рекомендуемый порядок фаз миграции md-спеки в SDAID.</summary>
public class Migration_S8_Phases : ISdaidDocumentSection
{
public Type Document => typeof(HowToMigrateMdToSdaid);
public Type Parent => typeof(object);
public int Order => 80;
public string Number => "";
public string Name => "Фазы миграции";
public string Body =>
"""
Не пытаться смигрировать всё разом. Лучше тонкая вертикальная нить, валидирующая методологию end-to-end, потом утолщение по слоям.
### Фаза 0: Подготовка
* Прочитать нотацию (контракты, kind-маркеры, helpers) — без неё не получится.
* Прочитать этот HowTo + методологию проекта (`00_Methodology/Methodology.cs`).
* Прочитать **весь** markdown-источник и составить инвентарь (см. секцию о чтении источника).
* Проект `Rugrid.Sdaid.Epas.Ntr.csproj` и базовая структура папок — **уже созданы**, `Folders.cs` с sidebar-папками заведён.
### Фаза 1: Vision + Глоссарий + FR
Самая «горизонтальная» часть.
1. **Vision-документ** (`IVisionDocument`) — **один на проект**, с секциями для каждого H2/H3 исходника. Дословный текст. Второй этап не заводит own-vision: новые эпики дописываются в тот же документ и в `60_Epics/`.
2. **Глоссарий** — по одному `IGlossaryTerm`-классу на каждый термин. Если в исходнике несколько мест с определениями и они отличаются — **спросить пользователя**, какой канон.
3. **Раздел Vision-глоссарий** становится view-секцией: ячейки строятся прямыми обращениями `GlossaryAnchors.RefTo<T>(T.Name)` и `T.Definition`.
4. **FR-таблицы Vision** — секции групп содержат **дословные markdown-таблицы** из источника. **Никаких FR-классов на этом этапе не создаётся** (прогрессивная проработка).
После этой фазы генератор показывает полный Vision со всеми разделами, кликабельную FR-сводку, страницы терминов. Это валидация, что нотация подходит проекту.
### Фаза 2: Пилотный эпик end-to-end
Выбрать один эпик (желательно самый зрелый, со всеми артефактами: use-case, state-machine, страницы, endpoints, handlers, services, ADR). **Перед миграцией построить и согласовать план.** Затем мигрировать целиком:
| Артефакт | Kind-маркер | Куда (слой) |
|---|---|---|
| Эпик-overview | `IEpicDocument` | `60_Epics/<E>/` |
| Use Case | `IUseCaseDocument` (с `PrimaryActor`) | `60_Epics/<E>/UseCases/<UC>/` |
| Endpoint + Command/Query + Result | `IApiContractDocument` | `45_Api/Endpoints/<Area>/` |
| Handler (canonical narrative) | `IHandlerDocument` | `55_Backend/Handlers/<Area>/` |
| Доменный / прикладной сервис | `IDomainServiceDocument` / `IApplicationServiceDocument` | `55_Backend/Services/<Area>/` |
| State machine | `IStateMachineDocument` | `60_Epics/<E>/UseCases/<UC>/StateMachines/` |
| Страница UI | `IPageDocument` | `65_Ui/Pages/<Area>/` |
| Локальный R-N эпика | `IAdrDocument` | `60_Epics/<E>/Adr/` |
**Что НЕ делаем на этом этапе:**
* **Реализационные артефакты не кладём под use-case.** Endpoint/handler/страница живут в своих слоях; use-case ссылается на них через `nameof()`.
Цель фазы — нащупать, что ещё не помещается в нотацию (вероятно, всплывут 1–2 новых расширения).
### Фаза 3: Domain + остальные эпики + кросс-проектные ADR
* **Domain.** Каждая сущность размещается в своём доменном модуле `50_Domain/<Module>/`. Сущностей, рождающихся в use-case-е, **не бывает**. Каждый модуль получает `IBoundedContextModuleDocument`-overview (`BC_<Module>.cs`) и `IDomainEntityDocument`-классы для сущностей и агрегатов.
* **Кросс-проектные ADR** — `40_Arch/ADR/`, `40_Arch/Tech/<Stack>/`.
* **Инфраструктурные контракты** — `70_Infrastructure/<Capability>/` (по необходимости).
* **Особенности реализации подсистем** — `80_Subsystems/<S>/` (по необходимости).
Циклически по эпикам; после пилота темп предсказуем.
### Фаза 4: Зачистка и валидация
* Проверить, что **все** разделы каждого исходного md-файла отражены в SDAID (инвентарь из фазы 0 — чеклист).
* Проверить, что таблицы в источниках стали view-секциями, а не дубликатами.
* Найти оставшиеся ссылки на старые md-пути и заменить на `nameof()`-упоминания там, где целевые классы уже есть.
* Прогнать генератор, открыть сайт, ткнуть в каждый раздел — рендерится ли как ожидалось.
### Фаза 5: Сверка с реализацией
Перенос закончен там, где спека совпала с источником. Но источник писался до и во время
реализации, поэтому код уже разошёлся со спекой в деталях. Эта фаза **находит расхождения
и предъявляет их пользователю** — и только.
**Ничего не синхронизируется молча.** Ни спека под код, ни код под спеку: методология
прямо запрещает тихую синхронизацию. Результат фазы — список, а не правки.
Что сверяем, блок за блоком:
- **Домен** — состав полей сущности из таблиц источника против `src/Ntr.Domain` (лишние и
отсутствующие поля, разошедшиеся типы и обязательность).
- **Методика расчёта** — множество `methodSectionId` в `55_Backend/Calc/` против
XML-doc калькуляторов в `src/Ntr.Calc` (секции только в спеке / только в коде).
- **UI** — страницы и компоненты против маршрутов и компонентов `src/Ntr.Web`
(пункты меню, вкладки, claim-и доступа).
- **API и backend** — endpoint-ы и хендлеры против контроллеров и CQRS-классов `src/`.
Каждое расхождение — строка в `SPEC_VS_IMPL_ntr-01.md`: где в спеке, где в коде, в чём
разница, и **пусто в колонке решения** — её заполняет пользователь. После его решения
расхождение либо превращается в правку спеки, либо в задачу на код, либо в открытый
вопрос эпика.
### Признаки готовности
* Все `Name`-ы, `Description`, `Body`, `Definition` — дословные цитаты из исходного md.
* Все типизированные ссылки — через `SdaidAnchors.RefTo<T>(T.Code)` / `GlossaryAnchors.RefTo<T>(T.Name)` / `nameof(T)`. Строковых ссылок с дублированием идентификаторов — нет.
* Каждый артефакт встречается в одном классе; таблицы — это views.
* Если в нотации обнаруживались разрывы — они закрыты расширением, не перефразированием, и записаны в журнал расширений.
""";
public string Version => "";
public string Status => "";
public string[] Comments => new string[0];
}
// ═══════════════════════════════════════════════════════════════════════════════
// 8.5. MIGRATION_MAP.md — обязательно поддерживать при каждой миграции
// ═══════════════════════════════════════════════════════════════════════════════
/// <summary>Карта md → cs, обязательное обновление при любой миграции.</summary>
public class Migration_S85_MigrationMap : ISdaidDocumentSection
{
public Type Document => typeof(HowToMigrateMdToSdaid);
public Type Parent => typeof(object);
public int Order => 85;
public string Number => "";
public string Name => "MIGRATION_MAP.md — карта md → cs";
public string Body =>
"""
Рядом с этим HowTo лежит `MIGRATION_MAP.md` — таблица соответствия исходных markdown-файлов (`rugrid-devdocs/service-enter/`, `rugrid-ntr/docs/`) и перенесённых SDAID-классов в `sdaid/Rugrid.Sdaid.Epas/`.
**Назначение карты:** в исходных файлах реализации (`src/`) и в задачах долгое время сохраняются ссылки на старые md-пути. После миграции документ живёт по новому пути, а ссылки в потребителях надо переключать. Карта даёт быстрый lookup «куда переехал этот md».
## Обязательное требование
**Любая миграция md-файла → SDAID-класс ОБЯЗАТЕЛЬНО фиксируется строкой в `MIGRATION_MAP.md` в той же сессии**, до сборки + регенерации сайта.
- Один md-файл → одна строка в соответствующей секции (эпик / ADR / методология / …).
- Если переносится несколько md-файлов в один SDAID-класс (объединение) — несколько строк с одним и тем же значением в правой колонке.
- Если один md-файл разделяется на несколько SDAID-классов — несколько строк с одним и тем же значением в левой колонке.
- Новая секция верхнего уровня (например, новый эпик, новая категория ADR) — новый заголовок `## …` в карте.
## Что НЕ делать
- **Не оставлять обновление карты «на потом».** Если миграция блока завершена без правки карты — это незаконченная миграция. Текущая сессия — единственная, в которой соответствие md → cs ещё свежо в голове.
- **Не дублировать содержимое карты в других местах.** Карта — единственный источник правды для md → cs.
- **Не описывать в карте «причины» переноса** — это не журнал миграции, а компактный lookup-индекс. Решения о структуре фиксируются в методологии / документе эпика.
## Чеклист на конец миграции блока
Перед `dotnet build` + регенерацией HTML после миграции блока:
1. Открыта `MIGRATION_MAP.md`.
2. Для каждого нового cs-файла в блоке — есть строка в соответствующей таблице.
3. Если блок целиком новый (новый эпик / новая категория) — есть отдельный `## …`-заголовок над таблицей.
""";
public string Version => "";
public string Status => "";
public string[] Comments => new string[0];
}
// ═══════════════════════════════════════════════════════════════════════════════
// 8.6. Миграция доменных сущностей
// ═══════════════════════════════════════════════════════════════════════════════
/// <summary>Конвенция миграции доменных сущностей и их обвязки в доменный модуль.</summary>
public class Migration_S86_DomainEntities : ISdaidDocumentSection
{
public Type Document => typeof(HowToMigrateMdToSdaid);
public Type Parent => typeof(object);
public int Order => 86;
public string Number => "";
public string Name => "Доменные сущности — модуль, DOC_ + класс, SdaidCode";
public string Body =>
""""
Сводный рецепт переноса доменной модели (описания из `ntr-01/domain/*`, реальные сущности из `src/Ntr.Domain`) → `50_Domain/<Module>/<Entity>.cs`.
## Структура папок и namespace
```
50_Domain/
<Module>/
BC_<Module>.cs ← IBoundedContextModuleDocument (overview модуля)
<Entity1>.cs ← один файл = DOC_<Entity1> + <Entity1> class
<Entity2>.cs
```
Namespace: `Rugrid.Sdaid.Epas.Ntr.Domain.<Module>`. Модуль выбирается по смыслу предметной области; при неоднозначности — вопрос пользователю, не молчаливый выбор.
## Один файл на сущность — два класса
Каждый файл `<Entity>.cs` содержит:
1. **`DOC_<Entity>`** — `IDomainEntityDocument, IFolder<NtrDomainFolder>`. Свободная проза, никаких обязательных секций. Поля:
- `public static string _refName = "Человеческое имя"` — для красивого display в `RefTo<DOC_X>()`.
- `Name`, `Description`, `Version`, `Status`, `Comments`.
- `Entity => typeof(<Entity>)` — парный класс-сущность.
- Произвольный набор `public static string <ChunkName>` со свободной прозой — каждый становится addressable chunk-ом `#chunk-<chunkname>`.
2. **`<Entity>`** — сам C#-класс сущности, помечен `[DomainEntity]`. На каждом FK-property — `[Relation]`. Остальное — как есть.
## Альтернатива: сущность-документ
Когда у сущности **пока нет готового C#-класса** (спека пишется вперёд реализации) или проза/требования по свойствам важнее, чем структура для tooling — сущность оформляется **как документ**, без парного `[DomainEntity]`-класса:
- Класс-документ помечен `IDomainEntityDocument, IFolder<NtrDomainFolder>`, `Entity => typeof(object)` (готового класса нет → генератор не подтягивает авто-структуру/исходник).
- **Секция-абзац верхнего уровня** — `public static string <Name> = "## Заголовок\n..."` (рендерится как chunk с якорем `chunk-<name>`).
- **Секция-свойство** — **nested-class с instance-свойством** `public string Body => "### Заголовок\n\n..."`. Только такой класс рендерится как раздел с якорем `chunk-<ИмяКласса>`, на который точно попадает `RefTo`. ⚠ `public static string Name`/`body` НЕ работают: bare-nested-класс — структурный контейнер, а не визуальный раздел.
- **Группировка** — контейнер-класс с `public string Body => "## Заголовок группы"`, внутри — секции-свойства.
- Опц. `public static string _refName = "…"` на классе секции — текст для `RefTo`-ссылки (поля с `_` генератор не рендерит как контент).
- Прозу, требования и особенности свойства пишем **в одном месте** (в его `Body`), не размазывая по XML-doc + отдельным chunk-ам.
**Что когда выбирать.** Документ-формат — для сущностей, описываемых спекой вперёд кода. Формат «класс + DOC» — когда нужен реальный `[DomainEntity]`-класс: сид-данные (`<Entity>SeedData`), `[Relation]`-диаграмма, компайл-сейфти `nameof(<Entity>.Field)`.
## Что переносить откуда — source-verbatim
Если исходник — C#-класс из `src/Ntr.Domain`, проза живёт **в XML-doc**: верхнеуровневый `<summary>`, `<remarks>`, per-property `<summary>`.
Правила переноса:
- **Description** документа = первое предложение верхнеуровневого `<summary>` дословно.
- **На классе остаётся** только это первое предложение + per-property XML-docs (они привязаны к свойствам, ценны для IDE-tooling).
- **Длинная проза из `<summary>`/`<remarks>` переносится в DOC_** как отдельные chunks. Каждый смысловой блок — своя `public static string`.
- **Текст переносится дословно**. Меняется ТОЛЬКО синтаксис формата:
- `<b>...</b>` → `**...**`
- `<c>...</c>` → backticks
- `<see cref="X"/>` → backticks `` `X` `` (не ссылки — это имена свойств/типов, не doc-артефакты)
- **Не выдумывать** разделов «Зачем нужна», «Откуда взялась», «Ключевые правила», если их нет в исходнике. Это главная ловушка миграции доменных сущностей.
## Встраивание исходника в прозу
В DOC_ обычно ставится финальный chunk:
```csharp
public static string Body = $"""
## Класс
{SdaidCode.OfClass<EntityName>()}
""";
```
- `SdaidCode.OfClass<T>()` — generic, для обычных классов.
- `SdaidCode.OfClass(typeof(StaticHelper))` — не-generic, для статических классов (seed-data, утилиты).
- `SdaidCode.OfMember<T>(nameof(T.X))` — для отдельного члена.
## Чеклист на одну сущность
1. Создать `50_Domain/<Module>/<Entity>.cs`.
2. Скопировать класс сущности из источника, проставить `[DomainEntity]` и `[Relation]`. Per-property XML-doc — без изменений.
3. Из верхнеуровневого `<summary>` оставить **только первое предложение**.
4. Над классом добавить `DOC_<Entity>` с `_refName`, `Name`, `Description` (verbatim), `Entity => typeof(<Entity>)`, chunks по смысловым блокам, финальный `Body` со `SdaidCode.OfClass<X>()`.
5. Если в модуле ещё нет `BC_<Module>.cs` — создать (без выдуманной прозы).
6. Обновить `MIGRATION_MAP.md`.
7. Сборка + регенерация + визуальная проверка страницы.
## Подводные камни
- **`init`-сеттеры в netstandard2.0** — нужен polyfill `IsExternalInit` (уже добавлен в `_Polyfills.cs`); там же stub `DateOnly`.
- **Static-классы как type argument** — `SdaidCode.OfClass<SomeSeedData>()` не скомпилируется (CS0718). Использовать не-generic перегрузку: `SdaidCode.OfClass(typeof(SomeSeedData))`.
- **Cross-модульные ссылки в XML-doc** на класс из ещё не мигрированного модуля — заменять `<see cref="X"/>` на `<c>X</c>`, иначе CS0246.
- **Стейл DLL после миграции** — если генератор падает с TargetInvocationException, пересобрать: `dotnet build sdaid/Rugrid.Sdaid.Epas.Ntr` и запустить генератор с `--no-build`.
"""";
public string Version => "";
public string Status => "";
public string[] Comments => new string[0];
}
// ═══════════════════════════════════════════════════════════════════════════════
// 9. Чеклист на старте следующей сессии
// ═══════════════════════════════════════════════════════════════════════════════
/// <summary>Что прочитать и подтвердить перед следующей итерацией миграции.</summary>
public class Migration_S9_NextSessionChecklist : ISdaidDocumentSection
{
public Type Document => typeof(HowToMigrateMdToSdaid);
public Type Parent => typeof(object);
public int Order => 90;
public string Number => "";
public string Name => "Чеклист на старте следующей сессии";
public string Body =>
"""
Когда пользователь скажет «продолжаем миграцию» — выполнить **в этом порядке**, до первого `Edit`/`Write`:
1. Прочитать нотацию: `15_Documents/SdaidDocument.cs`, `SdaidDocumentSection.cs`, `SdaidDocumentKinds.cs`, `SdaidAnchors.cs`, `SdaidStructuralLinks.cs`, `SdaidFolder.cs`, `SdaidFolderForKind.cs`, `DomainEntityAttribute.cs`; `20_Glossary/GlossaryTerm.cs`, `GlossaryAnchors.cs`; `30_Requirements/IRequirement.cs`. Это контракты, ничего без них не делается.
2. Прочитать **этот HowTo** целиком.
3. Прочитать текущее состояние спеки: `sdaid/Rugrid.Sdaid.Epas/`. Если что-то уже мигрировано — пересмотреть на соответствие правилам этого HowTo.
4. Если ловится дрейф (перефразированный текст, фабрикованные термины, row-specific хелперы) — поправить **до** новой работы. Иначе старые ошибки расползутся в новые файлы.
5. Прочитать источник (`rugrid-devdocs/service-enter/`, `rugrid-ntr/docs/`) — что ещё не мигрировано, сверяясь с `MIGRATION_MAP.md`.
6. Спросить пользователя: с какого источника продолжаем — vision, конкретный эпик, глоссарий, ADR?
7. **Только после этого** начинать миграцию следующего блока по фазам из секции «Фазы миграции».
### Признаки что чеклист пропущен
* Появляется prose в `Body`, которое не цитируется из источника → перефразирование вернулось.
* Создаются классы для артефактов, которых нет в источнике → фабрикация.
* Появляются строковые литералы в `RefTo` для типов, у которых есть статика `Code`/`Name` → пропустил прямое обращение.
* Таблица оформлена через row-specific helper → пропустил value-cell pattern.
Если поймали — остановиться, перечитать секцию «Антипаттерны», переделать.
""";
public string Version => "";
public string Status => "";
public string[] Comments => new string[0];
}
}