Разработка
Сборка, шлюзы, конвенции, которые обеспечивает инструментарий, и почему тестам никогда не нужен ключ API.
git clone <this repo> && cd handbooks
pnpm install
pnpm build
pnpm testТребуются Node ≥ 20.11 и pnpm ≥ 9. Нативной компиляции нет.
Повседневные команды
pnpm build # tsc -b (composite project references)
pnpm build:watch
pnpm test # build + vitest
pnpm test:watch
pnpm check # the everyday gate — run this before committing
pnpm check:all # check + packaging + install + CLI smoke — what CI runs
pnpm check:cli # every subcommand and config layer, end to end, offlinepnpm check выполняет по порядку:
typecheck— исходники, затем тесты поtsconfig.tests.jsoncheck:workspace— структурные инварианты монорепозиторияlint— eslint по всему репозиторию, допускается ноль предупрежденийformat:check— prettiertest:coverage— vitest с попакетными порогами покрытия
Это намеренно быстрый вариант. pnpm check:all добавляет три более тяжёлых шлюза —
check:packaging (publint + are-the-types-wrong), check:install (упаковать одиннадцать
тарболов, установить их обычным npm, прогнать CLI) и check:cli (см. ниже), — которым
место в CI и перед релизом, а не в каждом локальном цикле.
Что покрывает check:cli
scripts/smoke-cli.sh прогоняет настоящий бинарник от начала до конца против
встроенной мок-LLM, проверяя коды возврата и артефакты по каждой подкоманде, каждому слою
конфигурации и — самое главное — по отказам.
- Каждая поверхность
--helpи неизвестная подкоманда, завершающаяся с кодом1 - Происхождение значений в
config,--jsonи--check, возвращающий2при отсутствии обязательного значения - Неверные значения перечислений / целых чисел / фаз, завершающиеся с
1, а не проваливающиеся к значению по умолчанию - Матрица генерации: подмножества фаз,
--resume,--detail deep,--synth-mode doctor,--llm-cache,--narrate-lang zh - Каждый формат рендеринга и падение
renderна пустом рабочем каталоге skill, отказывающийся от--out, который съел бы собственный вход;validate, возвращающий2apply, отказывающийся от неоднозначного якоря и от выхода за пределы пути; настоящийrollback, восстанавливающий байт в байтresyncс LLM и без него, а также пустой дифф, который чисто пропускается- Приоритет: окружение шелла над файлом конфигурации,
.env.<name>надhandbook.config.<name>.yaml, форма с областью видимости над плоской, пустое как незаданное, и ключ API, замаскированный в выводеconfig - Вменяемость артефактов: все ожидаемые файлы на месте, покрытие карточками полное, нет нераспределённых файлов, расход токенов записан
Модульные тесты мокают generateHandbook и его соседей, поэтому они не могут поймать
флаг, который корректно разрешается, а потом никуда не передаётся, неверный код возврата
или контракт артефакта, сломавшийся на стыке. А эта проверка может — и она полностью
офлайн, поэтому безопасна в CI.
pnpm check:cli
SMOKE_PORT=9123 pnpm check:cli # if port 8123 is takenХук pre-commit прогоняет форматтер и линтер только по проиндексированным файлам, а
commit-msg обеспечивает Conventional Commits.
Философия тестирования
Всё работает офлайн. Никакому тесту никогда не нужен ключ API.
- Потоки, зависящие от LLM, тестируются против
MockChatClient— списка правил, где побеждает первое совпадение, — и против встроенной мок-конечной точки HTTP для настоящего клиента. - Детерминированные пакеты тестируются напрямую. Тесты анализатора строят настоящие мини-репозитории во временных каталогах и проверяют настоящие узлы и рёбра; мокнутое дерево разбора не доказало бы ничего о грамматике.
- Путям отказа уделяется столько же внимания, сколько и счастливым: неразбираемые ответы, частичные пакеты, уровни деградации, прерывания посреди запуска, побеги из песочницы, неоднозначные якоря.
pnpm test # everything
pnpm exec vitest run packages/analyzer # one package
pnpm exec vitest run -t "dropped calls" # one test by name
pnpm test:coverageЧетыре конвенции, которые обеспечивает инструментарий
Версии живут в одном месте
Каждая сторонняя версия объявлена в каталоге pnpm-workspace.yaml; пакеты зависят от
"catalog:" и никогда не повторяют диапазон. Литеральный диапазон в манифесте роняет
pnpm check:workspace, и точно так же роняет его неиспользуемая запись в каталоге.
{ "dependencies": { "zod": "catalog:" } }При упаковке pnpm переписывает catalog: в разрешённый диапазон, поэтому потребители
никогда не видят этот протокол.
dist/ — публикуемая поверхность
Сборочные проекты исключают *.test.ts и *.test-helper.ts; tsconfig.tests.json
проверяет типы тестов с noEmit. Source maps исключены из тарбола, потому что они
называют исходники, которые никогда не публикуются. Тестовый артефакт под dist/
роняет проверку.
Пороги покрытия — попакетные
Одно число на весь репозиторий прячет то, что важно: при 86% в целом @handbooks/cli
сидит на 23%. У каждого пакета свой порог в vitest.config.ts, выставленный чуть ниже
измеренного, поэтому он работает храповиком.
Если ваше изменение повышает покрытие, поднимайте порог вместе с ним. Не расширяйте разрыв, чтобы красный прогон стал зелёным.
Тесты разрешают @handbooks/* в исходники, а не в dist
Иначе покрытие всего, что потребляется через границу пакета, не приписывается никуда —
core/src/util/hash.ts показывал 0%, пока конвейер вызывал его на каждом запуске.
Настоящий dist проверяется через tsc -b и через pnpm check:install, который
устанавливает упакованные тарболы обычным npm и прогоняет по ним CLI. Это более сильная
проверка dist, чем был модульный тест.
Структурные инварианты
scripts/check-workspace.mjs обеспечивает семь правил, каждое из которых репозиторий
нарушал по меньшей мере однажды:
- Ссылки проектов TypeScript в точности отражают зависимости рабочего пространства.
- Зависимости рабочего пространства используют протокол
workspace:и действительно существуют. - Корневой файл-решение ссылается на каждый пакет.
- Сборочные проекты исключают тесты, и в
dist/их нет ни одного. - Форма манифеста единообразна —
type,description,license,files,engines,exports,scripts,publishConfig. - Публикуемый пакет никогда не зависит от приватного.
- Сторонние версии живут в каталоге и больше нигде.
Генерируемые файлы
Три файла генерируются из реестра настроек и сравниваются байт в байт тестом на расхождение:
pnpm run config:docs
# writes .env.example
# docs/content/docs/reference/configuration.md
# handbook.config.example.yamlРучное редактирование любого из них роняет сборку. Меняйте вместо этого реестр
(packages/core/src/config/registry.ts) и перегенерируйте.
Тот же тест на расхождение проверяет также, что оба README называют каждый зарегистрированный язык и не ссылаются ни на один несуществующий pnpm-скрипт, и что каждая относительная ссылка в них указывает на файл, отслеживаемый git.
Сайт документации
cd docs
pnpm install
pnpm dev # → http://localhost:3000Next.js + Fumadocs, MDX-контент под docs/content/docs/. Он не входит в рабочее
пространство pnpm, поэтому корневой pnpm install полностью его игнорирует.
Диаграммы живут в assets/ в корне репозитория — оба README ссылаются на них оттуда — и
копируются в docs/public/diagrams/ во время сборки скриптом
docs/scripts/sync-generated.mjs. Не копируйте их руками; копия в gitignore ровно по этой
причине.
Конвенции коммитов
Conventional Commits, обеспечиваемые commitlint:
feat(analyzer): add a Kotlin generic-tier spec
fix(patcher): refuse an anchor that matches zero times
docs(cli): document the --env cascade
chore(deps): bump vitestИзменениям, затрагивающим публикуемый пакет, нужен changeset:
pnpm changesetКоммитьте этот файл вместе с кодом. См. Выпуск релиза.
Что где лежит
packages/<name>/src/ source
packages/<name>/src/*.test.ts tests, colocated
scripts/ repo tooling (workspace checks, doc generation, smoke tests)
examples/ the offline demo, the mock LLM server, the fixture project
assets/ diagrams referenced by both READMEs
docs/ the documentation site (a standalone Next.js app)
docs/internal/ the engineering journal — LOCAL ONLY, gitignoredКоды возврата и вывод
Что означает каждый код возврата, что попадает в stdout, а что в stderr, и как писать скрипты под то и другое.
Добавление языка
Язык обобщённого уровня — это декларативная спецификация, а не парсер. Язык полного уровня — небольшой интерфейс. Ни тому ни другому не нужна новая зависимость.