[refactor][agent-roles-catalog] Перевод каталога ролей на YAML (instructions блок-скаляром, построчные диффы) #229

Closed
opened 2026-06-27 04:02:37 +03:00 by Ghost · 0 comments

Проблема

В каталоге ролей (agent-roles-catalog/) поле instructions — это один большой системный промпт, который в JSON хранится как одна длинная строка с экранированными \n. Из‑за этого:

  • любая правка одного предложения показывает в дифф‑е изменение всей строки целиком (см. недавнюю правку блока «СТАЙЛ‑ШИТ» — весь промпт ушёл в дифф одной строкой);
  • редактировать длинный многострочный промпт внутри JSON‑строки неудобно и ошибкоопасно (ручные \n, экранирование кавычек).

Цель — перевести файлы каталога на YAML и хранить instructions как литеральный блок‑скаляр (|-), чтобы:

  • диффы были построчными;
  • длинные промпты редактировались как обычный текст.

Архитектурный контекст (важно)

Каталог — это данные, которые читает отдельный сервер в рантайме. Сервер (AiAgentRolesCatalogProvider, apps/server/src/core/ai-chat/roles/catalog/) скачивает RAW‑файлы по базовому URL (AI_AGENT_ROLES_CATALOG_URL, задаётся per‑branch в CI: develop‑образ → develop‑URL, main‑образ → main‑URL), делает JSON.parse и прогоняет через рукописные type‑guard'ы (осознанно без zod / без зависимостей, т.к. вход недоверенный).

Сейчас guard жёстко требует typeof v.instructions === 'string' — массив он бы отверг. Поэтому смена формата на YAML — это не чистая правка данных, а скоординированное изменение: данные + парсер сервера + валидатор check.mjs + тесты + доки, выезжающие одной веткой.

Предлагаемое решение

1. Данные → YAML (5 файлов)

  • index.jsonindex.yaml
  • bundles/editorial/ru.jsonru.yaml, bundles/editorial/en.jsonen.yaml
  • bundles/research/ru.jsonru.yaml, bundles/research/en.jsonen.yaml
  • старые .json удалить.

instructions хранить литеральным блок‑скаляром |- (chomp, без хвостового \n), чтобы резолвнутый промпт был байт‑в‑байт прежним. Остальные поля (slug, emoji, name, description, launchMessage, version, schemaVersion, language) — обычные скаляры.

Конвертацию делать программно через библиотеку yaml (round‑trip‑safe), а не руками:

import YAML from 'yaml'
const obj = JSON.parse(readFileSync(src, 'utf8'))
const out = YAML.stringify(obj, { lineWidth: 0 }) // lineWidth:0 — не сворачивать длинные строки
// инвариант: YAML.parse(out) должен deepEqual obj
writeFileSync(dst, out)

lineWidth: 0 отключает фолдинг (чтобы однострочные description не превратились в folded‑скаляр). Многострочные строки yaml сам пишет литеральным блоком.

2. Сервер AiAgentRolesCatalogProvider

  • добавить зависимость yaml в apps/server/package.json;
  • заменить JSON.parse на безопасный parse из yaml (дефолтная схема: только JSON‑совместимые типы и стандартные теги, без кастомных !!‑тегов / выполнения кода);
  • пути index.jsonindex.yaml, bundles/${bundleId}/${language}.json.yaml;
  • обновить комментарии и тексты ошибок («not valid JSON» → «not valid YAML»);
  • type‑guard'ы (isCatalogRole и др.) НЕ меняются — после парсинга instructions остаётся string;
  • сохранить как есть: SSRF/path‑traversal‑гард (^[a-z0-9-]+$), redirect: 'error', таймаут 10 c, потоковый лимит MAX_BYTES = 1_000_000.

3. Валидатор agent-roles-catalog/scripts/check.mjs

  • парсить YAML через yaml вместо JSON.parse;
  • пути .json.yaml, обновить тексты ошибок;
  • зафиксировать yaml в agent-roles-catalog/package.json (devDependency). Нюанс: каталог не входит в pnpm‑workspace (packages: ['apps/*','packages/*']), но при import 'yaml' из scripts/check.mjs Node поднимается по дереву до корневого node_modules/yaml (он уже в сторе и станет прямой зависимостью сервера). Проверить, что в CI check.mjs находит модуль.

