# Tooling Every tool below was chosen and configured deliberately. The commands a contributor runs are in [CONTRIBUTING.md](../CONTRIBUTING.md) and the versions in [package.json](../package.json). ## Tool inventory - **TypeScript 7** — type checker and build (`tsc`). - **node --test** + `--strip-types` — test runner. - **c8** — coverage for `test:ci`. - **oxlint** — Rust linter, type-aware via **oxlint-tsgolint** (typescript-go). - **oxfmt** — Rust formatter (Prettier-compatible) for JS/TS, JSON/JSONC, YAML, Markdown, MDX, and more; its `package.json` key sorting replaces `sort-package-json`. - **cspell** — spell checking. - **knip** — unused dependencies, exports, and files. - **check-outdated** — dependencies behind the registry; exits non-zero when any is outdated. - **publint** — validates `package.json` for ESM publishing. - **@arethetypeswrong/cli** (`attw`) — validates `.d.ts` against module-resolution scenarios. - **lefthook** — git hooks. - **@spences10/pi-lsp** — read-only LSP code intelligence for AI agents (project-local `.pi/settings.json`); talks to this repo's TypeScript 7 via `tsc --lsp --stdio`. When each runs is in [CONTRIBUTING.md § Feedback tiers](../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` adds the emit-only options (`declaration`, `sourceMap`, `inlineSources`, `outDir`, `target: es2024`, `rewriteRelativeImportExtensions: true`) and excludes test files. #### Why - The editor and CI type-check from one config while the build emits from the other, so a test file cannot leak into `dist/`. - `inlineSources` embeds the original TypeScript in `dist/*.js.map`, so debuggers map into `src/` without it being shipped. - `declarationMap` stays off: a `.d.ts.map` cannot embed source and would dangle. ### The build starts from an empty `dist/` #### Decision (2026-09) `npm run build` runs a `prebuild` hook that empties `dist/`. #### Why - `tsc` does not prune orphaned emit output — dropping `declarationMap` left stale `*.d.ts.map` files — so reproducibility needs an empty `dist/`. - `prebuild` removes only `dist`; the manual `clean` resets `dist` + `coverage`, so a local coverage report survives a build. ### Source imports use `.ts` extensions #### Decision (2026-09) Source imports use `.ts`, never `.js`. #### Why - `node --strip-types` resolves the `.ts` form at test time. - `rewriteRelativeImportExtensions` rewrites them to `.js` in the emitted JavaScript. - The emitted `.d.ts` keep the `.ts` specifier, which TypeScript >= 5.0 resolves (see [README § Requirements](../README.md#requirements)), so no post-processing step is needed. #### Rejected - "Pre-fixing" an import to `.js`: it breaks the inner `node --strip-types` loop. ## Linting and formatting ### Type-aware oxlint is a config property #### Decision (2026-09) Type-aware oxlint is enabled via `options.typeAware: true` in `.oxlintrc.json` (powered by `oxlint-tsgolint`). #### Why - The script commands stay clean — no CLI flag. - A config property cannot be forgotten on one call site. #### Rejected - A CLI flag in the `check:oxlint` / `fix:oxlint` scripts: it puts the mode in two places and invites them to drift. ### `oxlint-disable` directives live next to the code #### Decision (2026-09) A type-aware rule that false-positives is silenced with a source-level `oxlint-disable` directive (see `src/primitive.ts`, `src/primitive.test.ts`), not by turning the rule off in `.oxlintrc.json`. #### Why - The disable sits next to the code it silences, visible to anyone reading the source. - The rule stays on everywhere else, so only the mis-firing line is exempted. #### Rejected - A project-wide disable in `.oxlintrc.json` for a false positive: it hides the exemption from the reader of the affected code and switches the rule off repo-wide for a one-site problem. #### Known issue - A source-level disable is a _human_ last resort. AI agents must not add one; they fix the type at its root (see [AGENTS.md § Never do](../AGENTS.md#never-do)). ### Unwanted stylistic rules are turned off in the config #### Decision (2026-09) A stylistic rule the project rejects is `"off"` in the `.oxlintrc.json` `rules` map, not silenced at a use site. Current entries: `eslint/capitalized-comments` (comments may start lowercase) and `eslint/no-ternary` (ternaries are allowed), joining the oxfmt-superseded rules already off. #### Why - The rule is wrong for the whole project, not mis-firing at one site, so there is no line to annotate. - Keeping the two mechanisms separate keeps a source-level `oxlint-disable` meaningful: it marks a lone exception. #### Rejected - A source-level `oxlint-disable` per use: the same exemption repeated at every site, and oxfmt can move the site. ### `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 than running oxlint/oxfmt and failing on `tsc` at 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 - `.editorconfig` covers the files oxfmt does not format: shell scripts, dotfiles, `LICENSE`, the commit-message template, and git's `COMMIT_EDITMSG` buffer. - oxfmt is the formatter; the overlapping keys 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` omits the `types` category. #### Why - `types` produces systematic false positives for libraries whose exported types are part of the public API. - The narrower scope keeps the signal high without config-file boilerplate. ### `maintain:outdated` ignores `@types/node` #### Decision (2026-09) Pass `--ignore-packages @types/node`. #### Why - DT pins `@types/node`'s `latest` dist-tag to LTS (22.x); current-line types ride other tags. Scan sees latest < installed — permanent "reverted", exit 1, zero signal. `--ignore-pre-releases` no help: 22.20.3 is stable. #### Rejected - `--types major,minor,patch`: hides real reverted reports elsewhere. - `@types/node@26.*`: tag stays wrong across majors; un-pin per bump = ritual. #### Known issue - A genuinely behind `@types/node` goes unreported; match it to `.node-version` by hand. ### `attw` targets ESM-only #### Decision (2026-09) `attw --profile esm-only` is used. #### Why - The package is intentionally ESM-only (no CommonJS shim), so CJS resolution scenarios are out of scope by design, not a bug. ### `tslib` is deliberately not used #### Decision (2026-09) `tslib` is not a dependency. #### Why - `tslib` is a runtime helper for old ES3/ES5 targets; this project targets ES2024. ## Git hooks and script wiring ### `LEFTHOOK_FILES` scopes commands to staged files #### Decision (2026-09) The pre-commit hook sets `LEFTHOOK_FILES` to the staged-files list, and the affected scripts use `${LEFTHOOK_FILES:-}` to default to the whole project. #### Why - It keeps `package.json#scripts` the single source of truth; `lefthook.yml` only says 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 are in [.vscode/extensions.json](../.vscode/extensions.json) (oxc, cspell, TypeScript native-preview, EditorConfig, todo-tasks). - TypeScript 7 runs via the `typescriptteam.native-preview` extension. - The oxc extension provides oxlint squiggles and oxfmt format-on-save; `.vscode/settings.json` pins it per language so a local `[language]` formatter setting 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 - It inspects `node_modules/typescript`, sees major >= 7 with no `lib/tsserver.js` (true of the `typescript-go` / `tsgo` port), and spawns the repo's own `tsc --lsp --stdio` — no `typescript-language-server` dependency is needed. - Earlier releases (`<= 0.0.10`) hard-wire to `typescript-language-server --stdio` and are TS6-only. - It is _intermediate_ agent feedback (hover, references, definition, symbols, diagnostics), with no rename / code-action / apply-edit surface, and is never a gate — `npm run check` / `verify` are. - `.pi/settings.json` is the committed declaration; `.pi/npm/` is a gitignored install cache that pi recreates on a trusted startup (running `npm install` for any missing project package), so it is deliberately not tracked.