dwe render ai
Сгенерировать hub-уровневую документацию для агентов для каждого включённого сервиса из пакета шаблонов. Пакет объявляет manifest.yml, перечисляющий файлы для рендера и симлинки для создания внутри hub-каталога сервиса (например, services/main/AGENTS.md плюс services/main/CLAUDE.md → AGENTS.md).
render ai и render ide делят большую часть пер-сервисной инфраструктуры — выборку, разрешение шаблонов, гарды безопасности путей, схему manifest. Отличаются в одном важном месте: политика коллизий инвертирована (выигрывает поверхностнейший, а не глубочайший).
Пофайловые локальные оверрайды через соседний <pack>.local/ shadow-tree применяются к AI-пакетам так же, как к IDE и git — положите файл в workspace/templates/ai/<pack>.local/<rel>, чтобы подменить им канонический без правки отслеживаемого пакета.
Содержание
Заголовок раздела «Содержание»- Конвейер
- Выборка сервисов
- Разрешение пакета шаблонов
- Схема manifest
- Пофайловый рендер
- Создание симлинков
- Проработанный пример
- Сообщения вывода
- Частые ловушки
- Связанные справочники
Конвейер
Заголовок раздела «Конвейер»flowchart TD
CFG["Загрузить объединённую конфигурацию"] --> SEL{"Аргумент задан?"}
SEL -- нет --> SP["Выбрать сервисы<br/>фильтр политики + коллизия"]
SEL -- да --> EXV["Валидировать аргумент"]
EXV --> RHA["Разрешить hub-якорь<br/>поверхностнейший выигрывает"]
SP --> LIST["Отсортированный список сервисов"]
RHA --> LIST
LIST --> EACH{"Для каждого сервиса"}
EACH --> RP["Разрешить пакет шаблонов"]
RP --> LM["Загрузить manifest.yml<br/>строгий декод"]
LM --> VM["Валидировать manifest"]
VM --> R["Рендерить каждый manifest.render"]
R --> SL["Создать каждый manifest.symlinks"]
SL --> EACH
EACH -- "конец" --> DONE["готово"]
Выборка сервисов
Заголовок раздела «Выборка сервисов»Гейт активации
Заголовок раздела «Гейт активации»Сервис участвует в рендере agent-доков, только если оба флага истинны:
| Гейт | Источник | По умолчанию |
|---|---|---|
| Уровень проекта | services.<name>.enabled (3-слойный merged + обязательный service override) | зависит от сервиса |
| Политика agent-доков | services.<name>.render.ai.enabled | true для type: app, false для остальных типов |
Agent-доки по умолчанию включены только для app-сервисов, потому что только у них есть выделенная директория исходников, где можно разместить hub-документацию. Tools/infra включают рендер явно через render.ai.enabled: true.
Если хоть один гейт ложный — сервис пропускается. Пропуски делятся на две группы:
| Группа | Когда | Сообщается как warning? |
|---|---|---|
| Политические пропуски | сервис отключён на уровне проекта, или render.ai.enabled равен false | нет — это документированное opt-out поведение |
| Actionable-пропуски | у сервиса нет hub-каталога, или другой сервис выиграл коллизию каталога | да — обычно это указывает на неверную настройку |
Нормализация каталога
Заголовок раздела «Нормализация каталога»Так же, как у render ide: сервисы без hub-каталога отбрасываются — либо dir пуст, либо он разрешается в корень проекта. Agent-докам нужен реальный hub-каталог для записи, и они не должны нацеливаться на корень проекта.
Разрешение коллизий: выигрывает поверхностнейший
Заголовок раздела «Разрешение коллизий: выигрывает поверхностнейший»Когда более одного выживающего сервиса указывает на один и тот же dir, ровно один выигрывает:
- Пройти цепочку
extendsкаждого сервиса и вычислить глубину. - Выигрывает сервис с самой поверхностной цепочкой. Глубина ограничена (сейчас 32 хопа) как cycle-guard.
- Ничьи на одной глубине ломаются лексикографически по имени, поэтому результат детерминирован.
Проигравшие сервисы выдают предупреждение, называющее победителя и спорный каталог.
flowchart LR
M["main<br/>extends: ничего<br/>глубина 0"] --> D
MD["main-debug<br/>extends: main<br/>глубина 1"] --> D
D["dir: services/main"] --> W{"поверхностнейший выигрывает"}
W --> WIN["рендерится main"]
W -. "skip warning" .-> MD
Обоснование: agent-доки описывают идентичность hub. Когда ребёнок extends родителя и делит его dir, владельцем канонического hub является родитель; ребёнок — runtime-вариант той же рабочей среды, а не отдельная рабочая среда со своей идентичностью. Выбор родителя сохраняет содержимое AGENTS.md стабильным при переключении вариантов.
Это противоположно политике «глубочайший выигрывает» у render ide. Причина — разница в том, что рендерится:
| Команда | Что рендерится | Чья точка зрения? |
|---|---|---|
render ide | per-variant конфигурации редактора (отладчик, launch-профиль) | вариант, с которым работают прямо сейчас |
render ai | идентичность hub и ориентация для AI-агента | канонический владелец hub |
Явный аргумент [service]
Заголовок раздела «Явный аргумент [service]»dwe render ai <name> трактует <name> как hub-якорь (та же модель, что у render ide).
Порядок валидации (первая ошибка побеждает):
- Сервис не в конфигурации.
- Сервис отключён на уровне проекта.
- У сервиса нет hub-каталога, или его hub — корень проекта.
render.ai.enabledравенfalse.
После валидации применяется то же «поверхностнейший выигрывает», ограниченное сиблингами, делящими hub. Если победитель отличается, info-строка объявляет подмену.
Так dwe render ai main-debug всё равно рендерит родителя main, когда оба включены — вариант разрешается в канонического владельца hub.
Разрешение пакета шаблонов
Заголовок раздела «Разрешение пакета шаблонов»Для каждого выбранного сервиса рендерер выбирает один каталог-пакет под workspace/templates/ai/. Цепочка разрешения зеркалит IDE точно, различается только базовый каталог.
flowchart TD
S{"render.ai.template задан?"}
S -- да --> EX["workspace/templates/ai/{template}/"]
EX -- существует --> USE["использовать этот пакет"]
EX -- отсутствует --> ERR["ошибка<br/>явный — это строго"]
S -- нет --> SN["workspace/templates/ai/{service-name}/"]
SN -- существует --> USE
SN -- отсутствует --> DEF["workspace/templates/ai/default/"]
DEF -- существует --> USE
DEF -- отсутствует --> WARN["warning + skip<br/>implicit не найден"]
Правила:
- Явный — это строго. Заданный
render.ai.template, которого не существует, — жёсткая ошибка, никакого тихого fallback. - Implicit-цепочка (когда
render.ai.templateне задан):<service-name>→ проход по цепочкеextends:предок-за-предком →default. Побеждает первый существующий пакет. Если implicit-цепочка исчерпана без пакета, рендер пропускается с предупреждением. Невалидные имена пакетов в цепочке (например, начинающиеся с точки или содержащие разделитель пути) пропускаются молча, и обход продолжается. - Пакет должен быть реальным каталогом; симлинки в корне пакета или в любом компоненте родителя отвергаются.
- Валидация ключа шаблона отвергает разделители путей, точки в начале и
...
Схема manifest
Заголовок раздела «Схема manifest»Каждый пакет обязан содержать manifest.yml в корне. Manifest объявляет, что пакет производит; в отличие от IDE-рендера, implicit-обхода нет. Если файл в пакете не упомянут в render, он игнорируется.
render: - from: AGENTS.md.tmpl to: AGENTS.md - from: .claude/CLAUDE.md.tmpl to: .claude/CLAUDE.md
symlinks: - link: CLAUDE.md to: AGENTS.mdManifest загружается со строгим YAML-декодом: неизвестные поля — жёсткая ошибка, чтобы опечатки вроде renders: ловились рано. Пустой файл или manifest с обоими списками пустыми тоже отвергается.
Записи render
Заголовок раздела «Записи render»| Поле | Обязательно | Описание |
|---|---|---|
from | да | путь к файлу шаблона относительно корня пакета. Должен оканчиваться на .tmpl и не быть абсолютным. Файл должен существовать как обычный (не симлинк, не каталог) |
to | да | путь назначения относительно hub-каталога сервиса. Может быть вложенным (например, .claude/CLAUDE.md). Не должен выходить из hub. Пусто или .. отвергается |
Записи symlinks
Заголовок раздела «Записи symlinks»| Поле | Обязательно | Описание |
|---|---|---|
link | да | путь создаваемого симлинка, относительно hub-каталога сервиса. Может быть вложенным. Не должен выходить из hub |
to | да | цель симлинка, относительно hub-каталога сервиса. Должна совпадать с to одной из записей render — симлинки могут указывать только на файлы, которые производит этот manifest |
Симлинки записываются как относительные пути, вычисленные от каталога ссылки до абсолютного пути цели внутри hub. Они никогда не выходят за пределы hub.
Правила валидации manifest
Заголовок раздела «Правила валидации manifest»Manifest валидируется до записи любого файла:
| Правило | Применяется к |
|---|---|
хотя бы одна запись render или symlinks | manifest |
from непуст, относителен, оканчивается на .tmpl | каждый render |
from не выходит из пакета и не содержит симлинков в родительских компонентах | каждый render |
from существует как обычный файл (не симлинк, не каталог) | каждый render |
to непуст, относителен, не разрешается в . или .. | каждый render |
to уникален в пределах manifest | каждый render |
link непуст, относителен, не выходит из hub | каждый symlink |
link уникален среди symlinks | каждый symlink |
link не пересекается ни с одним to из render (один путь не может быть и отрендеренным файлом, и симлинком) | каждый symlink |
to совпадает с известным render-назначением | каждый symlink |
Сравнения путей выполняются на очищенных путях, поэтому AGENTS.md и ./AGENTS.md трактуются как одно назначение.
Пофайловый рендер
Заголовок раздела «Пофайловый рендер»Для каждой записи render источник — файл шаблона внутри пакета, а назначение — путь to, объединённый с hub-каталогом сервиса. Рендерер:
- Читает файл шаблона.
- Парсит его как Go text/template в строгом режиме — любая ссылка на отсутствующее поле прерывает рендер вместо записи плейсхолдера
<no value>. - Выполняет шаблон по переменным шаблона.
- Разрешает назначение и прогоняет гарды безопасности путей.
- Создаёт отсутствующие родительские каталоги.
- Отказывается перезаписывать назначение, если оно — предсуществующий симлинк.
- Пишет отрендеренные байты.
Переменные шаблона
Заголовок раздела «Переменные шаблона»Шаблоны получают ту же форму объекта, что и IDE-шаблоны:
| Переменная | Источник |
|---|---|
.Project | блок project: из workspace.yml |
.Service | каноническая конфигурационная идентичность — корень цепочки extends: рендерящегося сервиса. Используйте для raw-config поисков по имени сервиса. Равно .Resolved без цепочки extends. |
.Resolved | render-идентичность — сервис, чей hub реально рендерится (победитель по политике коллизий). Для AI победитель — самый поверхностный расширитель, поэтому .Resolved обычно равно .Service. |
.ServiceCfg | эффективная конфигурация сервиса .Resolved после разрешения extends |
.Runtime | объединённый блок runtime |
.Cfg | объединённый DweConfig (продвинутое). .Cfg.Raw — мапа после слияния и нормализации DWE (services.* подставляется из per-service service.yml) — см. Шаблоны. Для обычных случаев предпочитайте выделенные поля выше. |
Совет. AI-выход ложится в
<svc.Dir>/<entry.To>— обычно в отслеживаемые проектные файлы (AGENTS.md,.claude/CLAUDE.md, …). Избегайте использования developer-local или секретных ключей через.Cfg.Rawв AI-шаблонах: любое значение, попавшее изworkspace/local.yml, всплывёт в отрендеренном файле и даст разные диффы у разных разработчиков в отслеживаемых артефактах. Используйте.Cfg.Rawтолько для общих для всего проекта соглашений.
Строгий режим рендера означает, что опечатка {{.Servic.Name}} прерывает рендер вместо вывода <no value>. Используйте {{if ...}} для полей, которые могут быть законно пустыми.
Доступ к .Cfg.Raw
Заголовок раздела «Доступ к .Cfg.Raw»Go-овский text/template разрешает dot-сегменты, только если каждый совпадает с [A-Za-z_][A-Za-z0-9_]*. До ключей с дефисами, точками, ведущими цифрами или любыми другими не-идентификаторными символами нельзя добраться dot-синтаксисом — используйте index.
{{ .Cfg.Raw.ai.persona }} {{- /* точка — identifier-safe ключи */ -}}{{ index .Cfg.Raw "my-tool" "api-key" }} {{- /* index — ключи с дефисами */ -}}Полный набор хелперов, доступных внутри *.tmpl (appURL, sprout-реестры, встроенные text/template), описан в Шаблоны.
Гарды безопасности путей
Заголовок раздела «Гарды безопасности путей»Рендерер применяет те же граничные проверки, что и IDE-рендер:
- Очистка пути. Валидация manifest уже отвергла абсолютные пути,
..-побеги и.-результаты. - Containment hub. Разрешённое назначение должно быть внутри абсолютного hub-каталога.
- Никаких симлинков в пути назначения. Существующие компоненты пути проверяются до создания любого каталога.
- Границы реального пути после создания. После создания родительских каталогов каталог назначения разрешается через любые симлинки и должен быть внутри и реального корня проекта, и реального hub. Это ловит гонку, когда симлинк подбрасывают между проверками.
- Никаких симлинков на месте файла назначения. Предсуществующий симлинк по target-пути отвергается (а не следуется по ссылке с перезаписью того, на что она указывает).
Сам пакет тоже под охраной: проверки родительских путей корня пакета отвергают любые симлинкнутые компоненты, а валидатор manifest запрещает симлинки в любом пути from.
Создание симлинков
Заголовок раздела «Создание симлинков»Для каждой записи symlinks рендерер создаёт симлинк по link, указывающий на to. Оба пути трактуются как относительные к hub-каталогу сервиса.
Шаги:
- Проверить, что и
link, иtoостаются внутри hub. - Создать родительский каталог ссылки с теми же гардами безопасности путей, что используются для рендерящихся файлов.
- Вычислить относительный путь от каталога ссылки до цели внутри hub. Симлинк на диске всегда относительный, чтобы hub оставался переносимым между машинами.
- Осмотреть существующий путь по местонахождению ссылки:
- симлинк уже указывает на правильную относительную цель — no-op (идемпотентно).
- симлинк указывает в другое место — удалить и пересоздать.
- обычный файл или каталог — отказать с ошибкой, советующей либо удалить файл, либо задать
render.ai.enabled: falseдля сервиса. - пути нет — создать симлинк.
Результат идемпотентен по содержимому: перезапуск dwe render ai приводит hub-каталог в то же финальное состояние. Учтите: отрендеренные файлы всегда перезаписываются на каждом прогоне (поэтому mtime обновляется), но байты полностью определены шаблонами и объединённой конфигурацией. Симлинки же пересоздаются только когда существующий отсутствует или указывает не туда.
Проработанный пример
Заголовок раздела «Проработанный пример»Раскладка:
workspace/services/ main/ service.yml main-debug/ service.ymlworkspace/templates/ai/ default/ manifest.yml AGENTS.md.tmpl .claude/CLAUDE.md.tmplManifest workspace/templates/ai/default/manifest.yml:
render: - from: AGENTS.md.tmpl to: AGENTS.md - from: .claude/CLAUDE.md.tmpl to: .claude/CLAUDE.md
symlinks: - link: CLAUDE.md to: AGENTS.mdШаблон workspace/templates/ai/default/AGENTS.md.tmpl:
# {{.Service}} Service Hub
This is the {{.Service}} service running inside a DWE-managed hub.The application source code is at `src/`.
Service container: {{.ServiceCfg.Container}}Workspace root: {{.ServiceCfg.DirInternal}}workspace/services/main/service.yml:
type: appcontainer: app-maindir: ./services/mainworkspace/services/main-debug/service.yml:
type: appextends: maincontainer: app-main-debugdir: ./services/main # тот же hub, что у родителя — коллизияdwe render ai:
- Выборка: оба сервиса проходят гейт активации (дефолтный
render.ai.enabled: true). Они делятdir: ./services/main. Уmainцепочка extends поверхностнее (глубина 0 против 1 уmain-debug), поэтому выигрываетmain.main-debugсообщается как пропуск из-за коллизии. - Разрешение пакета для
main:render.ai.templateне задан; implicit-цепочка пробуетworkspace/templates/ai/main/(не найдено), затемworkspace/templates/ai/default/(используется). - Manifest загружен и валиден: две записи рендера, один симлинк. Симлинк указывает на
AGENTS.md, который и есть одно из render-назначений. - Каждая запись рендера обрабатывается:
AGENTS.mdи.claude/CLAUDE.mdзаписываются вservices/main/. - Создаётся симлинк
services/main/CLAUDE.md → AGENTS.md.
Результат:
services/main/ AGENTS.md ← отрендерен из AGENTS.md.tmpl CLAUDE.md ← симлинк на AGENTS.md .claude/ CLAUDE.md ← отрендерен из .claude/CLAUDE.md.tmpldwe render ai main-debug производит те же файлы — явный аргумент валидируется, но hub-anchor разрешение выбирает main (поверхностнейший) и печатает ai [main-debug] — resolved to main (hub services/main).
Сообщения вывода
Заголовок раздела «Сообщения вывода»| Поток | Триггер |
|---|---|
| info | Явный аргумент разрешился в другого сиблинга — называется выбранный победитель и общий hub-каталог. |
| warning | Выбранный сервис пропущен, потому что у него нет hub-каталога (или его hub — корень проекта). |
| warning | Выбранный сервис пропущен, потому что другой сервис выиграл коллизию каталога — победитель назван. |
| success | По одной строке на каждый отрендеренный файл, с относительным путём внутри проекта. |
| success | По одной строке на каждый симлинк (вновь созданный или уже корректный), показывая и ссылку, и её цель. |
| info | Ничего не выбрано после применения политики и правил коллизии. |
Ошибки возвращаются как падение команды и называют проблемный сервис, чтобы источник проблемы был очевиден.
Частые ловушки
Заголовок раздела «Частые ловушки»- Предсуществующий не-симлинк по пути управляемого симлинка. Если
CLAUDE.mdуже существует как обычный файл (например, после прошлой ручной правки),render aiоткажется его перезаписать. Удалите файл или задайтеrender.ai.enabled: falseдля сервиса. toсимлинка должен ссылаться на render-назначение. Валидатор manifest это обеспечивает; нельзя симлинкнуть на произвольный файл вне manifest.- Опечатки в manifest — жёсткие ошибки. Строгий YAML-декод означает, что опечатанный ключ вроде
renders:илиsymlink:прерывает загрузку. Исправьте написание. - Пустой manifest отвергается. Manifest с обоими
render: []иsymlinks: []почти всегда ошибка. - Варианты разделяют идентичность родителя. Это намеренно. Если runtime-варианту по-настоящему нужен свой AGENTS.md, дайте ему другой
dir. - Шаблоны, ссылающиеся на отсутствующие поля, падают. Строгий режим рендера прерывает рендер при любой ссылке на отсутствующее поле. Заворачивайте опциональные поля в
{{if ...}}. render aiне обходит пакет. Файлы внутри пакета, не упомянутые вmanifest.yml, на этапе рендера молча игнорируются. Добавьте запись подrender:, чтобы включить их.
Связанные справочники
Заголовок раздела «Связанные справочники»- блок
services.<name>.render.ai—enabled,template, наследование черезextends render ide— родственная команда с противоположной (глубочайший-выигрывает) политикой коллизий- Запустите
dwe render ai --help, чтобы увидеть актуальный CLI-интерфейс