Чем больше сообщество вложится в мост, тем мощнее инструмент«DomainCraft» — рабочее название
DomainCraft

24 августа 2026 г. · 5 мин чтения

Компилятор, а не генератор: почему это не игра слов

DomainCraft — это не генератор шаблонного кода, а компилятор доменной модели: настоящий конвейер из лексера, парсера, валидатора, IR и рендерера. Рассказываем, чем это отличается от «печати файлов» и что даёт на практике.

«Сгенерировать код» может любой шаблонизатор. «Скомпилировать контракт» — только компилятор. Рассказываем, чем эти слова отличаются в DomainCraft — и почему это различие определяет всё.

Когда мы говорим, что DomainCraft — компилятор, первый вопрос закономерный: «А в чём разница? Это же просто генератор кода». Снаружи так и выглядит: файл YAML на входе, папка с кодом на выходе. Разница — в том, что происходит внутри и какие гарантии вы получаете.

Как всё задумывалось: генератор для бэкенда

Изначально DomainCraft был генератором и по замыслу, и по устройству: одна цель — бэкенд на C#/ASP.NET Core, один мост, 71 шаблон. Модель описана на YAML, пайплайн прогоняет шаблоны, печатает файлы. Всё работало — пока мост был один.

Как только мы захотели добавить второй язык (TypeScript на фронтенд), проявился главный вопрос: кто гарантирует, что два независимо сгенерированных пакета кода на разных языках говорят об одном и том же?

Шаблонизатор не видит модель как структуру: для него это текстовые файлы и подстановки. Один и тот же enum может стать OrderStatus в C# и строкой "pending" в JSON-клиенте — и никто не заметит, пока в рантайме не всплывёт рассинхронизация.

Прозрение: IR был, но мы видели только языки

IR появился в DomainCraft не как инновация, а как необходимость: невозможно рендерить шаблоны корректно, если нет структуры, из которой их рендерить. Долго мы воспринимали его узко: «есть IR — значит, легко добавить ещё один бэкенд-язык». В голове было только одно измерение — сам язык.

Главное прозрение пришло, когда мы поняли: вариативность не одномерна. Транспорт (REST, gRPC, минимальный API) и персистенция (EF Core, Dapper, Marten) — две независимые оси. Тот же IR можно рендерить не только в другой язык, но и в другой слой поверх того же языка — и комбинировать слои произвольным образом. Из этого выросло описанное в прошлом посте деление на оси и слои, где N вариантов персистенции × M вариантов транспорта = N+M репозиториев, а не N×M: От монолита к осям и слоям.

Компилятор: как устроен конвейер

В DomainCraft генерация — это не прогон шаблонов, а компиляция. Пайплайн буквально повторяет классический конвейер компилятора:

domain.yaml
   → Лексер   — разбирает каждое поле на токены
   → Парсер   — превращает YAML в структурированную модель
   → Валидатор — проверяет семантику (связи, права, индексы, данные)
   → IR-билдер — собирает связный граф объектов
   → Рендерер  — выводит код через мосты

Ключевой узел — внутреннее представление (IR). Это не промежуточный формат файлов, а разрешённая семантика: связный граф сущностей, полей, связей, прав, индексов. В нём уже нет YAML — только модель.