4. Тесты ai-agent-roles-catalog.provider.spec.ts

  • фикстуры JSON.stringify(...) → YAML (через YAML.stringify или YAML‑литералы);
  • тест «invalid JSON body» → «invalid YAML body» (подобрать заведомо невалидный YAML);
  • добавить тест: instructions как многострочный блок‑скаляр парсится и склейка строк корректна (равна ожидаемому промпту).

5. Доки и прочее

  • agent-roles-catalog/README.md: формат файлов (<lang>.yaml, index.yaml), примеры jsonc → yaml, «server fetches … .yaml», секция валидации;
  • apps/server/src/core/ai-chat/roles/catalog/catalog-types.ts: комментарии про .json.yaml;
  • .env.example (~стр. 136): «appends /index.json and /bundles//.json» → .yaml;
  • CI/Docker не меняются: AI_AGENT_ROLES_CATALOG_URL указывает на директорию, расширение добавляет сервер; Dockerfile каталог не копирует.

Версионирование и деплой‑связность

  • version ролей в index.yaml НЕ бампать — резолвнутый контент идентичен, меняется только сериализация.
  • schemaVersion: на обсуждение — оставить 1 (набор полей не изменился) либо бампнуть до 2 как сигнал смены сериализации.
  • Деплой‑связность: сервер читает RAW из той же ветки, из которой собран образ. Пока живёт старый образ, а данные в ветке уже YAML, старый сервер не распарсит → каталог временно недоступен. Митигировать одним из:
    • (а) на перехо��ный период принимать в провайдере оба формата (по расширению), затем удалить .json (рекомендуется, если есть долгоживущие старые образы);
    • (б) мёрджить код+данные вместе и пересобрать образ (короткое окно недоступности — приемлемо).

Безопасность

YAML‑парсинг недоверенного ввода: использовать parse с дефолтной схемой (без кастомных тегов / без !!js). Действующий лимит 1 МБ ограничивает DoS от расширения алиасов (billion laughs). Проверить/настроить лимиты парсера. Нужен security‑ревью этого пункта (есть метка security, если решим повесить).

Затрагиваемые файлы (чеклист)

  • agent-roles-catalog/index.yaml (из index.json)
  • agent-roles-catalog/bundles/editorial/{ru,en}.yaml
  • agent-roles-catalog/bundles/research/{ru,en}.yaml
  • удалить старые *.json
  • apps/server/.../catalog/ai-agent-roles-catalog.provider.ts
  • apps/server/.../catalog/ai-agent-roles-catalog.provider.spec.ts
  • apps/server/.../catalog/catalog-types.ts (комментарии)
  • apps/server/package.json (+yaml)
  • agent-roles-catalog/scripts/check.mjs
  • agent-roles-catalog/package.json (+yaml devDep)
  • agent-roles-catalog/README.md
  • .env.example
  • CHANGELOG.md (запись о ротации формата)

Критерии приёмки (DoD)

  • Все 5 файлов в YAML, старые .json удалены.
  • node agent-roles-catalog/scripts/check.mjsOK.
  • Round‑trip: для каждого файла YAML.parse(new) deepEqual JSON.parse(old).
  • Резолвнутый instructions каждой роли байт‑в‑байт совпадает с прежним.
  • pnpm --filter server test зелёный; провайдер парсит YAML, отвергает невалидный YAML и неверную схему как BadGateway, SSRF‑гард не тронут.
  • Линт/тайпчек зелёные; README и .env.example обновлены.

Открытые вопросы

  1. schemaVersion оставить 1 или бампнуть до 2?
  2. Деплой: вариант (а) переходная двойная поддержка форматов или (б) одновременный выезд?
  3. Расширение файлов: .yaml (в репо есть pnpm-workspace.yaml) vs .yml (crowdin.yml, docker-compose.yml) — предлагаю .yaml.

Спроектировано на основе разбора кода apps/server/src/core/ai-chat/roles/catalog/ и agent-roles-catalog/. Реализация требует ревью (формат данных + парсинг недоверенного ввода).

