From 75f2185b326b828e9a883f41307bccc6d7f733c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 15 Sep 2026 09:05:41 +0000 Subject: [PATCH 1/7] :memo: Track the CI publish hardening task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- backlog.tasks | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backlog.tasks b/backlog.tasks index b0e2ff0..cba9f7e 100644 --- a/backlog.tasks +++ b/backlog.tasks @@ -54,3 +54,9 @@ Maintenance: ✔ Guard the invariant in CI (`Assert the baked tool cache is present`) @done ☐ Enable force-pull for the runner so a changed act-ci image is never missed @low → the tag encodes only the Node version, so a Dockerfile change yields new content under an unchanged tag; with `forcePull=false` the runner keeps the old image (see CONTRIBUTING § CI runner image) +☐ Improve CI publish + ☐ Check whether publish job is only run on tags, if not, guard it + ☐ Gate only single steps + ☐ Do not publish to npm, if NPM_TOKEN is not set (e.g. PRs from forks) + ☐ Do not publish to Gitea, if GITEA_TOKEN is not set (e.g. PRs from forks) + ☐ Otherwise run the steps From 8915eb8faa83fd57989890c29ac29209d9e96c4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 15 Sep 2026 09:06:35 +0000 Subject: [PATCH 2/7] :construction_worker: Gate the publish steps on their tokens 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. --- .gitea/workflows/ci.yml | 52 ++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index abc878c..d05426e 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -65,8 +65,8 @@ jobs: - uses: actions/checkout@v4 # Fail fast when the job container is not the baked image: a stale # tag on the runner (`forcePull=false` in its pull log) silently - # reintroduces the per-job download. Mirrors the publish job's - # NPM_TOKEN assert — cheap, and it names the invariant. + # reintroduces the per-job download. Cheap, and it names the + # invariant. - name: Assert the baked tool cache is present run: | test -f "/opt/hostedtoolcache/node/$(tr -d '[:space:]' < .node-version)/x64.complete" @@ -141,20 +141,18 @@ jobs: # `.npmrc` rewrite here; only the Node download is skipped. container: image: gitea.e1nsnull.de/tmu/act-ci:26.8.2 - # The release page is created with the run's automatic Gitea token - # (`github.token`), not `NPM_TOKEN`, so it needs `contents: write`. - permissions: - contents: write + # Lift the optional publish secrets into job-level `env` so the steps + # below can gate on them: `secrets` is not an allowed context in a step + # `if` (see GitHub's context-availability table), `env` is. An unset + # secret arrives as the empty string, which is exactly the skip signal. + # A tag pushed without the maintainer's secrets (a fork, a manual + # dispatch) now runs the packaging checks and skips only the publish + # steps whose token is missing, instead of failing the job at an assert. + # Set both in the Gitea repo: Settings → Actions → Secrets. + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} steps: - # Double-gate: publish only runs on a tag *and* aborts here if NPM_TOKEN - # is unset, so a tag push never silently no-ops (or half-publishes). Set - # NPM_TOKEN in the Gitea repo: Settings → Actions → Secrets. - - name: Assert NPM_TOKEN is configured - run: | - if [ -z "${{ secrets.NPM_TOKEN }}" ]; then - echo "::error::NPM_TOKEN secret is not set — refusing to publish." - exit 1 - fi - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: @@ -173,19 +171,25 @@ jobs: path: dist/ - run: npm run publish:publint - run: npm run publish:attw - # The Gitea release page is created *before* `npm publish` on - # purpose: a broken page then fails CI without burning an npm - # version. The page is cheap to retry, a published version is not. - # The body is the matching Keep-a-Changelog section; an unknown tag - # makes the extractor exit non-zero, so the page can never go up - # empty. + # When both tokens are present the Gitea release page is created + # *before* `npm publish` on purpose: a broken page then fails CI + # without burning an npm version. The page is cheap to retry, a + # published version is not. The body is the matching + # Keep-a-Changelog section; an unknown tag makes the extractor exit + # non-zero, so the page can never go up empty. - name: Extract release notes from CHANGELOG.md + if: env.GITEA_TOKEN != '' env: TAG_REF: ${{ gitea.ref }} run: ./scripts/release-notes.sh "${TAG_REF#refs/tags/}" > release-notes.md - - uses: https://gitea.com/actions/gitea-release-action@v1 + - name: Create the Gitea release + if: env.GITEA_TOKEN != '' + uses: https://gitea.com/actions/gitea-release-action@v1 with: + token: ${{ env.GITEA_TOKEN }} body_path: release-notes.md - - run: npm publish --access public + - name: Publish to npm + if: env.NPM_TOKEN != '' + run: npm publish --access public env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ env.NPM_TOKEN }} From e90d2549e373697b362a305e79da419479e39d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 15 Sep 2026 09:06:39 +0000 Subject: [PATCH 3/7] :memo: Document the publish-step token gates 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. --- CONTRIBUTING.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 786164a..8928bf5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,18 +45,18 @@ Separately, some top-level scripts are **bare** (no prefix): the entry points th 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](./.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 | Gitea release page (body from CHANGELOG) + `publish:publint` + `publish:attw`, then `npm publish` | ~15s | +| 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](./.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 a release page (if `GITEA_TOKEN` is set) and `npm publish` (if `NPM_TOKEN` is set) | ~15s | ### Why these splits? @@ -115,3 +115,5 @@ Publishing is CI-only by policy. Local `npm publish` is not supported. The maint 2. The maintainer runs `npm run create:release`. VS Code opens `CHANGELOG.md` to finalize the `[Unreleased]` notes; because pubv refuses a dirty tree, any edit is committed first (then folded into the release commit), and pubv's interactive prompt suggests a version from those notes — the maintainer confirms or edits it. 3. `scripts/release.sh` creates a single release commit (graduated changelog + package.json bump, amended into one commit), tags it, and pushes everything to Gitea. 4. CI fires on both pushes: the `publish` job runs on the tag (`build` + publish-tier checks + release page + `npm publish`), while the branch run's `release-gate` job recognizes the release commit and skips `build`/`maintain` — the tag verifies the identical SHA, so no work is duplicated. The job graph lives in [.gitea/workflows/ci.yml](./.gitea/workflows/ci.yml) — keep that file, not this list, as the source of truth. The publish-tier checks must pass before the artifact is published. The `publish` job also creates the Gitea release page from the matching Keep-a-Changelog section (`scripts/release-notes.sh`); it runs _before_ `npm publish` so a broken page fails CI without consuming a version, and `npm publish` stays the last step. + +Each publish step is gated on its own secret, so a tag run without the maintainer's secrets (a fork, a manual dispatch) still runs the packaging checks and goes green instead of failing an assert: the release page needs `GITEA_TOKEN` and `npm publish` needs `NPM_TOKEN`. `secrets` is not an allowed context in a step `if`, so the job lifts both into job-level `env` and tests `env. != ''`. Create both under Settings → Actions → Secrets: `NPM_TOKEN` with npm publish rights, `GITEA_TOKEN` a Gitea token with repository write access. With both set, the combined run behaves as before. From 60e416b0fe839f6fdbe64278ace7bf629a4bbaf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 15 Sep 2026 09:06:48 +0000 Subject: [PATCH 4/7] :memo: Check off the CI publish task The publish job was already tag-only; the coarse NPM_TOKEN assert is replaced by per-step env gates, so an unset secret now skips only its own step. --- backlog.tasks | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backlog.tasks b/backlog.tasks index cba9f7e..b7cd0e3 100644 --- a/backlog.tasks +++ b/backlog.tasks @@ -54,9 +54,9 @@ Maintenance: ✔ Guard the invariant in CI (`Assert the baked tool cache is present`) @done ☐ Enable force-pull for the runner so a changed act-ci image is never missed @low → the tag encodes only the Node version, so a Dockerfile change yields new content under an unchanged tag; with `forcePull=false` the runner keeps the old image (see CONTRIBUTING § CI runner image) -☐ Improve CI publish - ☐ Check whether publish job is only run on tags, if not, guard it - ☐ Gate only single steps - ☐ Do not publish to npm, if NPM_TOKEN is not set (e.g. PRs from forks) - ☐ Do not publish to Gitea, if GITEA_TOKEN is not set (e.g. PRs from forks) - ☐ Otherwise run the steps +✔ Improve CI publish @done + ✔ Check whether publish job is only run on tags, if not, guard it @done + ✔ Gate only single steps @done + ✔ Do not publish to npm, if NPM_TOKEN is not set (e.g. PRs from forks) @done + ✔ Do not publish to Gitea, if GITEA_TOKEN is not set (e.g. PRs from forks) @done + ✔ Otherwise run the steps @done From 79b4d8c005eedad587bad7466b10f915759a1adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 15 Sep 2026 09:25:24 +0000 Subject: [PATCH 5/7] :recycle: Use the automatic token and fail closed on a partial release 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. --- .gitea/workflows/ci.yml | 49 ++++++++++++++++++++++++++--------------- CONTRIBUTING.md | 26 +++++++++++----------- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index d05426e..50f6d96 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -141,17 +141,17 @@ jobs: # `.npmrc` rewrite here; only the Node download is skipped. container: image: gitea.e1nsnull.de/tmu/act-ci:26.8.2 - # Lift the optional publish secrets into job-level `env` so the steps - # below can gate on them: `secrets` is not an allowed context in a step - # `if` (see GitHub's context-availability table), `env` is. An unset - # secret arrives as the empty string, which is exactly the skip signal. - # A tag pushed without the maintainer's secrets (a fork, a manual - # dispatch) now runs the packaging checks and skips only the publish - # steps whose token is missing, instead of failing the job at an assert. - # Set both in the Gitea repo: Settings → Actions → Secrets. + # The release page is created with the run's automatic Gitea token + # (`github.token`), so it needs `contents: write`. + permissions: + contents: write + # The npm token is optional: `secrets` is not an allowed context in a + # step `if` (see GitHub's context-availability table), so it is lifted + # into job-level `env`, where an unset secret arrives as the empty + # string and skips the publish rather than attempting an unauthenticated + # one. Set NPM_TOKEN in the Gitea repo: Settings → Actions → Secrets. env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -171,25 +171,38 @@ jobs: path: dist/ - run: npm run publish:publint - run: npm run publish:attw - # When both tokens are present the Gitea release page is created - # *before* `npm publish` on purpose: a broken page then fails CI - # without burning an npm version. The page is cheap to retry, a - # published version is not. The body is the matching - # Keep-a-Changelog section; an unknown tag makes the extractor exit - # non-zero, so the page can never go up empty. + # The Gitea release page is created *before* `npm publish` on + # purpose: a broken page then fails CI without burning an npm + # version. The page is cheap to retry, a published version is not. + # The body is the matching Keep-a-Changelog section; an unknown tag + # makes the extractor exit non-zero, so the page can never go up + # empty. - name: Extract release notes from CHANGELOG.md - if: env.GITEA_TOKEN != '' env: TAG_REF: ${{ gitea.ref }} run: ./scripts/release-notes.sh "${TAG_REF#refs/tags/}" > release-notes.md - name: Create the Gitea release - if: env.GITEA_TOKEN != '' + id: gitea_release uses: https://gitea.com/actions/gitea-release-action@v1 with: - token: ${{ env.GITEA_TOKEN }} body_path: release-notes.md - name: Publish to npm + id: npm_publish if: env.NPM_TOKEN != '' run: npm publish --access public env: NODE_AUTH_TOKEN: ${{ env.NPM_TOKEN }} + # All-or-nothing: the tag is only released once *both* the release + # page and the npm package are up. A skipped npm publish (NPM_TOKEN + # unset) has no `success` outcome, so `always()` reaches this check + # even after a failure and turns the skipped half into an explicit + # red job instead of a silently green one. + - name: Require both releases + if: always() + run: | + GITEA="${{ steps.gitea_release.outcome }}" + NPM="${{ steps.npm_publish.outcome }}" + if [ "${GITEA}" != success ] || [ "${NPM}" != success ]; then + echo "::error::incomplete release — gitea=${GITEA:-skipped} npm=${NPM:-skipped}" + exit 1 + fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8928bf5..f38e45b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,18 +45,18 @@ Separately, some top-level scripts are **bare** (no prefix): the entry points th 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](./.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 a release page (if `GITEA_TOKEN` is set) and `npm publish` (if `NPM_TOKEN` is set) | ~15s | +| 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](./.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 | ### Why these splits? @@ -116,4 +116,4 @@ Publishing is CI-only by policy. Local `npm publish` is not supported. The maint 3. `scripts/release.sh` creates a single release commit (graduated changelog + package.json bump, amended into one commit), tags it, and pushes everything to Gitea. 4. CI fires on both pushes: the `publish` job runs on the tag (`build` + publish-tier checks + release page + `npm publish`), while the branch run's `release-gate` job recognizes the release commit and skips `build`/`maintain` — the tag verifies the identical SHA, so no work is duplicated. The job graph lives in [.gitea/workflows/ci.yml](./.gitea/workflows/ci.yml) — keep that file, not this list, as the source of truth. The publish-tier checks must pass before the artifact is published. The `publish` job also creates the Gitea release page from the matching Keep-a-Changelog section (`scripts/release-notes.sh`); it runs _before_ `npm publish` so a broken page fails CI without consuming a version, and `npm publish` stays the last step. -Each publish step is gated on its own secret, so a tag run without the maintainer's secrets (a fork, a manual dispatch) still runs the packaging checks and goes green instead of failing an assert: the release page needs `GITEA_TOKEN` and `npm publish` needs `NPM_TOKEN`. `secrets` is not an allowed context in a step `if`, so the job lifts both into job-level `env` and tests `env. != ''`. Create both under Settings → Actions → Secrets: `NPM_TOKEN` with npm publish rights, `GITEA_TOKEN` a Gitea token with repository write access. With both set, the combined run behaves as before. +The Gitea release page uses the run's automatic token (`github.token`), so it only needs `contents: write`. `npm publish` is gated on `NPM_TOKEN`, lifted into job-level `env` because `secrets` is not an allowed context in a step `if`: an unset secret skips the publish instead of attempting an unauthenticated one. A tag is all-or-nothing, though — a final `always()` step fails the job unless both the release page and `npm publish` reported `success`, so a skipped or failed npm half turns the job red rather than silently green. Set `NPM_TOKEN` (npm publish rights) under Settings → Actions → Secrets. From f984576d4e89b5cb0944ae091b18a618bdc5840c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 15 Sep 2026 09:25:42 +0000 Subject: [PATCH 6/7] :memo: Record the automatic-token publish design in the backlog 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. --- backlog.tasks | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backlog.tasks b/backlog.tasks index b7cd0e3..3a3eb14 100644 --- a/backlog.tasks +++ b/backlog.tasks @@ -58,5 +58,6 @@ Maintenance: ✔ Check whether publish job is only run on tags, if not, guard it @done ✔ Gate only single steps @done ✔ Do not publish to npm, if NPM_TOKEN is not set (e.g. PRs from forks) @done - ✔ Do not publish to Gitea, if GITEA_TOKEN is not set (e.g. PRs from forks) @done + ✔ Do not publish to Gitea — uses the run's automatic `github.token`, so no secret gate is needed @done ✔ Otherwise run the steps @done + ✔ Fail the job unless both the Gitea release and npm publish succeeded @done From 76fe8993a7a0508dae4c409c952ce07d3fff0d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 15 Sep 2026 09:31:58 +0000 Subject: [PATCH 7/7] :memo: Track the docs cleanup and docs-site task --- backlog.tasks | 1 + 1 file changed, 1 insertion(+) diff --git a/backlog.tasks b/backlog.tasks index 3a3eb14..6a7c7e9 100644 --- a/backlog.tasks +++ b/backlog.tasks @@ -24,6 +24,7 @@ Bugs: Enhancements: Documentation: +☐ Clean up CONTRIBUTING.md and README.md, create docs ☐ Add usage examples to README.md ☐ Create `examples/` directory with runnable snippets ☐ Add comparison section vs. other TS pattern-matching libs