getPrimitiveUnionMatcherPartialW was declared with the intended
P-inferring signature but assigned via `as any`, hiding that the
declared parameter was not assignable to the implementation's. The
union in PatternPrimitiveUnionPartial makes the `_` arm demand
`_ ∈ keyof P`, which the exhaustive arm cannot prove.
Intersect the inference hook Simplify<P> with the implementation's
parameter shape, R pinned to PatternReturns<P>, so the assignment
type-checks while callers keep inferring P from the argument.
Add a spec for the exhaustive (no `_`) pattern.
launch.json: 'Debug current test file' F5 config running
node --test --strip-types on the active file.
settings.json: register .ts with the connor4312.nodejs-testing
extension via nodejs-testing.extensions, using --strip-types (the
repo pins node >=26 with native type stripping) instead of tsx, so
the extension discovers src/*.test.ts and shows Run/Debug lenses
above test() calls in the native Testing UI.
extensions.json: recommend connor4312.nodejs-testing; cspell.json:
allow 'connor' for the extension id.
.gitignore: un-ignore .vscode/launch.json so the config is shared.
Rework every test body into // Arrange / // Act / // Assert blocks
separated by blank lines: the factory is arranged once, the matcher is
built from it in a single act, and all type/runtime checks sink to the
end. index.test.ts adopts the same shape.
Also migrate off expect-type's deprecated toMatchTypeOf: the checks
are assignability tests, so toExtend is the faithful replacement.
Type-driven tests: every expectTypeOf pairs an assert, covering the
header matrix (exhaustive vs partial patterns, strict R vs widened
returns, string and numeric keys, _ fallback wiring).
Negative cases use Parameters<typeof factory>[0] assignability because
expect-type's .not.toBeCallableWith collapses to never on these generic
Simplify<>-wrapped signatures.
Same documented expectTypeOf floating-promise false positive as
index.test.ts, so the same file-level disable applies; testing.md
known-issue updated to list both files.
Curried matchers over string|number literal unions in four variants
(exhaustive/partial x strict/widened return inference), per the header
matrix in src/primitive.ts.
- Adds type-fest for Simplify/ValueOf.
- Deliberate oxlint escape hatches (as any dispatch) until a cast-free
formulation lands; see the file comments.
Actionable sentence in CONTRIBUTING.md § Rules the tools don't enforce;
the why — humans skim, agents imitate the dominant style, so verbosity
compounds — in development/README.md § Decision blocks. Both sides in
one commit per the write-each-fact-once rule.
DefinitelyTyped keeps the @types/node latest dist-tag on the LTS line,
so check-outdated compared the current-line types against a lower
version: a permanent "reverted" flag, scan exit 1, zero signal. Ignored
by package name; --types filtering and a version-scoped pin rejected
(rationale in development/tooling.md).
@types/node to ^26.6.1 and cspell to ^10.3.2 — the only packages
check-outdated found behind the registry, both bumps within the existing
semver ranges. cspell 10.3.2 drops its transitive
fast-json-stable-stringify. npm run verify is green with no rule or
format fallout. maintain:outdated still flags @types/node as "reverted"
because DefinitelyTyped's latest dist-tag stays on the 22.x LTS series;
known false positive, advisory scan only.
`create:finish` leaves the merge local until `create:release` pushes it, so
`main` is routinely ahead of its upstream between a merge and a release. The
branch front door demanded an exact match and refused until it was pushed,
forcing a manual `git push` that the model deliberately keeps out of it.
Reject only a `main` that is behind its upstream — matching `create:finish`,
which already tolerates being ahead — and record why pushing from `finish` was
rejected instead. The push stays with `create:release`, so the merge remains
reviewable locally, and the known issue about the deadlock is gone.
Resolves: backlog task "Resolve the finish/push tension".
A merged branch must carry a final commit adding a short summary of the
work under [Unreleased] in CHANGELOG.md. create:release derives the bump
heuristic from that body, so notes have to exist before release day;
rationale in development/workflow.md.
Same decisions, rationale, rejected alternatives and known issues, said with
less padding: ~5,530 -> ~4,730 words (-15%). Every fact from the first draft is
kept; only the wording, duplicated lead-ins and restated context are cut.
The GitHub Flow description and the type-first enforcement sentence still
appeared in both CONTRIBUTING.md and development/. development/ now carries only
the context and rationale that is not in CONTRIBUTING.md.
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.
Adds a Documentation task to validate Markdown code fences against src/, and a
Workflow task for the create:finish / create:branch upstream mismatch recorded
in development/workflow.md.
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.
branch.sh, finish.sh, release.sh, runner-image.sh and release-notes.sh
carried multi-paragraph 'how we got here / rejected' essays in comments. They
now live in the matching development/ category file, and each script keeps a
one-line pointer so the rationale is single-homed and cannot drift.
Follows the Perl/CPAN order: name + one-line description, Synopsis, Description,
Examples, API reference, License. Tooling, CI and decision content moved to
development/; the README keeps only user-facing material and a pointer to
CONTRIBUTING.md and AGENTS.md. No version line and no package.json link.
Adds Setup, Development commands, Code style and formatting (with the VSCode
extension hint) and Submitting changes. The decision prose moves to
development/; this file keeps the actionable rules (feedback-tier table, script
prefix list, standards) and links to the category files for the why.
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.
The act-ci image is rebuilt only when Node is bumped, which changes the
tag, so the runner's default no-force-pull behavior already picks up every
normal update. Forcing a pull would re-pull on every job and a missed
runner-side image change is acceptable, so document that decision as a
known issue with a manual docker rmi workaround instead of tracking a
force-pull task.
Point 1 dropped the GITEA_TOKEN gate (the run's automatic github.token is
always present), and point 2 added the all-or-nothing check; keep the checked
task from claiming behavior the workflow no longer has.
Drop the GITEA_TOKEN gate and pass no explicit token: gitea-release-action
defaults to the run's automatic github.token, so the release page needs only
contents: write. Keep the npm publish gated on NPM_TOKEN, but add a final
always() step that fails the job unless both the release page and npm publish
reported success, so a skipped npm half is an explicit red job instead of a
silently green one.
Record the two optional secrets and why the job lifts them into env: the
secrets context is unavailable in a step if, so env is the only place the gate
can read them.
The publish job aborted at the top-of-job NPM_TOKEN assert, so every tag run
since 0.1.1 failed before checkout and nothing was ever published. Lift both
optional secrets into job-level env (the secrets context is not allowed in a
step if) and gate each publish step: the Gitea release on GITEA_TOKEN, npm
publish on NPM_TOKEN. An absent secret now skips only its step, so a tag without
the maintainer's secrets stays green after the packaging checks.
Also restores the runner force-pull task that the new entry had replaced; it is
still open (CONTRIBUTING § CI runner image documents the stale-image failure).
Latest releases of the two oxc tools; both minor bumps within 0.x/1.x
semver ranges. check-outdated now reports zero outdated dependencies
and npm run verify passes with no rule or format fallout.
All steps confirmed green in CI ('Found in cache @ /opt/hostedtoolcache/node/
26.8.2/x64', no download). The runner-side force-pull item moves to its own
task, since a changed image under an unchanged tag is still silently ignored.
The diagnostics did their job (missing <version>/<arch>.complete marker, now
written by the image). Replace the noise with a fail-fast assert that names
the same invariant, mirroring the publish job's NPM_TOKEN check: a stale tag
on the runner would otherwise silently reintroduce the per-job download, with
nothing in the repo to catch it.
setup-node still downloaded although the image carried a perfect
/opt/hostedtoolcache/node/26.8.2/x64/ tree. actions/tool-cache accepts a
cached tool only when the sibling marker <version>/<arch>.complete exists
(tc.find() tests it explicitly); a bare directory is ignored, so the probe
fell through to the download. The marker is what tc.cacheDir() writes after
installing a tool, so the baked entry must create it too.
Job-container diagnostics also confirmed the path was never in question:
RUNNER_TOOL_CACHE=/opt/hostedtoolcache, no mount over it, node -v from the
baked path prints v26.8.2.
CONTRIBUTING records both invariants — the marker, and the fact that a
Dockerfile change keeps the same tag, which forcePull=false can hide.