AGENTS.md and CONTRIBUTING.md now state that a decision, its rejected alternatives and its known issues belong in development/<category>.md, that the actionable rule stays in CONTRIBUTING.md, and that both change in the same commit. development/README.md names library.md in the category list and spells out that the sync runs in both directions.
11 KiB
Contributing
This document is for maintainers and contributors working on the project itself. End-user documentation is in README.md. The reasons behind the rules here — the decisions, rejected alternatives, and known issues — live in development/. The machine entry point for AI coding agents is AGENTS.md; keep this file as the prose home for the rules so agents and humans don't diverge.
Setup
- Clone the repository.
- Install Node.js >= 26 — see .node-version; the exact pinned version is what CI and the runner image use.
npm install.npm run setup— the one-time clone configuration (currently registers the commit-message template).
Development commands
- Build:
npm run build - Test:
npm run test,npm run test:ci - Watch:
npm run watch - Checks:
npm run check,npm run fix - Verify:
npm run verify— the definition of done - Maintenance:
npm run maintain— advisory only - Individual fixes:
npm run fix:oxfmt,npm run fix:oxlint
Feedback tiers
The tools are organized into a feedback ladder. Each tier catches different things at different costs; the rule of thumb is "earlier tiers fire more often, faster tiers catch less, slower tiers are more thorough":
| Tier | When | What it runs | Time |
|---|---|---|---|
npm run watch |
manual | watch:test — re-runs tests on file save |
~0.1s |
| Pre-commit (auto) | on stage | tsc + oxlint + oxfmt + cspell (staged files only) | ~1.3s |
| Pre-push (auto) | on push | npm test (full tsc + unit tests) |
~3.5s |
npm run check |
manual | Correctness gates: tsc + oxlint + oxfmt + cspell (whole project) | ~3s |
npm run verify |
manual | Definition of done: npm run check + unit tests, one shot |
~6s |
npm run fix |
manual | Auto-resolve fixable issues (lint, format) | ~3s |
npm run maintain |
manual / CI (advisory) | maintain:knip + maintain:outdated (whole-project + network scans) |
~10s |
| CI build (auto) | on push to main / tag |
build job (build + correctness + packaging) — see .gitea/workflows/ci.yml |
~30s+ |
| CI maintain (auto, non-blocking) | on push to main |
npm run maintain — reports, never fails the build |
~10s |
| CI publish (auto) | on tag | packaging checks + publish:publint / publish:attw, then the Gitea release page and npm publish (skipped, and the job failed, without NPM_TOKEN) |
~15s |
Before pushing, run npm run verify — the one-shot correctness gate. Run
npm run maintain only on a maintenance / update-deps branch. Why the splits
are where they are: development/workflow.md § Feedback tiers.
Testing discipline (type-driven)
For this library the types are the feature, so the loop is type → red →
green → refactor: write the expectTypeOf(...) assertion first, then the
runtime assert.*, then the implementation. Every test pairs the two; keep
them together. Type-first is enforced structurally: npm test runs
check:tsc before the test runner, so a wrong type can never be papered over by
a passing assertion. Per AGENTS.md § Never do, reach
green honestly — fix the types, never suppress the checks you can't make pass.
Full rationale: development/testing.md.
Code style and formatting
oxfmt is the formatter and oxlint is the linter (with type-aware rules).
npm run fix resolves the fixable issues; npm run check verifies without
writing. Suppressions must be fixed at the root — do not add oxlint-disable
directives or as casts to force a green run (see
AGENTS.md § Never do).
Suggested VSCode extensions are in .vscode/extensions.json; the project's formatter and linter are wired up there. Toolchain decisions: development/tooling.md.
Commit messages
Gitmoji subject, imperative mood, 50/72 wrapping. The template is
commit-message-template; npm run setup
(or npm run setup:git-commit-message) registers it as git's
commit.template. Examples and rationale:
development/workflow.md § Commit messages.
Script prefix convention
A new npm run script must reuse an existing prefix: create: / check: /
fix: / test: / watch: / maintain: / publish: / setup:. If none fits,
that's a signal the script doesn't belong in the pipeline — not a reason to
invent a new prefix. If it genuinely does belong, add the prefix to the list
here in the same commit as its first member; an undocumented prefix becomes
invisible and quietly accrues members. Full convention and why create: exists:
development/workflow.md § Script prefix convention.
Rules the tools don't enforce
CI and review will bounce these even though npm run check and the linters
don't catch them. They're the high-frequency things a contributor (or an agent)
reaches for by default:
- Source imports use
.tsextensions, never.js.node --strip-typesonly resolves the.tsform at test time; "pre-fixing" an import to.jsbreaks the inner loop. (why: development/tooling.md) - A new
npm runscript must reuse an existing prefix. See Script prefix convention. oxlint-disabledirectives live in source, not.oxlintrc.json. The trade-off must sit next to the code it silences. This is a human last-resort convention; agents must not add these — see AGENTS.md § Never do. (why: development/tooling.md)- Don't put slow / network / whole-project scans in
checkor pre-commit. Advisory scans are not correctness gates; they belong undermaintain:. (why: development/workflow.md) - New work starts with
npm run create:branch, never a hand-writtengit switch -c/git checkout -b. The command carries the branch precondition; branching around it skips the clean-tree, current-mainand green-baseline checks, and the skip is invisible until a failure can no longer be attributed. (why: development/workflow.md) - Work is merged back with
npm run create:finish, never a hand-writtengit merge. The command carries the merge-side preconditions (clean tree, currentmain, afeature//fix//chore/branch) and runsnpm run verifyafter the merge, so a merge cannot land unverified. (why: development/workflow.md) - There is no local
npm run publish, andpublish:publint/publish:attwdon't go incheck. (why: development/publishing.md) - A decision or its rationale belongs in
development/, not here. This file holds the actionable rule;development/<category>.mdholds why, the rejected alternatives and the known issues. When you change a rule, update its category file in the same commit and cross-link the two. (why: development/README.md)
Branching model
GitHub Flow (single-developer). Every change — feature, fix, refactor —
branches off main and is merged back via a local commit. There is no pull
request workflow on Gitea yet.
- Base branch:
main - Branch naming:
feature/<desc>/fix/<desc>/chore/<desc> - Starting work:
npm run create:branch -- <prefix>/<desc>. It refuses, without changing anything, unless the working tree is clean, no merge/rebase/cherry-pick is in progress,mainmatches its upstream, andnpm run testis green onmain. The prefix is your call, inferred from the task; the script validates it rather than guessing it. - Merging:
npm run create:finish(on the branch). It re-asserts the same preconditions, merges--no-ff, runsnpm run verify, and deletes the branch only after the merge is green. The push is left tocreate:release, so the merge stays local and reviewable. - CI runs on every push to
main— see Feedback tiers and .gitea/workflows/ci.yml.
Full rationale, including the front-door decisions and a known issue about
main being ahead of its upstream between a merge and the next push:
development/workflow.md § Branching model.
Submitting changes
There is no pull request workflow on Gitea yet, so a contribution is submitted as a branch that is merged locally:
npm run create:branch -- <prefix>/<desc>.- Commit your work (one or more commits, per the tests and style rules above).
npm run verify— the definition of done.npm run create:finishto merge the branch intomainand verify the result.- Present a handover for review. Once there are no further objections, the maintainer pushes.
When the project is promoted to GitHub, this step becomes a normal pull request
against main.
Publishing
Publishing is maintainer-only and CI-only. See development/publishing.md.