MVP-архитектура CLI twin (v0, черновик)
Цель MVP: за 1–2 недели получить пользу без сложного графа. Принцип «буханки»: только чтение файлов и TS AST, без базы данных и без LLM.
Поток
passports (*.md) ─┐
payload-types.ts ─┼─► extract ─► graph (in-memory) ─► checks ─► report (md/json)
blocks/*/config ──┤ │
components (*.tsx)┘ └─► graph.cache.json (для diff между запусками)
Модули
| Модуль | Что делает | Как |
|---|---|---|
extract/passports |
Читает frontmatter паспортов | gray-matter + zod-схема паспорта |
extract/types |
Берёт интерфейсы блоков из payload-types.ts: поля, optional, union, relation |
ts-morph (TS AST) |
extract/code |
Находит, какие поля блока реально читает компонент (props.title, деструктуризация) и его импорты |
ts-morph |
extract/tests |
Ищет ссылки REQ-* в тестах (в названиях или комментариях) |
regex |
graph |
Узлы: Requirement, Field, BlockType, Component, Test. Рёбра: implements, reads, covers, imports | простые Map |
checks |
Правила, см. ниже | чистые функции (graph) => Finding[] |
report |
twin-report.md и --json для CI, ненулевой код выхода при ошибках |
— |
cache |
Сохраняет граф в graph.cache.json; twin diff сравнивает с прошлым |
JSON |
Проверки MVP (по приоритету)
- Контракт полей: поле из паспорта отсутствует в
payload-types.tsили отличается тип/required → error. - Поле, которое компонент не читает: есть в типе, но компонент его не использует → warn (мёртвое поле или забытый рендер).
- Чтение несуществующего поля: компонент читает поле, которого нет в типе. Обычно ловит
tsc, но мы показываем, какое требование это ломает. - Требование без теста:
tests: []→ warn. - Блок без паспорта / паспорт без блока → warn.
- Загрязнение: импорт компонента попадает под
deps.deny→ error. (Можно отложить на v2.)
Impact analysis
twin impact <id|path>, например twin impact hero.cta или twin impact REQ-hero-02:
обход графа от узла и список затронутых компонентов, тестов и требований.
Про payload-types.ts как «естественный граф» (кратко, критично)
- Плюс: это генерируемый источник правды о схеме. Relation-поля дают рёбра между коллекциями, а union блоков в
layoutдаёт список всех блоков. - Минус: условная логика админки (
admin.condition) в типы не попадает. Отсюдаrequired_ifв паспорте и чтениеconfig.tsв v2. - Минус: если забыть
payload generate:types, типы устаревают. Поэтомуtwinсначала сверяет дату изменения типов с конфигами или сам запускает генерацию. - Минус: relation приходят как
string | Docв зависимости от depth. Проверка должна это учитывать, иначе будут ложные срабатывания. - Вывод: типы годятся как основа графа данных, а бизнес-смысл и условия держат паспорта.
Стек и структура
Node + TypeScript, ts-morph, gray-matter, zod, commander. Запуск: npx twin check локально и в CI.
twin/
src/extract/{passports,types,code,tests}.ts
src/graph.ts src/checks/*.ts src/report.ts src/cache.ts src/cli.ts
Порядок работ
- Схема паспорта (zod) + 2 паспорта на реальных блоках.
- Извлечение из
payload-types.ts+ проверка 1. Уже приносит пользу. - Извлечение из кода + проверки 2–3.
- Тесты и
impact. - Кэш,
diff,deps— v2.