Перейти к содержимому

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>, чтобы подменить им канонический без правки отслеживаемого пакета.

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.enabledtrue для 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, ровно один выигрывает:

  1. Пройти цепочку extends каждого сервиса и вычислить глубину.
  2. Выигрывает сервис с самой поверхностной цепочкой. Глубина ограничена (сейчас 32 хопа) как cycle-guard.
  3. Ничьи на одной глубине ломаются лексикографически по имени, поэтому результат детерминирован.

Проигравшие сервисы выдают предупреждение, называющее победителя и спорный каталог.

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 ideper-variant конфигурации редактора (отладчик, launch-профиль)вариант, с которым работают прямо сейчас
render aiидентичность hub и ориентация для AI-агентаканонический владелец hub

dwe render ai <name> трактует <name> как hub-якорь (та же модель, что у render ide).

Порядок валидации (первая ошибка побеждает):

  1. Сервис не в конфигурации.
  2. Сервис отключён на уровне проекта.
  3. У сервиса нет hub-каталога, или его hub — корень проекта.
  4. 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.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.md

Manifest загружается со строгим YAML-декодом: неизвестные поля — жёсткая ошибка, чтобы опечатки вроде renders: ловились рано. Пустой файл или manifest с обоими списками пустыми тоже отвергается.

ПолеОбязательноОписание
fromдапуть к файлу шаблона относительно корня пакета. Должен оканчиваться на .tmpl и не быть абсолютным. Файл должен существовать как обычный (не симлинк, не каталог)
toдапуть назначения относительно hub-каталога сервиса. Может быть вложенным (например, .claude/CLAUDE.md). Не должен выходить из hub. Пусто или .. отвергается
ПолеОбязательноОписание
linkдапуть создаваемого симлинка, относительно hub-каталога сервиса. Может быть вложенным. Не должен выходить из hub
toдацель симлинка, относительно hub-каталога сервиса. Должна совпадать с to одной из записей render — симлинки могут указывать только на файлы, которые производит этот manifest

Симлинки записываются как относительные пути, вычисленные от каталога ссылки до абсолютного пути цели внутри hub. Они никогда не выходят за пределы hub.

Manifest валидируется до записи любого файла:

ПравилоПрименяется к
хотя бы одна запись render или symlinksmanifest
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-каталогом сервиса. Рендерер:

  1. Читает файл шаблона.
  2. Парсит его как Go text/template в строгом режиме — любая ссылка на отсутствующее поле прерывает рендер вместо записи плейсхолдера <no value>.
  3. Выполняет шаблон по переменным шаблона.
  4. Разрешает назначение и прогоняет гарды безопасности путей.
  5. Создаёт отсутствующие родительские каталоги.
  6. Отказывается перезаписывать назначение, если оно — предсуществующий симлинк.
  7. Пишет отрендеренные байты.

Шаблоны получают ту же форму объекта, что и IDE-шаблоны:

ПеременнаяИсточник
.Projectблок project: из workspace.yml
.Serviceканоническая конфигурационная идентичность — корень цепочки extends: рендерящегося сервиса. Используйте для raw-config поисков по имени сервиса. Равно .Resolved без цепочки extends.
.Resolvedrender-идентичность — сервис, чей 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 ...}} для полей, которые могут быть законно пустыми.

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-рендер:

  1. Очистка пути. Валидация manifest уже отвергла абсолютные пути, ..-побеги и .-результаты.
  2. Containment hub. Разрешённое назначение должно быть внутри абсолютного hub-каталога.
  3. Никаких симлинков в пути назначения. Существующие компоненты пути проверяются до создания любого каталога.
  4. Границы реального пути после создания. После создания родительских каталогов каталог назначения разрешается через любые симлинки и должен быть внутри и реального корня проекта, и реального hub. Это ловит гонку, когда симлинк подбрасывают между проверками.
  5. Никаких симлинков на месте файла назначения. Предсуществующий симлинк по target-пути отвергается (а не следуется по ссылке с перезаписью того, на что она указывает).

Сам пакет тоже под охраной: проверки родительских путей корня пакета отвергают любые симлинкнутые компоненты, а валидатор manifest запрещает симлинки в любом пути from.

Для каждой записи symlinks рендерер создаёт симлинк по link, указывающий на to. Оба пути трактуются как относительные к hub-каталогу сервиса.

Шаги:

  1. Проверить, что и link, и to остаются внутри hub.
  2. Создать родительский каталог ссылки с теми же гардами безопасности путей, что используются для рендерящихся файлов.
  3. Вычислить относительный путь от каталога ссылки до цели внутри hub. Симлинк на диске всегда относительный, чтобы hub оставался переносимым между машинами.
  4. Осмотреть существующий путь по местонахождению ссылки:
    • симлинк уже указывает на правильную относительную цель — no-op (идемпотентно).
    • симлинк указывает в другое место — удалить и пересоздать.
    • обычный файл или каталог — отказать с ошибкой, советующей либо удалить файл, либо задать render.ai.enabled: false для сервиса.
    • пути нет — создать симлинк.

Результат идемпотентен по содержимому: перезапуск dwe render ai приводит hub-каталог в то же финальное состояние. Учтите: отрендеренные файлы всегда перезаписываются на каждом прогоне (поэтому mtime обновляется), но байты полностью определены шаблонами и объединённой конфигурацией. Симлинки же пересоздаются только когда существующий отсутствует или указывает не туда.

Раскладка:

workspace/services/
main/
service.yml
main-debug/
service.yml
workspace/templates/ai/
default/
manifest.yml
AGENTS.md.tmpl
.claude/CLAUDE.md.tmpl

Manifest 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: app
container: app-main
dir: ./services/main

workspace/services/main-debug/service.yml:

type: app
extends: main
container: app-main-debug
dir: ./services/main # тот же hub, что у родителя — коллизия

dwe render ai:

  1. Выборка: оба сервиса проходят гейт активации (дефолтный render.ai.enabled: true). Они делят dir: ./services/main. У main цепочка extends поверхностнее (глубина 0 против 1 у main-debug), поэтому выигрывает main. main-debug сообщается как пропуск из-за коллизии.
  2. Разрешение пакета для main: render.ai.template не задан; implicit-цепочка пробует workspace/templates/ai/main/ (не найдено), затем workspace/templates/ai/default/ (используется).
  3. Manifest загружен и валиден: две записи рендера, один симлинк. Симлинк указывает на AGENTS.md, который и есть одно из render-назначений.
  4. Каждая запись рендера обрабатывается: AGENTS.md и .claude/CLAUDE.md записываются в services/main/.
  5. Создаётся симлинк services/main/CLAUDE.md → AGENTS.md.

Результат:

services/main/
AGENTS.md ← отрендерен из AGENTS.md.tmpl
CLAUDE.md ← симлинк на AGENTS.md
.claude/
CLAUDE.md ← отрендерен из .claude/CLAUDE.md.tmpl

dwe 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.aienabled, template, наследование через extends
  • render ide — родственная команда с противоположной (глубочайший-выигрывает) политикой коллизий
  • Запустите dwe render ai --help, чтобы увидеть актуальный CLI-интерфейс