Handbooks
Участие в разработке

Разработка

Сборка, шлюзы, конвенции, которые обеспечивает инструментарий, и почему тестам никогда не нужен ключ 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, offline

pnpm check выполняет по порядку:

  1. typecheck — исходники, затем тесты по tsconfig.tests.json
  2. check:workspace — структурные инварианты монорепозитория
  3. lint — eslint по всему репозиторию, допускается ноль предупреждений
  4. format:check — prettier
  5. test: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, возвращающий 2
  • apply, отказывающийся от неоднозначного якоря и от выхода за пределы пути; настоящий 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 обеспечивает семь правил, каждое из которых репозиторий нарушал по меньшей мере однажды:

  1. Ссылки проектов TypeScript в точности отражают зависимости рабочего пространства.
  2. Зависимости рабочего пространства используют протокол workspace: и действительно существуют.
  3. Корневой файл-решение ссылается на каждый пакет.
  4. Сборочные проекты исключают тесты, и в dist/ их нет ни одного.
  5. Форма манифеста единообразна — type, description, license, files, engines, exports, scripts, publishConfig.
  6. Публикуемый пакет никогда не зависит от приватного.
  7. Сторонние версии живут в каталоге и больше нигде.

Генерируемые файлы

Три файла генерируются из реестра настроек и сравниваются байт в байт тестом на расхождение:

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:3000

Next.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

На этой странице