The pre-commit hook is file-scoped (LEFTHOOK_FILES), so it can't
naturally run the test suite. Pre-push is the right tier for it:
- Runs after all commits are made but before the push leaves
the machine, catching regressions that span multiple commits
- ~3.5s including the tsc step (negligible vs the typical push
round-trip to CI)
- Offline and deterministic, same philosophy as pre-commit
lefthook.yml: new pre-push section, sequential (parallel: false
since there's only one command, but the explicit value documents
the intent that this hook runs commands in order rather than
racing).
project-specs.md: updated the CHECK TIERS table to add the
pre-push column with in it. Updated the rule-of-thumb
list to include the pre-push tier. Updated the 'why' notes to
explain why lives in pre-push rather than pre-commit
(LEFTHOOK_FILES doesn't apply to the test runner).
Adds a 'CHECK TIERS' subsection under the existing HOOKS section
in project-specs.md that explicitly documents:
- Where each check runs (pre-commit, npm run check, CI build, CI publish)
- A summary table mapping scripts to execution contexts
- The rationale for each split (speed, scope, side effects)
- Why check:knip is not in pre-commit (~4s, whole project)
- Why check:outdated is not in pre-commit (network dep, advisory)
- Why publish:* is not in check (validates dist/, needs build)
- Cross-reference from the CI/CD section back to the tiers table
The split is a deliberate design choice: pre-commit is the fast
safety net for what you just changed, npm run check is the full
local audit, CI is authoritative. Documenting it makes the
rationale explicit and stops anyone from re-adding the slower
checks to the hook.
Cleaner separation of concerns:
- options.typeAware: true in .oxlintrc.json activates type-aware
rules declaratively (equivalent to --type-aware CLI flag, but
the script command stays clean: just 'oxlint ...')
- Remove the 3 type-aware rule disables from .oxlintrc.json
- Add source-level oxlint-disable directives instead:
- 4x typescript/no-unsafe-type-assertion (pattern.ts: keysMatch
Object.keys() cast, candidate[] cast; structuralMatcher value
as S cast; match.ts: handler as ... cast in nextCases)
- 1x typescript/no-unnecessary-type-parameters (pattern.ts:
keysMatch <S extends object>)
- 1x file-level typescript/no-floating-promises in index.test.ts
(expectTypeOf() is a sync type-assertion library that the
type-aware linter misidentifies)
Disabling rules at the source (next to the line that needs the
exemption) documents intent more clearly than a global config
override, and makes the trade-off visible to anyone reading the
code. Re-enabling a rule in the future only requires removing the
inline comment, not editing a central config.
Activates type-aware rules via oxlint --type-aware, backed by
oxlint-tsgolint (TypeScript-Go). Catches unsafe type assertions,
unnecessary type parameters, and other issues regular oxlint
cannot see.
- Add oxlint-tsgolint devDep + 6 platform-specific native bindings
as optionalDependencies (same pattern as oxlint)
- Add --type-aware flag to check:oxlint
- Add 6 @oxlint-tsgolint/* platforms to check:outdated ignore list
- Disable 3 type-aware rules in .oxlintrc.json with rationale:
- typescript/no-unsafe-type-assertion, typescript/no-unnecessary-type-parameters:
fire on legitimate generic type machinery in keysMatch/MatchBuilder
that needs type-system restructuring (deferred to a follow-up)
- typescript/no-floating-promises in test files: expectTypeOf() is
a sync type-assertion library that oxlint-tsgolint misidentifies
Code simplifications enabled by the new strict checks:
- src/match.ts: drop value as unknown casts (T is already assignable
to unknown) and the redundant run(value) as R cast
- src/index.test.ts: drop unnecessary 'X' as 'X | Y' assertions in
match<...>(...) calls (literals are already assignable to the union)
Documentation updates in project-specs.md and README.md. Add
'tsgolint' to cspell word list.
- Add check:knip using --include dependencies,exports,files
(skips the noisy 'types' category, which produces false positives
for libraries whose exported types are part of the public API)
- Remove tslib and type-fest (both caught as unused by knip)
- Add 'knip' to cspell word list
- No knip config file: the --include flag keeps the scope targeted
without boilerplate, matching the 'keep it simple' principle
- Document in project-specs.md (CHECK section) and README.md
Validates the emitted .d.ts declarations against multiple TypeScript
module-resolution scenarios. Same rationale as publint: it validates
the publishable artifact, not the source, so it belongs in the
publish: prefix, not check:.
- Add publish:attw script using --profile esm-only (the package is
intentionally ESM-only; CJS resolution is out of scope by design)
- Add step to CI publish job, after publish:publint and before
npm publish
- Document the script and the esm-only rationale in project-specs.md
and README.md
- Add 'attw' and 'arethetypeswrong' to cspell word list
- Remove unused 'stricter' word that was added speculatively before
publint validates the publishable artifact (dist/ vs package.json),
not the source. It should run only at publish time, in the CI publish
job, not on every commit.
- Rename check:publint -> publish:publint
- Remove from 'check' chain
- Add 'publish:publint' as a step in the CI publish job, right
before 'npm publish'
- Introduce a 'publish:' script prefix for publish-time-only scripts
- Document the prefix convention (check:, fix:, test:, publish:)
in project-specs.md (as a top-level subsection under Scripts)
and in the README
- Add 'publint' and 'stricter' to cspell word list
A new script should pick the prefix that matches its lifecycle, not
invent a new one. The 'publish:' prefix has no 'npm run publish'
aggregator by design (publishing is CI-only).
lefthook and package.json had parallel command definitions for the
same tools (oxlint, oxfmt, cspell). Consolidate by making lefthook
call the npm scripts, with staged files passed via the
LEFTHOOK_FILES env var. The scripts use ${LEFTHOOK_FILES:-<default>}
so they default to the full project when invoked manually and to
the staged-files list when invoked from lefthook.
Changes:
- package.json#check:oxlint: `oxlint ${LEFTHOOK_FILES:-src}`
(lints src/ manually; staged files from lefthook)
- package.json#check:oxfmt: `oxfmt --check ${LEFTHOOK_FILES:-src}`
- package.json#check:cspell: `cspell lint ${LEFTHOOK_FILES:-.}`
(walks CWD manually; staged files from lefthook)
- package.json#check:tsc, check📦 unchanged (no file args)
- lefthook.yml: file-filtered hooks (oxlint, oxfmt, cspell) now use
`sh -c 'LEFTHOOK_FILES="$0" npm run check:*' {staged_files}` to
inject the staged-files list into the env. sort-package-json and
typecheck call npm scripts directly (no file args).
- project-specs.md: document the unification pattern
Why sh -c + env var instead of the simpler 'npm run ... -- {staged_files}':
'oxlint src file.ts' lints the whole src/ tree *plus* file.ts
(oxlint doesn't dedupe paths). Setting LEFTHOOK_FILES as an env
var (which lefthook's 'env:' config does not template) requires
the sh -c wrapper, but it gives the right semantics: when the
var is set, only the explicit files are checked; when unset,
the default (src/ or .) is used.
Verified:
- 'npm run check:oxlint' (no env) lints all of src/
- 'LEFTHOOK_FILES=src/match.ts npm run check:oxlint' lints only that file
- 'npx lefthook run pre-commit' with a staged TS file: cspell output
shows '1/1 src/match.ts' (only staged file, not whole project)
- All 5 hooks pass on a real staged change
- 'lefthook validate' reports 'All good'
- 'npm run check' exits 0
The previous config was a hybrid of v1 (jobs: array) and v1
(top-level commands: block) that no longer validates under
lefthook 2.x. lefthook 2.0.0's schema has top-level 'commands:'
set to 'false'; the v1 jobs: array form still works, but the
orphan named-commands block at the top was silently invalid
(caught by 'lefthook validate').
Migrate to the v2-native 'commands:' (named) form under the hook:
pre-commit:
parallel: true
commands:
oxlint:
glob: ...
run: npx oxlint {staged_files}
...
Changes:
- Add 'min_version: 2.0.0' to declare the v2 schema explicitly
- Replace pre-commit.jobs: array with pre-commit.commands: (named)
- Inline the glob on each command (was a top-level property on each
job in v1)
- Remove the orphan top-level 'commands:' block — it was never
referenced by any hook, and v2 forbids it
- Update project-specs.md: drop the dangling reference to the
lefthook 'commands:' block (it was the dead top-level one);
describe the v2 commands: form
Verified:
- 'lefthook validate' reports 'All good'
- 'lefthook run pre-commit' executes all 5 hooks (oxlint, oxfmt,
sort-package-json, typecheck, cspell); globs correctly skip hooks
on non-matching files (e.g. lefthook.yml alone) and run them on
TS/JS changes
- 'npm run check' exits 0
- 'lefthook dump' shows the normalized v2 config
project-specs.md:
- Replace Prettier/ESLint/Vite/Vitest references with oxfmt/oxlint/oxc
and TypeScript 7 / node --test
- Drop @tsconfig/strictest note; rules are inlined in tsconfig.json,
enumerated explicitly
- Document oxlint rule disables (no-undefined, sort-keys, id-length,
no-named-export) and test-file overrides (no-unused-expressions,
no-empty-file, no-nodejs-modules, no-magic-numbers)
- Document Node 26 / .node-version / engines.node / CI follow
- Document allowImportingTsExtensions + rewriteRelativeImportExtensions
for the node --strip-types quirk
- Replace Vite/vitest coverage example with c8 + node --test
- Sync scripts section: drop check:eslint, check:prettier and their
fix:* counterparts; add check:oxlint/check:oxfmt/fix:oxlint/fix:oxfmt
- Add current source layout (index.ts, match.ts, pattern.ts, index.test.ts)
- Update VSCode integration section with actual settings.json and
extensions.json contents
- Document the actual commit history with gitmoji prefixes
- Fix 'initilizing' typo
- Drop 'changesets is a devDep' claims (changesets is not installed)
package.json:
- Move @oxfmt/binding-* and @oxlint/binding-* from devDependencies to
optionalDependencies so the right binding is selected per platform
(CI on Ubuntu gnu gets the gnu binding automatically, not the
local musl one)
- Extend check:outdated to ignore the platform-specific bindings (they
show as 'not installed' on the current platform, which is correct
- Add cspell words: gitmoji, dbaeumer, msvc (the latter for the Windows
binding variant). Drop the unused 'nocheck' word
README.md:
- Drop the 'changesets is available as a devDep' line; changesets
isn't installed
- Drop .eslintrc.cjs and .prettierrc, add .oxlintrc.json and .oxfmtrc.json
- oxlint covers correctness/suspicious/perf/style/restriction categories
plus import and typescript plugins; oxfmt reads the same .prettierrc-style
options we had before
- Update lefthook.yml pre-commit jobs to run oxlint/oxfmt instead of
eslint/prettier
- Update .vscode/extensions.json (oxc replaces eslint/prettier/vitest) and
settings.json (oxc becomes the default formatter for TS/JS)
- Add 'oxlint', 'oxfmt', 'oxc', 'nodenext' to cspell dictionary
- Rewrite README 'Development' section to describe the new toolchain