Category files under development/ replace the decision prose that was scattered through README.md and CONTRIBUTING.md. Each decision is a block with Decision (YYYY-MM) / Why / Rejected / Known issue; the new library.md records the public API contract and its limitations. AGENTS.md and backlog.tasks now point at the new home.
8.2 KiB
Tooling
The choice and configuration of every tool below is the result of a deliberate trade-off, not a default. This file is the record of those trade-offs. The commands a contributor runs are in CONTRIBUTING.md; the tool versions are in package.json.
Tool inventory
- TypeScript 7 — type checker and build (
tsc). - node --test +
--strip-types— test runner. - c8 — code coverage for
test:ci. - oxlint — Rust-based linter, with type-aware rules powered by oxlint-tsgolint (typescript-go).
- oxfmt — Rust-based formatter (Prettier-compatible). Formats JS/TS,
JSON/JSONC, YAML, Markdown, MDX, and more; built-in
package.jsonkey sorting replacessort-package-json. - cspell — spell checking.
- knip — finds unused dependencies, exports, and files.
- check-outdated — reports dependencies behind the registry; it exits non-zero whenever any dependency is outdated.
- publint — validates
package.jsonfor ESM publishing correctness. - @arethetypeswrong/cli (
attw) — validates.d.tsdeclarations against multiple module-resolution scenarios. - lefthook — git hooks.
- @spences10/pi-lsp — read-only LSP code intelligence for AI coding agents
(project-local
.pi/settings.json). Talks to this repo's TypeScript 7 viatsc --lsp --stdio.
When each tool runs is in CONTRIBUTING.md § Feedback tiers.
TypeScript and build
One type-check config, one emit config
Decision (2026-09)
tsconfig.json extends @tsconfig/strictest + @tsconfig/node26.
tsconfig.build.json extends it to add the emit-only options (declaration,
sourceMap, inlineSources, outDir, target: es2024,
rewriteRelativeImportExtensions: true) and to exclude test files.
Why
- It lets the editor and CI type-check from one config while the build emits
from the other, so a test file cannot leak into
dist/. inlineSourcesembeds the original TypeScript indist/*.js.map, so debuggers can map intosrc/without it being shipped.declarationMapis intentionally off: a.d.ts.mapcannot embed source and would dangle.
The build starts from an empty dist/
Decision (2026-09)
npm run build first runs a prebuild hook that empties dist/.
Why
tscdoes not prune orphaned emit output. DroppingdeclarationMap, for example, left stale*.d.ts.mapfiles behind, so the build must start from an emptydist/to be reproducible.prebuildremoves onlydist; the manualcleanstill resetsdist+coverage, so a local coverage report survives a build.
Source imports use .ts extensions
Decision (2026-09)
Source imports use .ts extensions, never .js.
Why
node --strip-typesonly resolves the.tsform at test time.rewriteRelativeImportExtensions: trueintsconfig.build.jsonrewrites them to.jsin the emitted JavaScript.- The emitted
.d.tskeep the.tsspecifier, which TypeScript >= 5.0 resolves (see README § Requirements), so no post-processing step is needed.
Rejected
- "Pre-fixing" an import to
.js: it breaks the innernode --strip-typesloop.
Linting and formatting
Type-aware oxlint is a config property
Decision (2026-09)
Type-aware oxlint is enabled declaratively via options.typeAware: true in
.oxlintrc.json (powered by oxlint-tsgolint).
Why
- The script commands stay clean — no CLI flag.
- Type-aware mode is a property of the config, not the invocation, so it cannot be forgotten on one call site.
Rejected
- Passing a CLI flag in the
check:oxlint/fix:oxlintscripts: it puts the mode in two places and invites them to drift.
oxlint-disable directives live next to the code
Decision (2026-09)
Source-level oxlint-disable directives are used for known type-aware false
positives (see src/pattern.ts, src/match.ts, src/index.test.ts), rather
than rules being disabled in .oxlintrc.json.
Why
- The disable lives next to the code it silences, so the trade-off is visible to anyone reading the source.
Rejected
- Silencing a rule project-wide in
.oxlintrc.json: it hides the suppression from the reader of the affected code.
Known issue
- A source-level disable is a human last-resort convention. AI coding agents must not add one; they fix the type at its root instead (see AGENTS.md § Never do).
check:tsc runs first
Decision (2026-09)
check:tsc runs first in the npm run check chain.
Why
- A type error short-circuits the rest, which is faster feedback than letting
oxlint/oxfmt run and then failing on
tscat the end.
.editorconfig is a fallback, not a gate
Decision (2026-09)
.editorconfig exists for editor compatibility. Where both apply,
.oxfmtrc.json is authoritative.
Why
.editorconfigis a sane fallback for the files oxfmt does not format (shell scripts, dotfiles,LICENSE, the commit-message template, and git'sCOMMIT_EDITMSGbuffer).- oxfmt is the formatter; the overlapping
.editorconfigkeys only keep non-oxfmt editors close to the formatted result, so they cannot disagree with the checker.
Static analysis and packaging
knip omits the types category
Decision (2026-09)
knip --include dependencies,exports,files intentionally omits the types
category.
Why
- The
typescategory produces systematic false positives for libraries whose exported types are part of the public API. - The targeted scope keeps the signal high without config-file boilerplate.
attw targets ESM-only
Decision (2026-09)
attw --profile esm-only is used.
Why
- It is semantically correct: this package is intentionally ESM-only (no CommonJS shim), so CJS resolution scenarios are out of scope by design, not a bug.
tslib and type-fest are deliberately not used
Decision (2026-09)
Neither tslib nor type-fest is a dependency.
Why
tslibis a runtime helper for old ES3/ES5 targets; the project targets ES2024.type-festwas never imported.knipflagged both, which is the same signal that keeps the list honest.
Git hooks and script wiring
LEFTHOOK_FILES scopes commands to staged files
Decision (2026-09)
The pre-commit hook sets the LEFTHOOK_FILES env var to the staged-files list,
and the affected scripts use ${LEFTHOOK_FILES:-<default>} to default to the
whole project when invoked manually.
Why
- It keeps
package.json#scriptsas the single source of truth for the underlying commands —lefthook.ymlonly describes what to run on which files. - The same script works by hand (whole project) and staged (scoped), so there is no second command to maintain.
Editor and agent tooling
VSCode integration
- Recommended extensions: see .vscode/extensions.json (oxc, cspell, TypeScript native-preview, EditorConfig, todo-tasks).
- TypeScript 7 is used via the
typescriptteam.native-previewextension. - The oxc extension provides oxlint squiggles and oxfmt format-on-save;
.vscode/settings.jsonpins it per language so a user's local[language]formatter settings cannot override the project's choice.
@spences10/pi-lsp is pinned and read-only
Decision (2026-09)
@spences10/pi-lsp is pinned to 0.0.46 and used read-only.
Why
- The package inspects
node_modules/typescript, sees major >= 7 with nolib/tsserver.js(true of thetypescript-go/tsgoport), and spawns the repo's owntsc --lsp --stdiobinary — notypescript-language-serverdependency is required. - Earlier releases (
<= 0.0.10) hard-wire totypescript-language-server --stdioand are TS6-only. - The tool is intermediate agent feedback (hover, references, definition,
symbols, diagnostics). It has no rename / code-action / apply-edit surface,
and is never a correctness gate —
npm run check/verifyremain that. .pi/settings.jsonis the shared, committed declaration;.pi/npm/is a gitignored install cache that pi recreates automatically on a trusted startup (it runsnpm installfor any missing project package), so the cache is deliberately not tracked.