diff --git a/project-specs.md b/project-specs.md index 8a2a0df..b83a000 100644 --- a/project-specs.md +++ b/project-specs.md @@ -298,6 +298,66 @@ script does not belong in the standard pipeline. - Lefthook runs the relevant `check:*` scripts on staged files in parallel for pre-commit. See `lefthook.yml`. +### CHECK TIERS — what runs where and why + +The `check:*` scripts are split across three execution contexts +based on **speed**, **scope** (staged files vs whole project), and +**side effects** (network, registry queries). The split is a +deliberate design choice; the rule of thumb is: + +- **pre-commit hook**: fast, file-scoped, offline, deterministic. + Catches what _you just changed_ in under a second or two. +- **`npm run check`**: full project audit. Slower, may query the + network, runs everything that's not in pre-commit. +- **CI build job**: authoritative. Runs `npm run check` and + `npm run test:ci` on every push and PR. The ground truth — + if CI passes, the codebase is clean. + +| Script | pre-commit | `npm run check` | CI build | CI publish | +| ----------------- | :--------: | :-------------: | :------: | :--------: | +| `check:tsc` | ✅ | ✅ | ✅ | — | +| `check:oxlint` | ✅ | ✅ | ✅ | — | +| `check:oxfmt` | ✅ | ✅ | ✅ | — | +| `check:cspell` | ✅ | ✅ | ✅ | — | +| `check:knip` | — | ✅ | ✅ | — | +| `check:outdated` | — | ✅ | ✅ | — | +| `publish:publint` | — | — | — | ✅ | +| `publish:attw` | — | — | — | ✅ | + +#### Why these splits? + +- **`check:tsc`, `check:oxlint`, `check:oxfmt`, `check:cspell`** + are in pre-commit because they are fast (~0.2–0.5s each), fully + offline, and naturally scope to staged files via the + `LEFTHOOK_FILES` env var convention. They give instant feedback + on what you typed. + +- **`check:knip` is NOT in pre-commit** because it scans the whole + project (not staged files) and takes ~4s. Adding it would + roughly triple pre-commit time. It's still fast enough to run + locally before pushing, and CI catches it regardless. + +- **`check:outdated` is NOT in pre-commit** because (a) it queries + the npm registry (~6.5s) which means network dependency in a + hook, (b) the result is advisory, not a pass/fail correctness + check, and (c) the same network call in CI or locally on demand + gives the same answer. It belongs in scheduled or on-demand + runs, not in a blocking hook. + +- **`publish:publint` and `publish:attw` are NOT in `check`** — + they belong to the `publish:` prefix (see above) because they + validate the _publishable artifact_ (`dist/`) and require a + fresh build. Running them on every commit would be wasteful + (and slow, `npm pack` is involved). They run in the CI + `publish` job immediately before `npm publish`. + +#### What to do before pushing + +Run `npm run check` locally. If you only trust pre-commit + CI, +know that anything `check:knip` or `check:outdated` would catch +will be caught by CI on the PR before merge (assuming branch +protection is configured to require CI to pass). + --- ## 6. Repository & CI/CD @@ -309,13 +369,19 @@ script does not belong in the standard pipeline. `actions/setup-node@v4` (with `node-version-file: .node-version`, `cache: npm`), `npm ci`, `npm run build`, `npm run check`, `npm run test:ci`, then upload `coverage/` as an artifact. + Runs the **full `check` chain** (all 6 `check:*` scripts) and + the test suite with coverage. This is the authoritative gate — + if it passes, the codebase is clean. See §5 ("CHECK TIERS") + for which checks run here vs. locally vs. pre-commit. - `publish` job: only on `refs/tags/*`, depends on `build`. Steps: `actions/checkout@v4`, `actions/setup-node@v4` (with `node-version-file: .node-version` and `registry-url`), `npm ci`, `npm run build`, `npm run publish:publint` (see §5 for why this is `publish:` and not `check:`), `npm run publish:attw`, and `npm publish --access public` - with `NODE_AUTH_TOKEN` from secrets. + with `NODE_AUTH_TOKEN` from secrets. Runs the **publish-tier + checks** that validate the publishable artifact before + publishing to npm. ## 7. Versioning & Publishing