⟨/⟩ Ntr/00_Migration/HowToMigrateMdToSdaid.cs

815 строк · в начало

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];
    }
}