Context
CI runs yarn test (lint + types + unit) on every push and PR, but there are no local hooks today. Lint, type, and test failures surface only after pushing — sometimes only after a PR review cycle reveals a red check. The feedback loop is workable but slow, and it's a routine source of "fix typo" / "fix lint" follow-up commits in PR history. Coverage is also tracked (vitest baseline captured in #130) but no threshold is enforced, so coverage can drift downward silently between refactors.
Task
Implement the mainstream three-tier check pattern. Pre-commit runs lint-staged on the staged files only (ESLint --fix + Prettier --write) so style and lint problems get caught before they enter history. Pre-push runs yarn test — the same lint + types + unit tests CI already runs, just shifted left so failures surface locally before the push lands. CI gains a coverage threshold check using vitest's built-in coverage.thresholds, with values seeded at the current baseline so coverage can only ratchet upward.
Scope:
- Add
husky and lint-staged as dev dependencies. Run yarn husky init to set up the .husky/ directory and the prepare script in package.json. The prepare script ensures hooks install automatically for anyone running yarn install.
- Add
.husky/pre-commit containing yarn lint-staged.
- Add
.husky/pre-push containing yarn test.
- Add a
lint-staged block to package.json covering *.{ts,tsx} (ESLint --fix + Prettier --write) and *.{md,json,yml,yaml} (Prettier --write only).
- Add a
coverage.thresholds block to the vitest config (lines, functions, branches, statements). Run yarn test:coverage once to capture the current baseline before setting thresholds 1–2 % below those numbers — the ratchet pattern. Coverage can only go up; if it drops, CI fails.
- Add a
yarn test:coverage script to package.json if it doesn't already exist.
- Add a CI step in
.github/workflows/test-and-deploy.yml that runs yarn test:coverage and fails on threshold violation — either a new job or an extra step in the existing test job.
- Add a
Hooks and feedback loop subsection to CONTRIBUTING.md describing the three tiers, the git push --no-verify escape hatch, and how to debug a hook that's gating a legitimate push.
Notes:
Husky is the de-facto JavaScript standard (~30k stars, stable API). simple-git-hooks is a lighter alternative if dependency footprint matters more than ecosystem familiarity — flag this in the PR description so reviewers can express a preference. Coverage thresholds belong in CI only: running coverage in pre-push roughly doubles test runtime, slow enough that contributors will start reaching for git push --no-verify and the hook becomes functionally dead. Threshold values must be the current baseline, not aspirational — aspirational thresholds either fail constantly (and get bypassed) or hide a smaller gap than reality (and don't catch regressions). If pre-push grows beyond ~10 seconds for typical changes, switch the pre-push hook to vitest --changed so only tests for touched files run locally; CI continues to run the full suite.
Acceptance hinges on four measurable things: pre-commit completes sub-second on a 1–3 file change; pre-push runs the full yarn test and blocks on failure; CI fails on a coverage drop below baseline with a clear message naming the offending threshold; the prepare script installs hooks automatically on a fresh clone (verify in a clean container or rm -rf node_modules .husky && yarn install).
Context
CI runs
yarn test(lint + types + unit) on every push and PR, but there are no local hooks today. Lint, type, and test failures surface only after pushing — sometimes only after a PR review cycle reveals a red check. The feedback loop is workable but slow, and it's a routine source of "fix typo" / "fix lint" follow-up commits in PR history. Coverage is also tracked (vitest baseline captured in #130) but no threshold is enforced, so coverage can drift downward silently between refactors.Task
Implement the mainstream three-tier check pattern. Pre-commit runs
lint-stagedon the staged files only (ESLint--fix+ Prettier--write) so style and lint problems get caught before they enter history. Pre-push runsyarn test— the same lint + types + unit tests CI already runs, just shifted left so failures surface locally before the push lands. CI gains a coverage threshold check using vitest's built-incoverage.thresholds, with values seeded at the current baseline so coverage can only ratchet upward.Scope:
huskyandlint-stagedas dev dependencies. Runyarn husky initto set up the.husky/directory and thepreparescript inpackage.json. Thepreparescript ensures hooks install automatically for anyone runningyarn install..husky/pre-commitcontainingyarn lint-staged..husky/pre-pushcontainingyarn test.lint-stagedblock topackage.jsoncovering*.{ts,tsx}(ESLint--fix+ Prettier--write) and*.{md,json,yml,yaml}(Prettier--writeonly).coverage.thresholdsblock to the vitest config (lines, functions, branches, statements). Runyarn test:coverageonce to capture the current baseline before setting thresholds 1–2 % below those numbers — the ratchet pattern. Coverage can only go up; if it drops, CI fails.yarn test:coveragescript topackage.jsonif it doesn't already exist..github/workflows/test-and-deploy.ymlthat runsyarn test:coverageand fails on threshold violation — either a new job or an extra step in the existingtestjob.Hooks and feedback loopsubsection toCONTRIBUTING.mddescribing the three tiers, thegit push --no-verifyescape hatch, and how to debug a hook that's gating a legitimate push.Notes:
Husky is the de-facto JavaScript standard (~30k stars, stable API).
simple-git-hooksis a lighter alternative if dependency footprint matters more than ecosystem familiarity — flag this in the PR description so reviewers can express a preference. Coverage thresholds belong in CI only: running coverage in pre-push roughly doubles test runtime, slow enough that contributors will start reaching forgit push --no-verifyand the hook becomes functionally dead. Threshold values must be the current baseline, not aspirational — aspirational thresholds either fail constantly (and get bypassed) or hide a smaller gap than reality (and don't catch regressions). If pre-push grows beyond ~10 seconds for typical changes, switch the pre-push hook tovitest --changedso only tests for touched files run locally; CI continues to run the full suite.Acceptance hinges on four measurable things: pre-commit completes sub-second on a 1–3 file change; pre-push runs the full
yarn testand blocks on failure; CI fails on a coverage drop below baseline with a clear message naming the offending threshold; thepreparescript installs hooks automatically on a fresh clone (verify in a clean container orrm -rf node_modules .husky && yarn install).