Tooling and the quality gate
One command decides whether a change is ready: pnpm verify. Two things on this page keep that command honest. Work that belongs to continuous integration stays off the local machine, and no rule is allowed to look enforced when only a reviewer will ever catch it.
pnpm verify and its lanes
Section titled “pnpm verify and its lanes”pnpm verify runs its lanes in two rounds, parallel inside each, through concurrently. It reports every failure without aborting the siblings, so one run names every failure and not just the first one it met. Continuous integration invokes the same command literally, so a green local run means something. But when a green local verify turns into a red CI run, the bug is in verify and gets fixed as one.
| Lane | Command | What it checks |
|---|---|---|
format |
pnpm format:check |
Prettier formatting and import group order across the tree |
lint |
pnpm lint |
oxlint over every project |
typecheck |
pnpm typecheck |
TypeScript for every workspace project |
tools |
pnpm typecheck:tools |
TypeScript for the repository scripts under tools/ |
test |
pnpm test |
Vitest for every project |
packages |
pnpm lint:packages |
what a consumer would receive from each publishable package |
generated |
pnpm generate:runtime --check |
drift between the generated runtime bundle and its sources |
tables |
pnpm generate:idna --check |
the generated UTS #46 tables against the engine’s Unicode |
docs |
pnpm validate:docs |
the documentation against the tree it describes |
site |
pnpm validate:site |
the built documentation site, where every reference to a page of it has to resolve |
Every lane but two starts in the first round. typecheck and test wait for the second, because both read what the packages lane writes: typecheck follows the project references into packages/*/dist, and the tests import each package through the import condition of its exports map. That lane empties those directories before it refills them, so a single round would land a delete in the middle of a read, and the lane that failed would be the one with nothing wrong with it.
Beyond the lanes, the gate scans the tree for files ending in .test.ts, reports them straight away, and fails the run once the lanes have settled. .spec.ts is the only test suffix here, and no lane reports the other one. Every Vitest config collects src/**/*.spec.ts alone, and the .oxlintrc.json override carrying the test rules matches that suffix too, so a file with the other suffix is never collected and never meets a test rule: its assertions never run and every lane stays green. What it costs beyond that depends on where it sits. Under packages/, tsconfig.lib.json excludes the suffix and each manifest drops it from the files array, so nothing typechecks it, it never compiles into dist and no consumer receives it. Under apps/ and examples/ nothing excludes it, so the build typechecks a test nobody runs and compiles it into that project’s dist.
pnpm format is the mutating counterpart of the format lane and the only command in the set that rewrites files.
Documentation lane
Section titled “Documentation lane”Documentation is the part of a repository that rots without anything going red, so tools/docs/validate.ts gives it something that does. It reads the Markdown under docs/ and examples/, plus the pages GitHub surfaces at the repository root, and checks each of them against the tree they describe.
Every relative link has to resolve to a file that exists. But a relative link out of docs/ fails even when the file is there, because the site publishes that directory alone: the link resolves inside the repository and answers 404 on the site. Anything outside docs/ is named by its URL. apps/docs/src/sidebar.js holds the site’s reading order, and the lane compares that order against the tree in both directions. A page the sidebar never reaches is a failure, and so is an entry pointing at a page that does not exist.
A link written as a URL of the published site is checked too, and README.md writes every one of its documentation links that way so that a reader following one lands on the rendered page. The lane resolves such a URL back to the page under docs/ and checks the anchor on it, so a renamed page or a renamed heading fails here rather than in somebody’s browser. apps/docs/src/site.js holds the origin both the lane and the Astro config read.
Pages that list something the code owns go stale in silence, so the lane compares each such list against its source. The primitives map has to point at code roots and documentation paths that exist, with a unique identifier per entry and a summary of one line inside the cap. The concept map beside it on the architecture page has to name paths that exist. In the operator guide, the settings table has to hold exactly the environment variables the daemon declares, in both directions.
A relative link that rots is a documentation bug of the same weight as a type error, so it fails the same gate.
Git hooks
Section titled “Git hooks”Hooks are installed by husky through the root prepare script and stay affected-scoped, so a commit never pays for the whole tree.
| Hook | What runs |
|---|---|
pre-commit |
lint-staged: formats staged files, then lints and typechecks affected projects |
commit-msg |
commitlint with the conventional config |
pre-push |
affected lint, typecheck and tests against origin/main |
The full gate belongs to continuous integration. Do not widen a hook until a class of breakage proves the local cost worth paying, because a slow hook is a hook people learn to pass --no-verify to.
Continuous integration
Section titled “Continuous integration”ci.yml runs pnpm verify on the Node line the project declares, on every pull request and every push to main, and no path filter narrows it. One of its lanes builds the documentation site and reads what the build produced. So a change to a page passes the same gate a change to a module passes. Each workflow beside it is filtered to the changes it has an opinion about. compatibility.yml runs the testbed suites that prove compatibility with the upstream package, and steps aside only for a change touching prose alone. workflows.yml runs actionlint for syntax and expression types and zizmor for the security shapes a workflow can fall into, and both linters read only .github. pages.yml deploys the site, and only a change reaching the book or the app that builds it can produce a different one.
A filtered workflow reports no status on a change it skips. Name one as a required check and such a change waits forever on a status that never arrives. ci.yml is the workflow belonging in that position, because it runs on every change.
nightly.yml takes what a per-change gate has no minutes for: the fault and soak families, the containment corpus on a runner nobody warmed, and a full verify on the newest Node line. That last one is an early warning. A failure there names something the next Node line has broken, ahead of the day this project moves onto it.
- Pin every action by commit SHA, keep
permissionsminimal, and do not persist credentials. Dependabot keeps the pins fresh. - Keep workflow YAML thin, and name a job for what it does, not for what it is called elsewhere.
Runtime floor and workspace
Section titled “Runtime floor and workspace”engineStrictinpnpm-workspace.yamlenforces the runtime floor at install. With it set,pnpm installrefuses a Node outside the declaredenginesrange, and refuses a dependency that declares an incompatibility too. pnpm’s settings page says a project’s ownenginesfield fails the install with or without the setting. It does not. Without the setting the install prints a warning and exits 0. Runtime-neutral packages declare no range at all, by decision 0002, and the publishing rule below rests on that..nvmrccarries that floor as a bare major for the tools that read a file, the CI Node setup among them.packageManagerdeclares the pnpm version, and pnpm’spmOnFaildefault downloads and runs that version instead of whatever happens to be on the path.- Nx targets are inferred from the tool configuration; there are no project.json files.
buildcomes fromtsconfig.lib.json,typecheckfromtsconfig.json,testfromvitest.config.mts,lintfrom a package.json script. - TypeScript project references are generated.
pnpm nx syncwrites them from the project graph andnx sync:checkproves they are current, so a hand-editedreferencesarray survives only until the next person runs either one.
Publishing
Section titled “Publishing”Everything below is about what a package contains. Cutting a version and sending it out is Releasing.
- Published packages ship both
distandsrc, excluding specs and build metadata. Shipping sources gives consumers working sourcemaps and readable TypeScript stack traces. - The
packageslane is the rehearsal for a release. It builds each publishable package from a cleandist, runs publint over the manifest, then packs the tarball and asksattwwhat a consumer’s own module resolution finds inside it. A manifest can be correct while the tarball is not. So both halves run. - Every publishable package builds with
isolatedDeclarations. Runtime-neutral packages pin"types": []so host ambient types can never leak into a package that has to run anywhere. - The example host depends on the protocol package and the remote executor, never on the Node core. Reaching for the core would stop it proving that a host can live entirely on the wire contract.
Repository scripts
Section titled “Repository scripts”- Keep package.json scripts to one line that hands off to a file under
tools/. Real logic never sits inline in a script field, where nothing lints it and nothing types it. - Write repository scripts as plain TypeScript that Node runs directly, with no build step and no loader flag.
- Treat
tools/as deliberately not a workspace project. The roottsconfig-tools.jsontypes it and thetoolslane checks it. - Prefer a short tool config inside package.json over one more file at the repository root. A separate config file earns its place only by carrying something a JSON block cannot, such as a comment next to a rule.
Git conventions
Section titled “Git conventions”- Write conventional commits. commitlint enforces them in the
commit-msghook. - Write commit bodies as flowing paragraphs, one line per paragraph. Never hard-wrap at a column; the commitlint body and footer length rules are switched off for exactly this reason.
- Carry the contributor’s authorship only. Never add a co-author trailer or a tool-attribution trailer.
- Squash-merge only. The branch history is a draft and the main history is the record.
- Keep process artifacts out of the committed tree. Working notes, gate verdicts and trackers are not product.
Review
Section titled “Review”- Weigh every review finding, and never apply one wholesale. A finding can deserve a fix, a written refutation, or a deferral, and choosing between them is part of the review and not a way out of it.
- Fix a defect inside the change that uncovered it.
- Stop and report when a directive cannot be executed. Never substitute a workaround for an instruction that could not be carried out.
Enforced or written
Section titled “Enforced or written”The lane table names a checker for every rule that has one. But every other rule on this page holds by agreement alone, so it holds exactly as long as reviewers keep asking for it.