Commit Graph
88 Commits
Author SHA1 Message Date
tmu f4c50867fe 🔖 Write changelog for release 2026-09-13 21:08:37 +00:00
tmu 78bec7a5eb ✨ Emit precompressed sidecars for served assets
Add scripts/precompress.ts, a dependency-free Node 26 tool that writes
.br/.gz/.zst sidecars next to every text asset and keeps the originals, so
static-web-server can serve the variant matching Accept-Encoding and fall
back to the original. The coverage publish step runs it on the copied report.

Wire scripts/*.ts into the toolchain: type-check (tsconfig include), lint and
fix (oxlint paths), knip entry (CI invokes the file rather than importing it),
and a narrow .oxlintrc override allowing node:* imports in Node CLI scripts.
2026-09-13 20:46:59 +00:00
tmu 224afa9afb 👷 Publish tag coverage to the pages server
The build job bind-mounts the shared pages tree (the runner whitelists it
via container.valid_volumes) and, on tag pushes, wipes
/data/gitea-pages/<owner>/<repo>/<tag>/coverage before copying the c8 report
into it. Only this tag's coverage/ is touched; older tags and sibling
docs/landing trees are left for manual pruning.

Add a `tags: ["*"]` push trigger: a `branches` filter alone matches no tag
ref, so the tag-gated publish job (and this coverage step) could never run.

Track per-branch coverage as a backlog task.
2026-09-13 20:38:59 +00:00
tmu a865b466bd 👷 Add Gitea release page to CI
The publish job now opens a Gitea release for the tag, using the
matching Keep-a-Changelog section as the body via scripts/release-notes.sh.
It runs before npm publish so a broken page fails CI without burning an
npm version; npm publish stays the last step.

Uses the first-party gitea-release-action (the older actions/release-action
is deprecated and requires asset files) and contents: write for the run's
automatic token.
2026-09-11 22:06:05 +00:00
tmu 7216c418d0 📝 Add a few new ideas to backlog 2026-09-11 20:54:16 +00:00
tmu 1191f3c4d9 📝 Prune the backlog of closed tasks
All completed and cancelled setup-phase entries are resolved and reflected
in README/CONTRIBUTING/ci.yml; the only still-open items remain.
2026-09-11 20:30:16 +00:00
tmu eee73a151d 📝 Point feedback tiers at ci.yml as the source of truth
The CI build row and the publishing workflow restated a step list that
had already drifted from the workflow (build + publint in build, publish
consuming the build artifact). Reference .gitea/workflows/ci.yml instead
of duplicating the job graph.
2026-09-11 20:23:36 +00:00
tmu 4d92ce9f9a 📝 Check off dist-output v1.0 tasks
The emit work (rewriteRelativeImportExtensions, inlineSources, prebuild
clean) resolved the v1.0 dist/ output block, and attw is covered by the
CI publish tier rather than a standalone checklist item.
2026-09-11 20:23:32 +00:00
tmu b29f924416 🔧 Clean dist before each build
tsc does not prune orphaned emit output, so dropping declarationMap left
stale *.d.ts.map files in dist/ until a manual clean. A prebuild hook now
empties dist/ first, keeping the build reproducible. It deliberately leaves
coverage/ alone, since the manual `clean` resets both.
2026-09-11 20:04:33 +00:00
tmu a0dd187042 📝 Document the TypeScript 5.0 type floor
Consumers need TypeScript >= 5.0: the emitted declarations use const type
parameters and keep relative .ts specifiers, both of which resolve only on
TS >= 5.0. That also makes the emitted .ts specifiers a non-issue, so the
backlog item is closed without a .d.ts post-step: TS <= 4.9 cannot parse
the declarations anyway. The README/CONTRIBUTING wording now says the
rewrite applies to the JavaScript output, not to the declarations.
2026-09-11 19:58:36 +00:00
tmu e7a058d608 🔧 Inline sources and drop declaration maps
The JS source maps now embed their sources (inlineSources), so a debugger
resolves into src/ even though the package ships only dist/. declarationMap
is dropped: a .d.ts.map cannot embed source and would dangle against the
unshipped src/.

Also removes a duplicate backlog entry for the same task and closes the
sourcemap item.
2026-09-11 19:50:50 +00:00
tmu e96c3f89ac 📝 Hand over the two open emit items
Record what a following agent cannot cheaply reconstruct for the
remaining setup-phase items (.d.ts .ts extensions, sourcemap sources):
the verified current emit state, the fact that every existing gate is
already green so no red signal will confirm the fix, that the durable
proof is a consumer-resolution typecheck rather than exit 0, and that
the two items share the emit surface so they belong in one change.
2026-09-10 23:53:46 +00:00
tmu 78aa97d670 📝 Correct the pi-lsp false-positive finding
The TS1295/TS1287 errors were not an LSP misconfiguration: the server
was still running from an earlier local test that had emptied
package.json, and kept the corrupted project state alive. Driven with
pi-lsp's exact handshake, tsc --lsp --stdio reads tsconfig.json
correctly. Record the real caveat instead: no file watchers, so
tsconfig.json/package.json edits can leave diagnostics stale until the
server restarts.
2026-09-10 23:49:09 +00:00
tmu 13ea134d05 📝 Document the .pi extension install model
.pi/settings.json is the shared, committed declaration and .pi/npm/ is
a gitignored cache pi recreates on a trusted startup, so the generated
.gitignore is intentional and nothing needs tracking. Record this in
the README tooling notes and close the reproducibility item as
resolved by design.
2026-09-10 23:37:23 +00:00
tmu b18e03ff97 📝 Keep verify free of the emit path
verify is the local quick source-correctness check; the emit path is
already gated by CI's build job, so adding build to verify would only
add inner-loop friction for something CI covers.
2026-09-10 23:31:45 +00:00
tmu 0ceb5a586e 📝 Confirm pre-push stays a local quick check
Pre-push runs npm test only. That is intended: the staged-file checks
are pre-commit's job and whole-project correctness is CI's, which
checks the pushed commits' content. Dirty working-tree files are out
of scope for a local check that CI backs up, so no dirty-tree guard is
added.
2026-09-10 23:25:04 +00:00
tmu fe9d59f8d4 👷 Publish the dist built by the build job
The publish job re-ran npm ci + build from scratch, discarding the
output that check, test:ci and publint had already gated, and paying
the build twice on a tag push. Upload dist/ as an artifact from build
and download it in publish instead. npm ci stays for the publint/attw
binaries; only the redundant rebuild is gone.
2026-09-10 23:05:46 +00:00
tmu 303544796e 👷 Run publint in the CI build job
publish:publint and publish:attw only ran in the tag-triggered publish
job, so a packaging break stayed green until release. Add publint to
build (offline, fast, packs the built dist). attw stays in publish,
where the full resolution matrix is worth the cost.
2026-09-10 23:03:14 +00:00
tmu 968a478c93 ♻️ Use rm for the clean target
The node -e rmSync one-liner was carried over from the build-tool
switch and only existed for cross-platform shell safety. The repo is
POSIX-only (sh scripts, Node 26, Linux CI), so rm -rf is simpler and
consistent with the rest of the tooling.
2026-09-10 22:56:12 +00:00
tmu e86709f593 📝 Sweep doc and formatter drift
README named --experimental-strip-types, which was renamed to
--strip-types. Document the current flag only.

The extension recommendations list is unchanged, but the VSCode section
undercounted it and now matches extensions.json. The per-language
formatter blocks in .vscode/settings.json stay (they keep a user's
local formatter settings from overriding the project's) and gain the
JSX/TSX language ids to match what oxfmt and the lefthook glob cover.
2026-09-10 22:49:01 +00:00
tmu 1709fbe842 🔧 Ignore pubv in knip via knip.json
pubv is a CLI invoked by release.sh, never imported, so knip reported
it as an unused devDependency on every advisory run. Add a knip config
with an ignoreDependencies entry; verified removal of the entry
reproduces the report.
2026-09-10 22:43:04 +00:00
tmu 0b461c233f 🔥 Drop redundant main/types fields
exports already declares types + import for the root entry, so the
top-level main/types pair was duplicate metadata. publint and attw stay
green with exports alone, verified against a packed consumer install.
2026-09-10 22:42:32 +00:00
tmu 8d1a03f025 🔧 Remove coverage in clean target
clean only wiped dist, leaving the gitignored coverage/ tree to
accumulate across runs. Remove both so a clean checkout state is
actually reachable.
2026-09-10 22:42:14 +00:00
tmu b7d1dbdf06 📝 Track setup-phase review findings in the backlog
Record the tooling, CI and docs gaps found reviewing the setup phase
(src/ is placeholder code and was excluded from the review).

The entries cover pi-lsp install reproducibility, gating the emit path
in verify, .ts extensions left in emitted .d.ts, pi-lsp false-positive
diagnostics, CI packaging checks and artifact reuse, pre-push gate
strength, plus low-impact cleanups (clean target, redundant fields,
knip false positive, sourcemap sources, doc drift).
2026-09-10 22:37:59 +00:00
tmu 99bda289a6 📝 Document .editorconfig as oxfmt fallback
Recommend `EditorConfig.EditorConfig` in .vscode/extensions.json so editors
without native `.editorconfig` support pick it up, and add an "Editor
configuration" section to CONTRIBUTING.md: `.editorconfig` is an
editor-compatibility fallback for the file types oxfmt does not format
(shell scripts, dotfiles, the commit-message template, git's
`COMMIT_EDITMSG` buffer), with `.oxfmtrc.json` authoritative where both
apply. `.editorconfig` itself is unchanged.
2026-09-10 21:47:08 +00:00
tmu 0c6d1483de ♻️ Port CI workflow from GitHub Actions to Gitea Actions
The runner now lives on Gitea; move the workflow to .gitea/workflows/ and
delete the stale .github copy (Gitea ignores .github/, so it's drift bait).
Use the native gitea.ref context for the tag gate; keep actions pinned to
their GitHub sources (the runner has internet + caches them). Drop the
upload-artifact coverage step in favour of the self-hosted webserver plan
tracked in the backlog. Add an empty-secret gate as publish's first step so
a tag push without NPM_TOKEN fails loudly instead of silently no-oppping.
2026-09-10 19:41:09 +02:00
tmu 9a08262076 📝 Track self-hosted coverage hosting plan in backlog
Record the decision to host CI coverage from a tiny dir-listing server in
the Gitea docker setup (shared volume keyed by repo/tag) instead of the
GitHub upload-artifact zip. This unblocks dropping the coverage artifact
from the Gitea CI port. Adds the lipanski docker-image author to cspell.
2026-09-10 19:40:35 +02:00
tmu 672fd1f7e4 ♻️ Adopt setup: prefix and retire the stray use: script
Introduce a setup: prefix for one-time clone configuration (mutates the
local environment, never a hook or CI step) with setup as its umbrella
aggregator. Move use:git-commit-message to setup:git-commit-message as
its first member, retiring use: — the undocumented prefix tracked in the
backlog. Update both CONTRIBUTING.md prefix lists, the bare-command
inventory, and the stale use: reference in branch.sh.
2026-09-08 23:01:49 +02:00
tmu 844e80d650 ♻️ Group workflow scripts under a create: prefix
`branch` and `release` were bare commands, but bare in this repo means
"how you invoke a tier" or "runs one tool" — neither fits a command that
opens or closes a unit of work. They get their own prefix now, since a
prefix is how this repo records _when_ a script runs.

`create:` because both members genuinely create something (a branch, a
release) and it is a plain verb rather than VCS slang. No bare `create`
aggregator: running "all the workflows" describes nothing anyone wants,
and `publish:*` already precedents a prefix without one.

The rule "reuse an existing prefix, never invent one" now says what it
actually means: a new prefix is allowed when the scripts belong in the
pipeline, provided it enters both lists in the same commit as its first
member. That is the lesson from `use:`, which is referenced by prose yet
in none of the lists — already tracked in backlog.tasks.

The bare-command sentence shrinks to `build`, `clean`, `verify`.
2026-09-08 22:51:35 +02:00
tmu 8c2855aa92 📝 Track the undocumented use: prefix in the backlog
`use:git-commit-message` is referenced by CONTRIBUTING.md but appears in
none of the lists that define the script vocabulary, so the rule "reuse
an existing prefix, never invent one" is currently violated by its own
exception.

Recorded as a task rather than fixed here: the prefix wants a name that
says what it is for, and that is the same decision as naming a prefix
for the other lifecycle scripts, so the two should be chosen together.
2026-09-08 22:32:05 +02:00
tmu 8efd13b2f4 ✨ Add branch front-door command
The branching model assumed a clean, current `main` and a green baseline
before any edit, but both were prose, and prose nobody checks silently
becomes a suggestion. `npm run branch -- <prefix>/<desc>` asserts the
precondition and only then creates the branch, so a later failure is
attributable to the change that caused it.

Checks run cheap-first (`--porcelain` deliberately catches untracked
files, which would otherwise ride onto the new branch) and the suite
runs last, so an ineligible tree never pays for it. `main`'s remote is
derived from its upstream rather than hardcoded: this repo has both
`origin` and `origin_https`, and `main` tracks the latter, so a
`git fetch origin main` currency check would compare against a ref that
is never updated here. The baseline runs after switching to `main`, so a
red `main` restores the starting branch instead of stranding the caller
on it.

The prefix stays a judgment call: the script validates it against the
documented vocabulary instead of inferring it.

Prose kept as index only — AGENTS.md points agents at the command from
the task workflow, CONTRIBUTING.md owns the model and the bare-script
tier (dropping its stale hardcoded count of "two" conveniences).
2026-09-08 22:14:34 +02:00
tmu 93cb37441d 📝 Document pi-lsp tooling and agent usage
README § Tooling and § Tooling decisions record the extension and the
reasoning behind it: read-only by design, auto-detects TypeScript 7,
and never a correctness gate — verify is. AGENTS.md § First action tells
agents the lsp_* tools exist, to prefer lsp_references over grep -w for
colliding identifiers, and to treat empty LSP output as inconclusive.

Close the backlog evaluation task with a resolved note, and allowlist
the tsgo/tsserver tool names for cspell.
2026-09-08 20:47:40 +02:00
tmu 863198d472 ✨ Adopt @spences10/pi-lsp project-local
Declare the read-only LSP extension in .pi/settings.json so it is
shared with the team and auto-installed on project trust.

Pin 0.0.46 explicitly: a bare `pi install` writes `^0.0.10`, and in
semver a caret on 0.0.x pins to exactly 0.0.10, which hard-wires
typescript-language-server and predates TypeScript 7. 0.0.46 detects
the repo's own typescript@7 (no lib/tsserver.js) and spawns
tsc --lsp --stdio against it — no extra server package required.

Commit pi's own .pi/npm/.gitignore sentinel rather than adding a rule
to the project root .gitignore: pi put the file there deliberately, so
leaving it in place (and tracked) is the least-surprise option for a
human reading the tree.
2026-09-08 20:47:22 +02:00
tmu 15aef06e32 🐛 Require @done on every ✔ backlog line
The sandy081.todotasks extension marks a line done from the ✔ glyph
alone, then unconditionally decorates its @done tag via
lineText.indexOf("@done"). On a bare ✔ that returns -1, so the
decorator builds a Range with a negative character offset, VS Code
throws, and all decoration/highlighting for the document dies.

The committed backlog.tasks contained two such lines (the branching-
model subtasks), so re-opening the file reproducibly broke
highlighting. Add @done to them and document the required ✔/@done
(and ✘/@cancelled) coupling in AGENTS.md.
2026-09-08 14:56:47 +02:00
tmu b50149aad6 📝 Document 'release' as a bare script in prefix convention
'release' (./scripts/release.sh) runs no tool and aggregates no
prefix:* family, so it fits neither a tool entry point nor a tier
aggregator. List it among the bare conveniences alongside 'verify'.
2026-09-08 14:07:36 +02:00
tmu 06bc6bc43e 📝 Document GitHub Flow branching model and agent task workflow
Adopt single-developer GitHub Flow: branches off main with a
feature/fix/chore prefix, merged back via 'git merge --no-ff' (local
PR). Releases are not triggered by pushes; only the maintainer runs
'npm run release', which tags and pushes; CI publishes to npm on the
tag. Gitea is the lab; GitHub is reserved for later promotion.

Replace the 'not yet settled' backlog note in AGENTS.md with concrete
agent instructions: a task with subtasks gets a branch, a leaf task is
worked on the current branch, and a fixed handover template (Implemented
/ Judgement calls / Known problems) frames the pre-merge review. Drop
the stale 'MR' reference in 'Never do' (no MR workflow). Update the
feedback-tier table and publishing workflow to gate CI on push to main,
not PRs. Check off the branching-model backlog group and open a task
for the release CI workflow (blocked on a missing Gitea runner).
2026-09-08 14:07:33 +02:00
tmu 75d605fabe 📝 Add backlog.tasks template and format rules
Introduce backlog.tasks, a project backlog in vscode-todotasks format
(not Markdown), seeded as a reusable template: Setup, v1.0, Bugs,
Enhancements, Documentation and Maintenance projects, with the
implementation tasks to be filled in later.

Document the format in AGENTS.md so agents can read and update the file,
recommend sandy081.todotasks in .vscode/extensions.json, and whitelist
"todotasks" for cspell, which both new mentions otherwise fail.

The branch, review and release policy is deliberately not asserted here:
it is unsettled, so it is tracked as a task-group under Setup and AGENTS.md
tells agents to take branch and merge instructions from the user until that
work is documented.
2026-09-08 11:42:46 +02:00
tmu 837203c29e 📝 Rename test-loop "blue" phase to "type"; cite TDD
The type-first spec step was labelled "Blue", which collides with the
Red-Green-Blue convention where blue means the refactor phase (and our
loop already has an explicit Refactor step). Rename phase 1 to Type and
describe the loop as type -> red -> green -> refactor.

Tie the discipline to its established name: Type-Driven Development
(Edwin Brady), quoting the Idris framing -- treat the type as the plan,
let the compiler/type-checker drive you to a program that satisfies it.
Section heading becomes "Testing discipline (type-driven)"; update the
AGENTS.md pointer + anchor. Add "idris" to the cspell dictionary.

De-duplicate: CONTRIBUTING.md no longer re-lists the banned directives
(@ts-ignore, as casts, ...); it links to AGENTS.md "Never do", the
single home for that rule, so the lists can't drift.
2026-09-07 23:44:35 +02:00
tmu c7f566500e ⬆️ Upgrade dependencies to latest versions 2026-09-07 23:01:33 +02:00
tmu 09c5b129bb ✨ Add pubv-based release.sh and npm run release
Release front-door: pubv runs preflight + the interactive semver
heuristic and graduates CHANGELOG.md [Unreleased] -> dated; npm version
syncs package.json + lockfile; git commit --amend folds them into
pubv's single release commit; the tag is created AFTER the amend so it
marks the exact commit on main that gets published; then push. Tags are
v-less (--tag-prefix=none) to match the changelog compare refs.

@runwisp/pubv added as a devDependency. The short "how we got here /
what was rejected" note lives as a comment atop the script; the docs/
exploration was removed as too much to read.
2026-09-07 18:17:28 +02:00
tmu b9fb054176 🔧 Point repo metadata at Gitea; drop redundant .npmignore
The repository.url shipped github.com/tmueller/... but the real remote
is gitea.e1nsnull.de/tmu/..., so the npm page's Repository link 404'd.
Add homepage + bugs, fix the URL, and rename the CONTRIBUTING "GitHub"
reference to the forge. Delete .npmignore: when a package.json `files`
allowlist is present npm never consults .npmignore, so it was dead
weight that would only drift. Add "gitea" to the cspell dictionary.
2026-09-06 14:42:35 +02:00
tmu 464789b385 📝 Document type-first (red/green/blue) testing discipline
New behavior is written type-first: the expectTypeOf (blue) comes
before the assert (red), since for a pattern-matching library the
types are the feature. Adds a CONTRIBUTING.md section and a
discoverability pointer in AGENTS.md's "Read these" index.

Human-facing rationale lives in CONTRIBUTING.md; AGENTS.md only links
it so agents and reviewers don't diverge.
2026-09-06 01:08:09 +02:00
tmu 49a78f3683 📝 Deduplicate docs into one home per fact
The same facts were restated in up to three files, and the
restatements had already drifted: two claimed timings were 2-5x
off, and the maintain rationale existed in four copies with
different numbers each. Duplicated prose is a liability, not
redundancy.

Each fact now has exactly one owner, with links from the others:

- tool inventory: README § Tooling
- configuration rationale: README § Tooling decisions
- prefix taxonomy: CONTRIBUTING § Script prefix convention
- what runs when and at what cost: CONTRIBUTING § Feedback tiers
- publish procedure: CONTRIBUTING § Publishing workflow

README loses its whole Script prefix convention section and the
Workflows section; CONTRIBUTING loses the Why-these-splits bullets
that repeated the tier table and the rules list. Prose is 23KB to
17KB, with no rule or rationale dropped.
2026-09-06 00:45:15 +02:00
tmu 892aead383 🐛 Fix commit template setup with git config
Copying the template into .git/COMMIT_EDITMSG has no lasting
effect: git pre-fills that file from the commit.template config,
so the script registered nothing and the template never appeared.
Use git's own mechanism, which is what git reads for every
interactive commit.

Name, prefix and call site are unchanged, so README's setup
bullet stays correct; only the sentence describing the old copy
in CONTRIBUTING needed rewording. Note the config holds a path
relative to the working directory, so it applies to commits made
from the repo root.
2026-09-06 00:26:22 +02:00
tmu 682ecf1163 ✨ Enforce pinned Node via engine-strict
The `engines` field is advisory by default: an install run under an
unpinned Node only emits a notice and still succeeds, so it rewrites
package-lock.json with that older npm's resolution rules. That is not
hypothetical — it happened while dropping the redundant platform
bindings, and the resulting lockfile was rejected by `npm ci`.

With engine-strict the mismatch is a hard failure instead, so the pin
in .node-version is actually load-bearing for lockfile integrity.
Verified: node 24 install now aborts with EBADENGINE, node 26 stays
green (`npm run verify`, `npm ci` in sync), and npm auto-excludes
.npmrc from the published tarball.
2026-09-06 00:13:12 +02:00
tmu 8b4722da26 🔥 Drop redundant optionalDependencies block
oxlint, oxfmt and oxlint-tsgolint each declare their own platform
bindings as optionalDependencies gated by os/cpu, so npm already
installs exactly the one matching the host. Listing all 20 at the
root duplicated that: the lockfile diff shows no package added or
removed, only re-tagged as a dev transitive.

The root list was also the sole reason maintain:outdated carried a
20-package --ignore-packages hack, since check-outdated scans root
deps. Without it the script is one flag and reports clean.

Verified on the pinned node 26: fresh `npm ci` in sync, `npm run
verify` green, type-aware oxlint still fires (no-floating-promises),
and `npm install --os/--cpu` dry-runs resolve the darwin-arm64 and
win32-x64 bindings from the lockfile.
2026-09-06 00:09:33 +02:00
tmu da5ab79f9b ♻️ Replace verify skill with an npm verify script
A skill duplicated AGENTS.md policy and only worked in Agent-Skills harnesses.
A bare top-level script is the repo-native affordance: every harness (and CI,
and a human) reads package.json#scripts as the source of truth. Add
`npm run verify` = `npm run check` + `test:unit` (tsc runs once, since check
already type-checks) as the one-shot whole-project correctness gate.

Delete .agents/skills/verify/SKILL.md. Document verify in README (Development +
a Tooling-decisions bullet + bare-command note in the prefix list), CONTRIBUTING
(feedback-tier table, "Before pushing", the bare-command paragraph, a "why these
splits" bullet), and AGENTS.md (test = fast iterating gate, verify = definition
of done; skill pointer removed).
2026-09-05 21:46:59 +02:00
tmu 3d0fe18d6a ✨ Add verify skill for the agent loop
A project-local Agent Skill (.agents/skills/verify/SKILL.md) wraps the
correctness ladder as an on-demand procedure: run npm run test, recover by
fixing the root cause in the types (not by suppressing them), optionally run
whole-project npm run check, and commit through the hook. Exposed as
/skill:verify for pi, and read by any Agent-Skills harness from .agents/skills/.

It is a procedure, not a doc fork: it links AGENTS.md / CONTRIBUTING.md /
package.json instead of restating the rules, so there is no drift.

Agents must not start `npm run watch` -- it never returns and blocks the turn.
The skill's ladder drops watch and calls it out under "What not to run here";
AGENTS.md gains a matching guardrail.
2026-09-05 21:28:44 +02:00
tmu 103cf04f8d ♻️ Run both maintain scans independently (;)
The maintain aggregator used &&, so if maintain:knip exited non-zero (e.g. an
unused export) maintain:outdated never ran and the report was partial. Switch
to ; so both scans always run and every finding surfaces. CI still treats the
job as non-blocking, so the trailing exit code is moot there. Zero new dep.
2026-09-05 18:49:59 +02:00
tmu 7973bf0d0b 📝 Move knip + outdated to a maintain: prefix
`npm run check` should be the fast, offline correctness ladder only (tsc +
oxlint + oxfmt + cspell, ~3s), so an agent can run it as a confirmation gate
during feature work. check:knip / check:outdated were advisory whole-project /
network scans, and check-outdated exits non-zero whenever any dep is behind.
Keeping them in check made `npm run check` (and the CI build gate) fail on
dependency freshness, which must not block an unrelated feature PR.

Rename them to maintain:knip / maintain:outdated, aggregate under `npm run
maintain`, and run it in CI as a dedicated non-blocking job (continue-on-error)
that surfaces findings without ever gating a merge. Update the script-prefix
convention, the feedback-tier table, and AGENTS.md: the agent may now run
`npm run check`; only `maintain` stays out of the feature loop.
2026-09-05 16:47:55 +02:00
tmu 8940537fc0 📝 Add hook-discipline rule to AGENTS.md
The agent must not use `git commit --no-verify` (or skip any hook) to get a
green run — bypassing a check to silence it is the same anti-pattern as
@ts-nocheck or an oxlint-disable. The pre-commit checks are fast and offline,
so a redundant run costs nothing. Records the "redo through the hook" recovery
command.
2026-09-05 16:35:14 +02:00
tmu 436b49e3be 📝 Add AGENTS.md as the agent entry point
AI coding agents read AGENTS.md automatically, not README. Make AGENTS.md
a thin pointer: the stable first-action facts plus links to CONTRIBUTING.md,
README.md, and package.json#scripts. Keep the rules prose in CONTRIBUTING.md
so humans and agents don't diverge (same anti-drift move as dropping
project-specs.md).

Clarify the agent's verification loop: npm run test is the mandatory gate;
the pre-commit hook already runs the fast offline checks (tsc, oxlint, oxfmt,
cspell) on staged files, and npm run check (knip + outdated) is repo-maintenance,
not part of the feature loop. Forbid type-system escape hatches (@ts-nocheck,
oxlint-disable, as-casts) as agent-only rules; a human may still add a
review-visible disable as a last resort, so the location convention stays in
CONTRIBUTING.md with a forward reference.
2026-09-05 16:32:15 +02:00
tmu 8924dd67d3 ✨ Add watch tier with watch:test child
Adds the earliest feedback tier to the ladder: `npm run watch`
re-runs tests on file save, started manually in a dedicated
terminal pane. Built on Node 26's native `node --test --watch`
(no new dependency).

Structure follows the existing prefix convention:
- `watch:test` — runs the test suite in watch mode
- `watch` — umbrella that aggregates `watch:*` children
  (currently just `watch:test`; future `watch:oxlint` etc.
  would aggregate here, switching to concurrent execution)

CONTRIBUTING.md documents the new tier in three places: the
prefix convention list, the feedback tiers table (new top row),
and a 'Why these splits?' bullet explaining why watch is a
manual tier rather than a hook. README's Development section
gains a one-line pointer.
2026-09-05 00:40:41 +02:00
tmu 538272c222 📝 Restore unique maintainer content as CONTRIBUTING.md
After dropping project-specs.md last commit, three categories of
content were lost that have no other home in the repo:

1. The script prefix convention with its 'pick the right prefix,
   don't invent one' rule
2. The feedback-tier system (table + 'why these splits?')
3. The publishing workflow (tagged-release flow)

These are maintainer/contributor-facing material, not user-facing.
The standard OSS location for this kind of doc is CONTRIBUTING.md,
which keeps it separate from README.md (user docs) so the two
don't drift. README.md gains a pointer at the bottom.

The content is condensed: no config dumps, no restatements of
package.json, no historical commit-message examples. Only the
rationale that isn't already in the actual config files.
2026-09-05 00:35:00 +02:00
tmu e78f624cbb 🔥 Drop project-specs.md; absorb unique rationale into README
project-specs.md was a parallel document that restated most of what was
already in package.json, README.md, and the config files themselves.
The only content that wasn't already captured elsewhere was the
'why' behind a handful of non-obvious tooling choices, which now
lives in README.md as a new 'Tooling decisions' subsection.

This eliminates the drift problem between the two docs (the source
of drift in the previous commit) by having one source of truth for
'what' (the config files) and one source for 'why' (README).
2026-09-05 00:21:56 +02:00
tmu 1a749b13d6 📝 Reconcile project-specs.md and cspell.json with the feature/setup branch 2026-09-05 00:13:10 +02:00
tmu 1fe3dbe28b ✨ Add npm run fix aggregator for the fix:* scripts
Override the prior design choice: there is now an npm run fix
script that runs fix:oxlint && fix:oxfmt. Rationale: the friction
of having to remember and run two separate fix commands after
npm run check outweighs the 'intentional fixes' argument once
check has already told you which fixers are needed. The diff
after running fix remains the review surface.

- package.json: new 'fix' script (npm run fix:oxlint && npm run fix:oxfmt)
- project-specs.md: updated the FIX section to document the new
  aggregator, removed the 'no fix aggregator by design' text in
  two places (FIX section and PREFIX CONVENTION section)
- README.md: same updates, plus the Development section header
  changed from 'Format/Fix' to 'Individual fixes' to reflect
  the new hierarchy
2026-09-04 23:58:18 +02:00
tmu 8a8d57172c ⬆️ Update cspell 2026-09-04 23:53:26 +02:00
tmu 7e9401590e ✨ Add pre-push hook that runs the test suite
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).
2026-09-04 23:41:59 +02:00
tmu 74e4538094 📝 Document the three-tier check system (pre-commit / check / CI)
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.
2026-09-04 23:36:32 +02:00
tmu 34e9b52ee8 ♻️ Move type-aware config to .oxlintrc.json; use source-level disable directives
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.
2026-09-04 23:27:58 +02:00
tmu 2b3ef0f721 ✨ Add oxlint-tsgolint for type-aware linting
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.
2026-09-04 23:20:02 +02:00
tmu ec98223f8d ✨ Add knip for unused dependency and dead code detection
- 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
2026-09-04 23:08:56 +02:00
tmu 3fa72820df ✨ Add @arethetypeswrong/cli (attw) as publish:attw
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
2026-09-04 18:08:23 +02:00
tmu 01d1084e1f 🚚 Rename publint script to publish:publint and document prefix convention
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).
2026-09-04 18:01:01 +02:00
tmu 24a3880aad 🐛 Fix test scripts: Node 26 doesn't auto-discover tests in directory args
node --test src/ on Node 26+ treats src/ as a module path, not as a
directory to scan for test files, producing a 'Cannot find module'
error. Switch the test scripts to an explicit glob that lists all
*.test.ts files under src/.

Affects test, test:ci, test:unit.
2026-09-04 17:47:30 +02:00
tmu 7de4604ab4 ♻️ Move emit-only options from tsconfig.json to tsconfig.build.json
- Build-only options (target, declaration*, sourceMap, outDir) live in the
  build config where they belong
- Drop redundant options (composite: false is the default;
  allowSyntheticDefaultImports is implied by esModuleInterop; resolveJsonModule
  is unused)
- Drop lib override to inherit es2025+ libs from @tsconfig/node26
- Add explicit include to tsconfig.build.json (include is not inherited via
  extends, only exclude was carrying the file selection by accident)
2026-09-04 17:46:03 +02:00
tmu b649013d1c ✨ Extend tsconfig from @tsconfig/node26 and @tsconfig/strictest
- Replace hand-rolled strict flags with @tsconfig/strictest
- Pick up Node 26 module/lib/target from @tsconfig/node26
- Add exactOptionalPropertyTypes (the main strictness gain)
- Pin target to es2024 to remain conservative for the published build
2026-09-04 17:33:43 +02:00
tmu c6f7a5d1b0 🔧 Reconcile config/doc contradictions and remove stale entries 2026-09-03 22:08:01 +00:00
tmu 99fd85ca7e 🔧 Resolve oxlint warnings; defer stylistic rules to oxfmt 2026-09-03 21:33:04 +00:00
tmu b3a4e14f45 🔥 Drop typescript.tsdk references; TS 7 is provided by the typescriptteam extension 2026-09-03 21:09:38 +00:00
tmu c3f913fae9 🔧 Fix small issues with the project setup 2026-09-03 21:01:28 +00:00
tmu 0598a9eb4a 🔧 Expand oxfmt to format project config and docs files 2026-09-03 20:58:33 +00:00
tmu 7349d0dec2 🔥 Remove sort-package-json in favor of oxfmt's built-in sortPackageJson 2026-09-03 20:58:17 +00:00
tmu e45592ae00 🔧 Unify lefthook and package.json scripts via LEFTHOOK_FILES env var
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
2026-09-03 18:44:19 +00:00
tmu 45b6c141e8 🔧 Migrate lefthook config to v2 schema
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
2026-09-03 18:12:01 +00:00
tmu 9bc87aab87 ⬆️ Upgrade devDependencies to latest major versions
- @types/node 22.20.1 -> 26.4.1
- c8 10.1.3 -> 12.0.0
- check-outdated 2.16.1 -> 3.0.0
- cspell 8.19.4 -> 10.2.1
- lefthook 1.13.6 -> 2.1.12
- sort-package-json 3.7.1 -> 4.0.0
- type-fest 4.41.0 -> 5.9.0

Verified all configs remain compatible without changes:
- cspell 10 reads the existing JSON config (v0.2 format unchanged)
- lefthook 2 accepts the v1-style config (pre-commit.jobs[], commands:)
- c8 12 still wraps 'node --test --strip-types' and produces the same
  text/lcov/html reports
- check-outdated 3 keeps the --ignore-packages and --ignore-pre-releases
  flags we use

Full pipeline passes:
- npm run check exits 0 (oxlint, oxfmt, tsc, cspell, sort-package-json,
  check-outdated all green)
- npm test: 6/6 pass
- npm run test:ci: coverage report generated successfully
- lefthook run pre-commit: hooks execute correctly
2026-09-03 14:08:28 +00:00
tmu 0c3e51c768 ✅ Ignore coverage directory in .gitignore 2026-09-03 13:54:34 +00:00
tmu ab6eb0f34c 📚 Sync project-specs.md and package.json with current state
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
2026-09-03 13:42:37 +00:00
tmu cb5b302238 🔧 Remove Prettier from editor formatter config
Prettier is no longer a project dependency and oxc.oxc-vscode handles
formatting for all file types the project touches. Switch [json],
[jsonc], [markdown], [mdx], and [yaml] formatters from
esbenp.prettier-vscode to oxc.oxc-vscode (oxfmt under the hood,
Prettier-compatible for JS/TS and native for JSON/MD/YAML).

Drop 'esbenp' from cspell dictionary (no longer referenced).
2026-09-03 13:09:53 +00:00
tmu 9a242225ee 🐛 Use .ts extensions in imports for node --strip-types
Node's --strip-types does not rewrite import specifiers the way bundlers
and Deno do, so 'import ... from "./match.js"' literally looks for
match.js (not match.ts) and fails with ERR_MODULE_NOT_FOUND at test
time. Fix by switching source imports to .ts extensions and letting
TypeScript's rewriteRelativeImportExtensions do the rewrite at build
emit time.

Changes:
- tsconfig.json: enable allowImportingTsExtensions (works because the
  root config has noEmit: true)
- tsconfig.build.json: enable rewriteRelativeImportExtensions so the
  emitted dist/*.js files keep './match.js' style imports (correct for
  consumers), not './match.ts'
- src/index.ts: import './match.ts' and './pattern.ts'
- src/match.ts: import './pattern.ts'
- src/index.test.ts: import './index.ts'

Verified: npm run test runs tsc --noEmit (passes) and node --test
--strip-types src/ — all 6 tests pass. tsc -p tsconfig.build.json
emits dist/*.js with './match.js' imports as before; consumers see no
change.
2026-09-03 13:05:20 +00:00
tmu 77dc40da07 🔧 Declare Node 26 as the supported runtime
- Add .node-version (single line: '26') so fnm/nvm/volta/mise and the
  VS Code Node version manager extension auto-switch when entering the
  project directory
- engines.node in package.json: bump from '>=22.6' to '>=26' (npm-side
  install-time declaration)
- Drop --experimental-strip-types flag from node --test invocations:
  Node 26 has --strip-types unflagged
- .github/workflows/ci.yml: switch from hard-coded node-version: 24 to
  node-version-file: .node-version in both build and publish jobs so CI
  follows the .node-version file (single source of truth)
- .npmignore: add .node-version (developer-tool file, not for publish)
- .vscode/settings.json: pin js/ts.tsdk.path to node_modules/typescript/lib
  so the editor uses the project's TS 7, not a different global install
- .vscode/extensions.json: swap ms-vscode.vscode-typescript-next for
  typescriptteam.native-preview (the official TS team extension that
  ships nightly TS support, including TS 7)
- cspell: add 'esbenp' and 'typescriptteam' to dictionary
- package-lock.json: regenerated for engines.node >=26

tsconfig.json target/lib are unchanged: es2024 is the highest stable ES
target TS 7.0.2 ships, and Node 26 supports all ES2024 features natively.
2026-09-03 12:45:32 +00:00
tmu e5df17310e ✅ Add expect-type for type-level tests
- Install expect-type@1.4.0 (devDependency)
- New src/index.test.ts exercises the public API with both runtime
  assertions (node:assert/strict inside node:test) and type-level
  assertions (expectTypeOf().toEqualTypeOf, toMatchTypeOf, toHaveProperty)
- Tests are excluded from tsc -p tsconfig.build.json (no .d.ts pollution
  in dist/)
- Type-level checks happen during tsc --noEmit (run by 'npm run check' and
  the test script); expectTypeOf assertions have no runtime side effect,
  so they pair naturally with node --test in the same test() blocks
- cspell: add EDITMSG to dictionary (used in commit-message-template path)
- oxlint test-file overrides: silence no-nodejs-modules (we use node:
  builtins) and no-magic-numbers (literals in tests are fine)

Coverage:
- match() returns a builder with with/exhaustive/otherwise
- P.literal narrows to its literal type, P.type to its typeof target
- exhaustive() returns the union of handler return types
- otherwise() falls back when no case matches
- exhaustive() throws when no case matches
2026-09-03 10:39:15 +00:00
tmu 08384bbc82 ✨ Add initial pattern-matching API
Introduce the core builder and pattern constructors:

- match(value): chain .with(pattern, handler) and terminate with
  .exhaustive() (throws on no match) or .otherwise(handler).
- P.literal, P.type, P.when, P.any, P.shape for pattern construction.
- Each pattern is a Matcher<T> whose matches acts as a type guard, so
  handler parameters are narrowed to the matched type.

The MatchBuilder accumulates handlers and returns a new builder per
.with() call (immutable chaining); finalization runs the cases in order
and returns the first match's handler result.

TypeScript configuration relaxed in .oxlintrc.json:
- Disable eslint/no-undefined (we use undefined as the no-match sentinel)
- Disable eslint/sort-keys (handler order matters; alphabetical would
  be wrong)
- Disable eslint/id-length (T/R/U/V generics are standard TS convention)
- Disable import/no-named-export (false positive for library entry points)

Verified:
- tsc --noEmit passes with strict + noUncheckedIndexedAccess
- tsc -p tsconfig.build.json emits dist/*.js + .d.ts + sourcemaps
- node -e "import('./dist/index.js')" loads and exposes { match, P }
2026-09-03 10:35:34 +00:00
tmu 6c67e91e68 🔥 Remove Vite scaffold leftovers
- Delete public/vite.svg, src/typescript.svg, src/style.css
- Empty public/ directory removed
- .npmignore: drop 'public/' entry (directory no longer exists), add
  .oxlintrc.json and .oxfmtrc.json to ignore list
- cspell: add .oxfmtrc, .oxlintrc, .sortpackagerc to dictionary so
  config-file name references aren't flagged
- lefthook: drop 'outdated' from pre-commit (kept in commands block for
  explicit CI invocation); it shouldn't block commits for being behind on
  upstream patch releases
2026-09-03 10:28:08 +00:00
tmu 078a8c30fd 🔧 Replace ESLint and Prettier with oxlint and oxfmt
- 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
2026-09-03 10:25:47 +00:00
tmu 3530262261 🔧 Replace Vite/Vitest with TypeScript 7 and node --test
- Use tsc 7 for build (vite no longer needed for a Node library)
- Use node --test with --experimental-strip-types for tests (vitest dropped)
- Use c8 for coverage instead of vitest's built-in coverage
- Add tsconfig.build.json separating typecheck from emit config
- Drop DOM lib, add strict noUncheckedIndexedAccess/noImplicitOverride
- Pin engines.node to >=22.6 (required for --experimental-strip-types)
- Update CI to Node 24 LTS (unflagged strip-types)
- Add @types/node for node:test / node:assert types
2026-09-03 09:54:21 +00:00
tmu cac1349c95 🎉 Initial commit 2026-02-02 13:38:42 +01:00