Every `ts`-tagged fence in README.md / CONTRIBUTING.md now becomes an
executed `node:test` case in a gitignored generated file, so a
documented example cannot drift from the API. The generator hoists and
merges the leading imports, rewrites the library specifier to the
`#test-tiny-pattern-ts` alias, and rejects a fence with no describing
paragraph or one that re-imports a prelude module.
CI regenerates via `pretest:ci`; `create:doc-tests` runs the generator,
formats, then typechecks the output against a scoped tsconfig that
relaxes `noUnusedLocals`. c8's default excludes already omit the
generated `*.test.ts`, so `test:ci` is unchanged.
Bump the copyright to 2026 in LICENSE and README, add the
comparison-section notes to the backlog, and trim the primitive-union
factory JSDoc now that the universe rules live in README § Caveats.
Export the matcher builder types from the barrel and rename them off the
internal Strict/Widening suffixes, so the type a factory returns is
nameable: PrimitiveUnionMatcher/W, TaggedUnionMatcher/W, and the
tagged-union factory types. Add TSDoc to every public symbol — the
declarations carry it into the published package — and pin the exported
names in src/index.test.ts.
Document the API in README and record the surface decision (and the
rejected matcher-shared export) in development/library.md.
test:ci now runs c8 with --all --include "src/**/*.ts" --100, so the
build job fails when any runtime file under src/ is untested. --all is
what makes the gate non-vacuous: without it c8 counts only the files the
suite happened to load, and a new untested module stays invisible.
Add src/index.test.ts to load the public barrel, which was previously
never imported at runtime and so read as 0% under --all. matcher-shared.ts
is types-only (an empty runtime image) and carries a file-level c8 ignore
with the reason.
Why 100% and the rejected alternatives: development/ci.md § Coverage
threshold.
The tagged-union matcher selected handler parameters with
`Extract<T, Record<K, V>>`, which only keeps a member whose
discriminant is a singleton literal. A property that is itself a
union (`{ color: "red" | "green" | "blue" }`) or optional
(`{ type?: "x" }`) matched no member, so every handler received
`never`, and the fallback saw the whole shape instead of the
unhandled tags.
Replace the selector with `Narrowed`, which distributes over `T`,
drops a member whose `K` cannot take the tag, and narrows `K`. The
`T[K] extends V` fast path keeps an exact member (a discriminated
union's declared interface) untouched. The fallback reuses
`Narrowed` over `Exclude<Tags, HandledTags>`, so a union-valued
property narrows to the unhandled tags rather than the whole shape.
Matching a defined tag on an optional property makes the key
required (`{ type: "x" }`); the `undefined` tag yields
`{ type?: never }` under `exactOptionalPropertyTypes` (absence) or
`{ type?: undefined }` when the property admits an explicit
`undefined`.
Remove every done/cancelled backlog item; the reasons they recorded are
already in development/ (library.md, testing.md, tooling.md, workflow.md)
and git history is the archive.
The one exception was the TS 7 LSP shutdown workaround, which lived only in
a code comment. Record it under development/testing.md § Autocomplete
(Known issue) before dropping the bug item.
Record the non-exhaustive broad-type hole in the primitive matcher and the
never-typed handler parameters the tagged-union matcher produces when a
property is a union, so both can be picked up as their own work units.
Extend the matcher universe beyond string | number so a union can carry
boolean, null and undefined members. true|false, null and undefined are
not property keys, so handler keys are their stringification while the
callback still receives the real member. Symbols are rejected (a brand
is compile-time only) and bigint is not a property key.
A fallback paired with a handler map that already covers `T` was still
accepted, even though its remainder is empty. Fold the guard into
`Handled`'s own (F-bounded) constraint so it is checked after inference;
a conditional in the fallback parameter is evaluated while `Handled` is
still its constraint and rejects every partial map whose handler callbacks
need contextual typing.
Rewrite development/library.md around the new shape: the fallback is a second
argument (so it can receive the remainder), `R` needs an inference site in the
handler map, `Exact` restores the excess-property check, and the overload order
is load-bearing. Record this branch's rejected alternatives (single-object `_`,
currying, `this`/HKT, variance/`const`/`NoInfer`/brands) and the redundant-
fallback known issue. Add the changelog note and check off the backlog task.
The autocomplete helper still carries a hand-rolled JSON-RPC client
(Content-Length framing, a pending-request map, server-request replies).
vscode-languageserver-protocol provides all of it plus typed requests, so
track the move as a task with subtasks.
Drop the strict/widened aliases from src/index.ts so the factories are
exported under their own names, and align the type tests, the autocomplete
probe (which now imports the public surface from ./index.ts) and library.md
with them.
Also correct the widened-overload comment: the excess-property check does not
apply to a generic P, so MatcherWidening's keyof guard — not the closed
constraint — is what rejects keys outside T/_, as library.md already
documented.
The matcher is now two three-overload factories; library.md documents why
the union merge, inferred universe, conditional RequireKeys and cases-first
paths were rejected, and lists the two open issues (the fallback sees all of
T; a redundant _ is still accepted). testing.md records the language server
as the autocomplete oracle, and CONTRIBUTING points the exception at the
matcher's own test file.
The backlog marks the design-doc and autocomplete groundwork done alongside
the adoption.
The example-code removal stripped the synopsis and examples with the
template modules; track bringing them back around the real API, with the
previous section order recorded so its placement is not re-guessed.
`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".
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.
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.
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).
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.
Record chore/fix-ci's two done steps and the remaining ops step (build/push
the image; first CI run is the acceptance test). Glyph/tag coupling honoured:
every ✔ line carries @done, open parent stays ☐.
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.
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.
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.
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.