development/ had restated the actionables that CONTRIBUTING.md owns: the branching step list, the script prefix list, the feedback-tier rule of thumb, the commit convention and the type-driven test loop. Those now live only in CONTRIBUTING.md; development/ keeps the decision blocks and links to the rule. development/README.md, CONTRIBUTING.md and AGENTS.md state the 'write each fact once' principle explicitly.
2.4 KiB
Testing
For this library the types are the feature — narrowing, exhaustive()
returns, the Matcher<T> contract — so a runtime-only test loop would verify
the wrong thing. The contributor-facing commands are in
CONTRIBUTING.md; this file records why the loop is shaped
the way it is.
Type-driven development
The rules — the loop and the pairing rule — are in CONTRIBUTING.md § Testing discipline (type-driven). What follows is why the loop is type-driven and what was rejected.
Decision (2026-09)
Test-driven development is type-driven here: the compile-time expectation is written before the runtime assertion, and both before the implementation.
Why
- A runtime-only test can pass while the type is wrong, and a type-level library would then ship a broken feature that its tests bless.
- The type error is a more precise spec than a failing assertion, because it states the exact expected type before the logic exists.
Rejected
- Runtime-first (classic red/green): it verifies the value, not the contract, and the contract is the product.
- Testing the type only: it would not catch handler wiring,
exhaustive()throwing, or theotherwisefallback (seesrc/index.test.ts).
Enforcement
Type-first is also 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 so both the type check and the runtime assertion pass, never suppress the
ones you can't make pass.
The runner is node --test --strip-types "src/**/*.test.ts" and the tiers are
listed in
CONTRIBUTING.md § Development commands.
c8 uses V8 coverage, so the --strip-types source is instrumented without a
build step. The runner relies on the .ts import-extension convention (see
tooling.md).
Known issues
- The type-aware linter misidentifies
expectTypeOf()as a floating promise, sosrc/index.test.tscarries a file-leveloxlint-disable typescript/no-floating-promiseswith an explanatory comment. This is a known false positive, not a rule we want off project-wide (see tooling.md § oxlint-disable directives live next to the code).