📝 Add development/ docs for decisions and known issues
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.
This commit is contained in:
1 parent
97dfe9e7b4
commit
dc7ce22fc5
9 files changed
+1049
-5
No files matched your search
@@ -0,0 +1,247 @@
|
||||
# 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](../CONTRIBUTING.md); the
|
||||
tool versions are in [package.json](../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.json` key sorting
|
||||
replaces `sort-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.json` for ESM publishing correctness.
|
||||
- **@arethetypeswrong/cli** (`attw`) — validates `.d.ts` declarations 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 via
|
||||
`tsc --lsp --stdio`.
|
||||
|
||||
When each tool 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` 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/`.
|
||||
- `inlineSources` embeds the original TypeScript in `dist/*.js.map`, so
|
||||
debuggers can map into `src/` without it being shipped.
|
||||
- `declarationMap` is intentionally off: a `.d.ts.map` cannot 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
|
||||
|
||||
- `tsc` does not prune orphaned emit output. Dropping `declarationMap`, for
|
||||
example, left stale `*.d.ts.map` files behind, so the build must start from an
|
||||
empty `dist/` to be reproducible.
|
||||
- `prebuild` removes only `dist`; the manual `clean` still resets `dist` +
|
||||
`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-types` only resolves the `.ts` form at test time.
|
||||
- `rewriteRelativeImportExtensions: true` in `tsconfig.build.json` 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 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: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)
|
||||
|
||||
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](../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 `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` is a sane fallback for 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 `.editorconfig` 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` intentionally omits the `types`
|
||||
category.
|
||||
|
||||
#### Why
|
||||
|
||||
- The `types` category 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
|
||||
|
||||
- `tslib` is a runtime helper for old ES3/ES5 targets; the project targets
|
||||
ES2024.
|
||||
- `type-fest` was never imported.
|
||||
- `knip` flagged 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#scripts` as the single source of truth for the
|
||||
underlying commands — `lefthook.yml` only 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](../.vscode/extensions.json) (oxc, cspell, TypeScript
|
||||
native-preview, EditorConfig, todo-tasks).
|
||||
- TypeScript 7 is used 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 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 no
|
||||
`lib/tsserver.js` (true of the `typescript-go` / `tsgo` port), and spawns the
|
||||
repo's own `tsc --lsp --stdio` binary — no `typescript-language-server`
|
||||
dependency is required.
|
||||
- Earlier releases (`<= 0.0.10`) hard-wire to `typescript-language-server
|
||||
--stdio` and 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` / `verify` remain that.
|
||||
- `.pi/settings.json` is the shared, committed declaration; `.pi/npm/` is a
|
||||
gitignored install cache that pi recreates automatically on a trusted startup
|
||||
(it runs `npm install` for any missing project package), so the cache is
|
||||
deliberately not tracked.
|
||||
Reference in new issue
Block a user