Desarrollo
La compilación, los controles, las convenciones que impone el tooling y por qué las pruebas nunca necesitan una clave de API.
git clone <this repo> && cd handbooks
pnpm install
pnpm build
pnpm testRequiere Node ≥ 20.11 y pnpm ≥ 9. Sin compilación nativa.
Comandos del día a día
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 ejecuta, en este orden:
typecheck— las fuentes y luego las pruebas contratsconfig.tests.jsoncheck:workspace— las invariantes estructurales del monorepolint— eslint sobre todo el repositorio, cero advertencias toleradasformat:check— prettiertest:coverage— vitest con umbrales de cobertura por paquete
Es deliberadamente el rápido. pnpm check:all añade tres controles más pesados —
check:packaging (publint + are-the-types-wrong), check:install (empaqueta once
tarballs, los instala con npm a secas, ejecuta la CLI) y check:cli (más abajo) — que
corresponden a CI y a antes de una publicación, no a cada ciclo local.
Qué cubre check:cli
scripts/smoke-cli.sh ejecuta el binario real de extremo a extremo contra el LLM
simulado incluido, comprobando códigos de salida y artefactos en cada subcomando, cada capa
de configuración y — lo más importante — los rechazos.
- Todas las superficies de
--help, y un subcomando desconocido saliendo con1 - La procedencia de
config,--jsony--checksaliendo con2ante un valor requerido ausente - Valores de enum / entero / fase inválidos saliendo con
1en lugar de caer al valor por defecto - La matriz de generación: subconjuntos de fases,
--resume,--detail deep,--synth-mode doctor,--llm-cache,--narrate-lang zh - Todos los formatos de renderizado, y
renderfallando sobre un directorio de trabajo vacío skillrechazando un--outque se comería su propia entrada;validatesaliendo con2applyrechazando un ancla ambigua y un escape de ruta; unrollbackreal restaurando byte por byteresynccon y sin LLM, y un diff vacío que se omite limpiamente- Precedencia: el entorno del shell sobre el archivo de configuración,
.env.<name>sobrehandbook.config.<name>.yaml, con ámbito sobre plano, vacío-como-no-definido, y la clave de API enmascarada en la salida deconfig - Cordura de los artefactos: todos los archivos esperados presentes, cobertura de fichas completa, ningún archivo sin asignar, uso de tokens registrado
Las pruebas unitarias simulan generateHandbook y sus vecinos, así que no pueden detectar
un flag que se resuelve correctamente y luego nunca se propaga, un código de salida
equivocado o un contrato de artefacto que se rompió en la costura. Esto sí puede — y es
totalmente offline, así que es seguro en CI.
pnpm check:cli
SMOKE_PORT=9123 pnpm check:cli # if port 8123 is takenUn hook de pre-commit ejecuta el formateador y el linter solo sobre los archivos en el
índice, y commit-msg impone Conventional Commits.
Filosofía de las pruebas
Todo se ejecuta offline. Ninguna prueba necesita jamás una clave de API.
- Los flujos que dependen del LLM se prueban contra
MockChatClient— una lista de reglas, gana la primera coincidencia — y contra un endpoint HTTP simulado incluido para el cliente real. - Los paquetes deterministas se prueban directamente. Las pruebas del analizador construyen mini-repositorios reales en directorios temporales y comprueban nodos y aristas reales; un árbol de análisis simulado no demostraría nada sobre una gramática.
- Las rutas de fallo reciben la misma atención que las rutas felices: respuestas no parseables, lotes parciales, niveles de degradación, abortos a mitad de ejecución, escapes del sandbox, anclas ambiguas.
pnpm test # everything
pnpm exec vitest run packages/analyzer # one package
pnpm exec vitest run -t "dropped calls" # one test by name
pnpm test:coverageCuatro convenciones que impone el tooling
Las versiones viven en un único sitio
Cada versión de terceros se declara en el catálogo de pnpm-workspace.yaml; los paquetes
dependen de "catalog:" y nunca repiten un rango. Un rango literal en un manifiesto hace
fallar pnpm check:workspace, y una entrada de catálogo sin usar también.
{ "dependencies": { "zod": "catalog:" } }pnpm reescribe catalog: al rango resuelto al empaquetar, así que los consumidores nunca
ven el protocolo.
dist/ es la superficie publicada
Los proyectos de compilación excluyen *.test.ts y *.test-helper.ts; tsconfig.tests.json
comprueba los tipos de las pruebas con noEmit. Los source maps quedan fuera del tarball
porque nombran fuentes que nunca se publican. Un artefacto de prueba bajo dist/ hace
fallar el control.
Los umbrales de cobertura son por paquete
Un único número para todo el repositorio esconde lo que importa: con un 86% global,
@handbooks/cli está en el 23%. Cada paquete tiene su propio umbral en vitest.config.ts,
fijado justo por debajo de lo que mide, de modo que avanza como un trinquete.
Si tu cambio sube la cobertura, sube el umbral con ella. No amplíes la brecha para que pase una ejecución en rojo.
Las pruebas resuelven @handbooks/* al código fuente, no a dist
De lo contrario, la cobertura de todo lo que se consume cruzando el límite de un paquete no
se atribuye a ninguna parte — core/src/util/hash.ts medía 0% mientras el pipeline lo
llamaba en cada ejecución.
El dist real lo verifican tsc -b y pnpm check:install, que instala los tarballs
empaquetados con npm a secas y ejecuta la CLI contra ellos. Eso es un control sobre dist
más fuerte de lo que era una prueba unitaria.
Las invariantes estructurales
scripts/check-workspace.mjs impone siete reglas, cada una de las cuales el repositorio
violó al menos una vez:
- Las referencias de proyecto de TypeScript reflejan exactamente las dependencias del workspace.
- Las dependencias del workspace usan el protocolo
workspace:y existen de verdad. - El archivo de solución raíz referencia todos los paquetes.
- Los proyectos de compilación excluyen las pruebas, y
dist/no contiene ninguna. - La forma del manifiesto es uniforme —
type,description,license,files,engines,exports,scripts,publishConfig. - Un paquete publicable nunca depende de uno privado.
- Las versiones de terceros viven en el catálogo y en ningún otro sitio.
Archivos generados
Tres archivos se generan a partir del registro de configuración y se comparan byte por byte mediante una prueba de deriva:
pnpm run config:docs
# writes .env.example
# docs/content/docs/reference/configuration.md
# handbook.config.example.yamlEditar a mano cualquiera de ellos hace fallar la build. Cambia el registro en su lugar
(packages/core/src/config/registry.ts) y regenera.
Esa misma prueba de deriva comprueba también que ambos READMEs nombran todos los lenguajes registrados y no referencian ningún script de pnpm inexistente, y que cada enlace relativo en ellos apunta a un archivo rastreado por git.
El sitio de documentación
cd docs
pnpm install
pnpm dev # → http://localhost:3000Next.js + Fumadocs, contenido MDX bajo docs/content/docs/. No forma parte del
workspace de pnpm, así que un pnpm install en la raíz lo ignora por completo.
Los diagramas viven en assets/ en la raíz del repositorio — ambos READMEs los referencian
desde ahí — y se copian a docs/public/diagrams/ en tiempo de compilación mediante
docs/scripts/sync-generated.mjs. No los copies a mano; la copia está en gitignore
exactamente por esa razón.
Convenciones de commit
Conventional Commits, impuestos por 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 vitestLos cambios que afectan a un paquete publicado necesitan un changeset:
pnpm changesetCommitea ese archivo junto con el código. Consulta Publicar versiones.
Dónde vive cada cosa
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, gitignoredCódigos de salida y salida por consola
Qué significa cada código de salida, qué va a stdout frente a stderr y cómo escribir scripts contra ambos.
Añadir un lenguaje
Un lenguaje de nivel genérico es una especificación declarativa, no un parser. Uno de nivel completo es una interfaz pequeña. Ninguno de los dos necesita una nueva dependencia.