## Проблема В каталоге ролей (`agent-roles-catalog/`) поле `instructions` — это один большой системный промпт, который в JSON хранится как **одна длинная строка** с экранированными `\n`. Из‑за этого: - любая правка одного предложения показывает в дифф‑е изменение **всей строки целиком** (см. недавнюю правку блока «СТАЙЛ‑ШИТ» — весь промпт ушёл в дифф одной строкой); - редактировать длинный многострочный промпт внутри JSON‑строки неудобно и ошибкоопасно (ручные `\n`, экранирование кавычек). Цель — перевести файлы каталога на **YAML** и хранить `instructions` как **литеральный блок‑скаляр** (`|-`), чтобы: - диффы были построчными; - длинные промпты редактировались как обычный текст. ## Архитектурный контекст (важно) Каталог — это **данные, которые читает отдельный сервер** в рантайме. Сервер (`AiAgentRolesCatalogProvider`, `apps/server/src/core/ai-chat/roles/catalog/`) скачивает RAW‑файлы по базовому URL (`AI_AGENT_ROLES_CATALOG_URL`, задаётся per‑branch в CI: develop‑образ → develop‑URL, main‑образ → main‑URL), делает `JSON.parse` и прогоняет через **рукописные type‑guard'ы** (осознанно без zod / без зависимостей, т.к. вход недоверенный). Сейчас guard жёстко требует `typeof v.instructions === 'string'` — массив он бы отверг. Поэтому смена формата на YAML — это **не чистая правка данных**, а скоординированное изменение: данные + парсер сервера + валидатор `check.mjs` + тесты + доки, выезжающие одной веткой. ## Предлагаемое решение ### 1. Данные → YAML (5 файлов) - `index.json` → `index.yaml` - `bundles/editorial/ru.json` → `ru.yaml`, `bundles/editorial/en.json` → `en.yaml` - `bundles/research/ru.json` → `ru.yaml`, `bundles/research/en.json` → `en.yaml` - старые `.json` удалить. `instructions` хранить литеральным блок‑скаляром `|-` (chomp, без хвостового `\n`), чтобы **резолвнутый промпт был байт‑в‑байт прежним**. Остальные поля (`slug`, `emoji`, `name`, `description`, `launchMessage`, `version`, `schemaVersion`, `language`) — обычные скаляры. **Конвертацию делать программно через библиотеку `yaml`** (round‑trip‑safe), а не руками: ```js import YAML from 'yaml' const obj = JSON.parse(readFileSync(src, 'utf8')) const out = YAML.stringify(obj, { lineWidth: 0 }) // lineWidth:0 — не сворачивать длинные строки // инвариант: YAML.parse(out) должен deepEqual obj writeFileSync(dst, out) ``` `lineWidth: 0` отключает фолдинг (чтобы однострочные `description` не превратились в folded‑скаляр). Многострочные строки `yaml` сам пишет литеральным блоком. ### 2. Сервер `AiAgentRolesCatalogProvider` - добавить зависимость `yaml` в `apps/server/package.json`; - заменить `JSON.parse` на **безопасный** `parse` из `yaml` (дефолтная схема: только JSON‑совместимые типы и стандартные теги, **без** кастомных `!!`‑тегов / выполнения кода); - пути `index.json` → `index.yaml`, `bundles/${bundleId}/${language}.json` → `.yaml`; - обновить комментарии и тексты ошибок («not valid JSON» → «not valid YAML»); - **type‑guard'ы (`isCatalogRole` и др.) НЕ меняются** — после парсинга `instructions` остаётся `string`; - сохранить как есть: SSRF/path‑traversal‑гард (`^[a-z0-9-]+$`), `redirect: 'error'`, таймаут 10 c, потоковый лимит `MAX_BYTES = 1_000_000`. ### 3. Валидатор `agent-roles-catalog/scripts/check.mjs` - парсить YAML через `yaml` вместо `JSON.parse`; - пути `.json` → `.yaml`, обновить тексты ошибок; - зафиксировать `yaml` в `agent-roles-catalog/package.json` (devDependency). Нюанс: каталог **не** входит в pnpm‑workspace (`packages: ['apps/*','packages/*']`), но при `import 'yaml'` из `scripts/check.mjs` Node поднимается по дереву до корневого `node_modules/yaml` (он уже в сторе и станет прямой зависимостью сервера). Проверить, что в CI `check.mjs` находит модуль. ### 4. Тесты `ai-agent-roles-catalog.provider.spec.ts` - фикстуры `JSON.stringify(...)` → YAML (через `YAML.stringify` или YAML‑литералы); - тест «invalid JSON body» → «invalid YAML body» (подобрать заведомо невалидный YAML); - добавить тест: `instructions` как многострочный блок‑скаляр парсится и склейка строк корректна (равна ожидаемому промпту). ### 5. Доки и прочее - `agent-roles-catalog/README.md`: формат файлов (`<lang>.yaml`, `index.yaml`), примеры jsonc → yaml, «server fetches … .yaml», секция валидации; - `apps/server/src/core/ai-chat/roles/catalog/catalog-types.ts`: комментарии про `.json` → `.yaml`; - `.env.example` (~стр. 136): «appends /index.json and /bundles/<id>/<lang>.json» → `.yaml`; - **CI/Docker не меняются**: `AI_AGENT_ROLES_CATALOG_URL` указывает на директорию, расширение добавляет сервер; Dockerfile каталог не копирует. ## Версионирование и деплой‑связность - **`version` ролей в `index.yaml` НЕ бампать** — резолвнутый контент идентичен, меняется только сериализация. - **`schemaVersion`**: на обсуждение — оставить `1` (набор полей не изменился) либо бампнуть до `2` как сигнал смены сериализации. - **Деплой‑связность**: сервер читает RAW из той же ветки, из которой собран образ. Пока живёт старый образ, а данные в ветке уже YAML, старый сервер не распарсит → каталог временно недоступен. Митигировать одним из: - (а) на перехо��ный период принимать в провайдере **оба** формата (по расширению), затем удалить `.json` (рекомендуется, если есть долгоживущие старые образы); - (б) мёрджить код+данные вместе и пересобрать образ (короткое окно недоступности — приемлемо). ## Безопасность YAML‑парсинг недоверенного ввода: использовать `parse` с дефолтной схемой (без кастомных тегов / без `!!js`). Действующий лимит 1 МБ ограничивает DoS от расширения алиасов (billion laughs). Проверить/настроить лимиты парсера. Нужен security‑ревью этого пункта (есть метка `security`, если решим повесить). ## Затрагиваемые файлы (чеклист) - [ ] `agent-roles-catalog/index.yaml` (из `index.json`) - [ ] `agent-roles-catalog/bundles/editorial/{ru,en}.yaml` - [ ] `agent-roles-catalog/bundles/research/{ru,en}.yaml` - [ ] удалить старые `*.json` - [ ] `apps/server/.../catalog/ai-agent-roles-catalog.provider.ts` - [ ] `apps/server/.../catalog/ai-agent-roles-catalog.provider.spec.ts` - [ ] `apps/server/.../catalog/catalog-types.ts` (комментарии) - [ ] `apps/server/package.json` (+`yaml`) - [ ] `agent-roles-catalog/scripts/check.mjs` - [ ] `agent-roles-catalog/package.json` (+`yaml` devDep) - [ ] `agent-roles-catalog/README.md` - [ ] `.env.example` - [ ] `CHANGELOG.md` (запись о ротации формата) ## Критерии приёмки (DoD) - [ ] Все 5 файлов в YAML, старые `.json` удалены. - [ ] `node agent-roles-catalog/scripts/check.mjs` → `OK`. - [ ] Round‑trip: для каждого файла `YAML.parse(new)` deepEqual `JSON.parse(old)`. - [ ] Резолвнутый `instructions` каждой роли **байт‑в‑байт** совпадает с прежним. - [ ] `pnpm --filter server test` зелёный; провайдер парсит YAML, отвергает невалидный YAML и неверную схему как `BadGateway`, SSRF‑гард не тронут. - [ ] Линт/тайпчек зелёные; README и `.env.example` обновлены. ## Открытые вопросы 1. `schemaVersion` оставить `1` или бампнуть до `2`? 2. Деплой: вариант (а) переходная двойная поддержка форматов или (б) одновременный выезд? 3. Расширение файлов: `.yaml` (в репо есть `pnpm-workspace.yaml`) vs `.yml` (`crowdin.yml`, `docker-compose.yml`) — предлагаю `.yaml`. --- _Спроектировано на основе разбора кода `apps/server/src/core/ai-chat/roles/catalog/` и `agent-roles-catalog/`. Реализация требует ревью (формат данных + парсинг недоверенного ввода)._
Ghost added the refactorenhancement labels 2026-06-27 04:02:37 +03:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#229