refactor(agent-roles-catalog): YAML catalog with block-scalar instructions (#229) #231
Reference in New Issue
Block a user
Delete Branch "feat/229-catalog-yaml"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #229.
Каталог ролей переведён на YAML:
index.yaml+bundles/<id>/<lang>.yaml,instructions— литеральный блок-скаляр (|-) → построчные диффы, редактируется как обычный текст. Конвертация программная сdeepStrictEqualround-trip против старого JSON (контент байт-в-байт, версии не бампнуты).Загрузчик (
ai-agent-roles-catalog.provider.ts):parseYamlчерезyaml@2.8.3(safe JSON-схема,strict,maxAliasCountот billion-laughs); пути fetch →.yaml; size-cap/redirect:error/timeout/SSRF-гард без изменений. Валидаторcheck.mjs, README,.env.exampleобновлены. Тесты: YAML-парсинг, round-trip блок-скаляра, malformed→BadGateway, дубль-ключ→BadGateway, size-cap/path-traversal на.yaml. tsc + 124 jest + check.mjs + build зелёные.Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
🤖 Generated with Claude Code
Code review — PR #231: миграция каталога agent-roles JSON → YAML
Вердикт: Request changes (нужны правки).
Сама конвертация сделана аккуратно: YAML побайтово эквивалентен JSON на точке ветвления (
deepStrictEqual5/5,node scripts/check.mjsзелёный), парсинг безопасен (safe-схемаyaml, без выполнения кода;maxAliasCountгасит billion-laughs независимо от 1 MB cap; SSRF/redirect:error/timeout/size-cap/SEGMENT_REнетронуты; секретов в диффе нет). Блокируют merge две вещи: ветка отстала отdevelopи при слиянии теряется обновление ролиfact-checker(v3), а также остался один устаревший комментарий «untrusted JSON».Must fix before merge
[regressions] Перебазировать ветку на актуальный
developи перенести в YAML контентfact-checkerv3, затем обновитьcontent-hashes.json. —agent-roles-catalog/index.yaml,bundles/editorial/{en,ru}.yaml,scripts/content-hashes.jsonВетка отрезана от merge-base
904f7b4и конвертирует старый (v2) контентfact-checker. Тем временем вdevelopуже влит коммит89edddc5(«fact-checker flags errors instead of confirming facts»):index.jsonподнял версию 2→3, переписаныdescription+instructionsвeditorial/{en,ru}.json, обновлёнcontent-hashes.json. Этот PR удаляет те.json, поэтому 3-way merge конфликтует modify/delete именно на этих файлах (отсюдаmergeable:false). Если разрешить конфликт «просто выбросить .json», тоindex.yamlостанется сfact-checker version: 2и старым текстом — улучшение изdevelopпропадёт. При этомcontent-hashes.json(его PR не трогает) на пост-merge дереве ждёт v3, поэтомуcheck.mjsупадёт (content changed but version was not bumped (still 3)) — молча в прод стухшее не уедет, но подсказка самого чека («bump version, then --update-hashes») при наивном следовании зафиксирует старый v2-хеш под новой версией и узаконит регресс зелёным чеком. Правильный путь: rebase наdevelop, регенерировать YAML из v3-JSON (вindex.yamlfact-checker version: 3), затемnode scripts/check.mjs --update-hashesи закоммитить обновлённыйcontent-hashes.jsonв этом же PR.[documentation] Заменить устаревший комментарий «untrusted JSON» → «untrusted YAML». —
apps/server/src/core/ai-chat/roles/ai-agent-roles.service.ts:190Строка
// Catalog (admin-only). The catalog is curated, untrusted JSON fetched +— единственная оставшаяся в server-коде ссылка на старый формат (проверено grep'ом по всему репозиторию;catalog-types.tsиprovider.tsPR уже поправил на YAML). Каталог теперь YAML, комментарий противоречит коду. ПоменятьJSON→YAML.[suggestion][documentation] Уточнить docstring
parseYamlпроstrict: true. —apps/server/src/core/ai-chat/roles/catalog/ai-agent-roles-catalog.provider.ts:87-90Комментарий утверждает, что
strict: true«rejects unknown custom tags». По факту вyaml@2.8.3неизвестный тег (!!js/function …) не бросает — выдаёт warning и резолвит узел в обычный скаляр (а дальше его отсекает hand-written type guard). Реальная гарантия безопасности (core-схема не конструирует не-JSON типы и не исполняет код; type guard режет лишнее) сохраняется, и часть про duplicate keys верна — переформулировать только пункт про custom tags, чтобы не вводить в заблуждение про security-critical парсинг.[suggestion][conventions] Задокументировать, что
check.mjsберётyamlиз корневогоnode_modules(черезshamefully-hoist), а не из своего пакета. —agent-roles-catalog/scripts/check.mjs:11-13,agent-roles-catalog/package.json:7-9agent-roles-catalog/вне pnpm-workspace (pnpm-workspace.yaml= толькоapps/*,packages/*) и не имеет своегоnode_modules.import "yaml"резолвится вверх по дереву в корневойnode_modules/yaml— но он там есть только потому, чтоapps/serverобъявляетyaml, а.npmrc(shamefully-hoist=true) поднимает его в корень. Добавленный блокdevDependencies.yamlвagent-roles-catalog/package.jsonпри этом инертен (pnpm его не ставит). В чистом checkout без корневогоpnpm installвалидатор упал быERR_MODULE_NOT_FOUND; blast-radius низкий (в CI/Dockercheck.mjsне вызывается). Добавить в README (секция «Validating») строку, что нужен предварительныйpnpm installв корне; либо объявитьyamlв корневомpackage.json, чтобы не зависеть от хойстинга.Test coverage
Покрыто осмысленными ассертами: happy-path YAML (index + bundle), malformed-YAML→BadGateway, duplicate-key (
strict)→BadGateway, malformed-shape→BadGateway, round-trip блок-скаляраinstructions(точное многострочное сравнение), size-cap / path-traversal на.yaml, fallbackresponse.text(). Пробелы по новой логике этого PR:[warning] Нет теста на новый guard
maxAliasCount(alias-bomb). —…/ai-agent-roles-catalog.provider.ts:95maxAliasCount: 100добавлен как защита от billion-laughs (об этом прямо сказано в docstring), но ни один тест не скармливает парсеру alias-тяжёлый документ. Добавить тест: тело YAML с вложенными алиасами свыше лимита →fetchIndex()rejectBadGatewayException(заодно проверит, что throw мапится, а не утекает как 500).[warning] Нет теста на YAML-коэрцию неэкранированного скаляра в не-строку. —
…/ai-agent-roles-catalog.provider.spec.ts:299-311Переход с JSON на YAML рождает новый failure-mode, которого у JSON не было:
slug: 12/name: no(без кавычек) YAML приводит к числу/булеву, а защищает только type guard. Единственный malformed-shape тест использует явно не-строковое значение (schemaVersion: 'x') — это и JSON мог бы. Добавить кейс с сырой YAML-строкой (не черезstringifyYaml, иначе значение перекавычится), где строковое поле задано неэкранированным скаляром, и проверить rejectBadGatewayException.[suggestion] Усилить ассерт на пустое тело→null→reject. —
…/ai-agent-roles-catalog.provider.spec.ts:159-168Смена парсера меняет поведение:
JSON.parse('')бросал,parseYamlDoc('')возвращаетnull(далее отсекается guard'ом). Сейчас это лишь побочно задето в тесте про redirect черезrejects.toBeDefined()— пройдёт даже если пустое тело утечёт как 500. Добавить отдельный тест: пустое/whitespace тело → rejectBadGatewayException(/malformed/i).Architecture & design (forward-looking, non-blocking)
None. Раздвоение валидации (runtime type-guards в
provider.tsvs кросс-файловые проверки + version-gate вcheck.mjs) — преджестирующая, когерентная архитектура в раздельных пакетах; этот PR её не ухудшает. Зависимостьyamlоправдана (уже была транзитивной — promote без новой supply-chain поверхности), мёртвого кода/лишних тестов не внесено.🤖 Multi-aspect review (security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture). Generated with Claude Code.
0ec954b382to38a863e5f7Provider tests only exercised synthetic stringifyYaml fixtures, so a hand-conversion error in one of the 6 real catalog files (index.yaml, bundles/{editorial,research}/{en,ru}.yaml) — a stray quote/colon in a description, a broken emoji/arrow, a block-scalar indent slip that silently changes or drops instructions — was caught by no automated test. scripts/check.mjs is the only other guard and is wired into no CI/turbo/husky step. Add a real-files test block that reads each shipped file off disk, parses it with the SAME options the provider uses (strict: true, maxAliasCount: 100), and validates it through the provider's own exported type guards (isCatalogIndex / isCatalogBundleFile / isCatalogRole). It is driven from the real index so new bundles/langs are auto-covered, asserts the editorial bundle still ships fact-checker, and requires every declared role to be present with non-empty instructions/name in each language file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>