Мост — по сути backend компилятора для своего языка: он умеет превратить IR в файлы своего мира (C#, TypeScript, React-хуки, админка). Само IR ни от какого языка не зависит.

              ┌→ мост C#   → ASP.NET Core API
domain.yaml → IR ─→ мост TS  → типизированный клиент + хуки
              └→ мост Admin → админ-панель

Что компилятор гарантирует, а шаблонизатор — нет

Ошибки ловятся до рантайма

Компилятор не соберёт исполняемый файл с неразрешёнными символами — он просто не соберётся. DomainCraft не выдаст код, в котором права ссылаются на несуществующую сущность, индекс — на отсутствующее поле, а on_delete стоит на обязательной связи. Валидатор проверяет сотни таких правил ещё до генерации: связность отношений, коллизии в правах, типы seed-данных, дубли первичных ключей, противоречивые модификаторы.

В обычном генераторе эти ошибки всплыли бы спустя время, в рантайме приложения. Здесь — в момент «компиляции», до первого деплоя.

Один контракт — во всех слоях

У модели один источник правды — IR. Из него компилируется всё:

  • типы сущностей и DTO;
  • wire-значения enum (credit_card — одинаково в C#, TypeScript и JSON);
  • грамматика фильтров path:op:value — и для API, и для типизированного клиента;
  • матрица прав — в endpoint-ы, в кнопки и в клиентские проверки;
  • контракт ошибок (409 CONCURRENCY_CONFLICT, field-errors);
  • миграции базы и переименования (old_name).

C#-сервер и TypeScript-клиент не могут разойтись по проводам: оба скомпилированы из одного IR, а не написаны двумя командами по разным документациям.

Контракт проверяет независимый оракул

Скомпилированному коду мало «собралось нормально» — нужно ещё доказать, что он соответствует контракту. У DomainCraft для этого две системы проверки, и обе независимы от мостов:

  • k6 — HTTP-оракул, один на все языки. Запускает сгенерированное приложение и бьёт по нему реальными запросами: статусы, права, изоляция @owner, оптимистичные блокировки, пагинация, грамматика фильтров. Это 136 проверок, которым безразлично, на каком языке написан сервер, — только контракт.
  • Per-language TCK — рукописный оракул для каждого тонкого ядра. Существует то, что через HTTP не проверить: compile-time типы и wire-сериализация (hidden/readonly-исключения, wire-значения enum, типованные фильтры). Тесты написаны людьми против контракта, а не порождены тем же IR, что и мосты, — иначе ошибка моста воспроизвелась бы в тесте и он «прошёл» бы.

Генератор «протестирован». Компилятор — сертифицирован против независимого оракула. Разница в уровне гарантий.

Один и тот же выход — всегда

Компилятор детерминирован: один исходник, одни и те же биты на выходе. В DomainCraft каждая итерация по словарям идёт через отсортированный список — два прогона дают посимвольно одинаковые файлы. Это база для аудита, для диффов в код-ревью и для уверенности, что «чуть-чуть перегенерил — и всё съехало» не случится.

Тот же компилятор работает в редакторе

Ярче всего это показывает GUI-редактор. Тот же лексер, парсер и валидатор скомпилированы в WASM и крутятся прямо в браузере: структура модели в редакторе проверяется тем же кодом, что и в CLI. Один компилятор, одно поведение — в терминале и в редакторе. Шаблонизатору такое не под силу.

Зачем это в будущем

Пока языков два — C# и TypeScript — выигрыш выглядит абстрактным. Будущее многократно его усиливает:

  • Новый мост — новый backend. Dapper вместо EF Core, gRPC вместо REST — это маленький слой со своими шаблонами, а комбинация собирается флагом --replace. Новая комбинация не требует нового «генератора».
  • Новые платформы — на том же контракте. Dart-мост (в планах) получит из коробки то, что уже есть у C# и TypeScript: wire-значения enum, грамматику фильтров, права — бесплатно, потому что это семантика, а не копипаста.
  • ИИ-агенты строят против контракта. Агент получает типизированный клиент, в котором права, типы и query-схема уже собраны из того же IR — и пишет экраны, а не догадывается об API.

Итог

«Компилятор» — это не статус в README, а ответ на вопрос, чему вы доверяете модель. Генератор печатает файлы и уходит. Компилятор принимает модель, проверяет её, выводит один контракт и раздаёт по всем слоям продукта. А независимые оракулы (k6 и per-language TCK) подтверждают: контракт реально выполняется, а не декларирован.

Один файл, один компилятор, один контракт — и оракулы, которые за него отвечают.

domaincraft generate --domain domain.yaml --bridge csharp-rest --admin