From 844e80d650406f8b2e1460318a39a90d1ca0be54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 8 Sep 2026 22:51:35 +0200 Subject: [PATCH] :recycle: Group workflow scripts under a create: prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `branch` and `release` were bare commands, but bare in this repo means "how you invoke a tier" or "runs one tool" — neither fits a command that opens or closes a unit of work. They get their own prefix now, since a prefix is how this repo records _when_ a script runs. `create:` because both members genuinely create something (a branch, a release) and it is a plain verb rather than VCS slang. No bare `create` aggregator: running "all the workflows" describes nothing anyone wants, and `publish:*` already precedents a prefix without one. The rule "reuse an existing prefix, never invent one" now says what it actually means: a new prefix is allowed when the scripts belong in the pipeline, provided it enters both lists in the same commit as its first member. That is the lesson from `use:`, which is referenced by prose yet in none of the lists — already tracked in backlog.tasks. The bare-command sentence shrinks to `build`, `clean`, `verify`. --- AGENTS.md | 2 +- CONTRIBUTING.md | 13 +++++++------ package.json | 4 ++-- scripts/branch.sh | 39 ++++++++++++++++++++++++++------------- scripts/release.sh | 2 +- 5 files changed, 37 insertions(+), 23 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 33f4ce7..cf6c52a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,7 +41,7 @@ Never start a long-lived / blocking process such as `npm run watch`. It runs unt ### Working on tasks -- **Task with subtasks** (a task that has indented children): create the branch with `npm run branch -- /`, inferring the prefix from the task content (`feature/…` / `fix/…` / `chore/…`) — do not hand-write `git switch -c`, the command enforces the clean-tree / current-`main` / green-baseline precondition. Work on each subtask with commits, then present a concise handover for the user to review. Use this fixed shape: +- **Task with subtasks** (a task that has indented children): create the branch with `npm run create:branch -- /`, inferring the prefix from the task content (`feature/…` / `fix/…` / `chore/…`) — do not hand-write `git switch -c`, the command enforces the clean-tree / current-`main` / green-baseline precondition. Work on each subtask with commits, then present a concise handover for the user to review. Use this fixed shape: ```md ## Handover — diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d981352..599d727 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,10 +7,10 @@ This document is for maintainers and contributors working on the project itself. 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 `.ts` extensions, never `.js`.** `node --strip-types` resolves the `.ts` form at test time; `rewriteRelativeImportExtensions` emits `.js` in `dist/`. "Pre-fixing" an import to `.js` breaks the inner loop. (rationale: README § Tooling decisions) -- **A new `npm run` script must reuse an existing prefix** (`check:` / `fix:` / `test:` / `watch:` / `maintain:` / `publish:`). If none fits, that's a signal the script doesn't belong in the pipeline — not a reason to invent a new prefix. (see [Script prefix convention](#script-prefix-convention)) +- **A new `npm run` script must reuse an existing prefix** (`create:` / `check:` / `fix:` / `test:` / `watch:` / `maintain:` / `publish:`). 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 both lists here in the same commit; an undocumented prefix becomes invisible and quietly accrues members. (see [Script prefix convention](#script-prefix-convention)) - **`oxlint-disable` directives 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](./AGENTS.md#never-do). (rationale: README § Tooling decisions) - **Don't put slow / network / whole-project scans in `check` or pre-commit.** Advisory scans are not correctness gates; they belong under `maintain:`. (see [Feedback tiers](#feedback-tiers) and [Script prefix convention](#script-prefix-convention)) -- **New work starts with `npm run branch`, never a hand-written `git switch -c`/`git checkout -b`.** The command carries the branch precondition; branching around it skips the clean-tree, current-`main` and green-baseline checks, and the skip is invisible until a failure can no longer be attributed. (see [Branching model](#branching-model)) +- **New work starts with `npm run create:branch`, never a hand-written `git switch -c`/`git checkout -b`.** The command carries the branch precondition; branching around it skips the clean-tree, current-`main` and green-baseline checks, and the skip is invisible until a failure can no longer be attributed. (see [Branching model](#branching-model)) - **There is no local `npm run publish`, and `publish:publint` / `publish:attw` don't go in `check`.** (see [Publishing workflow](#publishing-workflow)) ## Commit messages @@ -23,6 +23,7 @@ Examples from history: `:sparkles: Add watch tier with watch:test child`, `:recy Script names in `package.json` use a prefix that signals _when_ the script is intended to run. A `:` script is implicitly aggregated by a `` script (if one exists) and run by the corresponding lefthook hook or CI step. Picking the right prefix documents the script's intended lifecycle: +- `create:*` — front doors of the repo's own workflow; these mutate git state rather than the source. `create:branch` opens a unit of work (asserts a clean tree, a current `main` and a green baseline before it branches), `create:release` closes one (maintainer-only). No bare `create` aggregator on purpose — see `publish:*` for the precedent. - `check:*` — read-only verification; never modifies files. Aggregated by `npm run check`. - `fix:*` — mutating counterpart of a `check:*` script. Aggregated by `npm run fix`; the diff is the review surface. - `test:*` — test scripts. `test` is the canonical entry point (`check:tsc` + unit tests); `test:unit` skips the typecheck for fast local iteration; `test:ci` adds c8 coverage. @@ -30,9 +31,9 @@ Script names in `package.json` use a prefix that signals _when_ the script is in - `maintain:*` — advisory repo-maintenance scans: read-only, but whole-project and/or network-bound, so never a correctness gate. Aggregated by `npm run maintain`. - `publish:*` — validates the _publishable artifact_ (e.g. `dist/`) rather than the source, so it needs a fresh build. (see [Rules the tools don't enforce](#rules-the-tools-dont-enforce) and [Publishing workflow](#publishing-workflow)) -A new script should pick the prefix that matches its lifecycle, not invent a new one. If no existing prefix fits, that's a signal the script doesn't belong in the standard pipeline. +A new script should pick the prefix that matches its lifecycle, not invent a new one. If no existing prefix fits, that's a signal the script doesn't belong in the standard pipeline. When it genuinely does belong, a new prefix is allowed — but it enters both lists in this file in the same commit as its first member, otherwise the rule "reuse an existing prefix" silently develops an exception (`use:` is tracked as exactly that in `backlog.tasks`). -Separately, some top-level scripts are **bare** (no prefix): the entry points that either run a single tool (`build`, `clean`) or aggregate a `prefix:*` family (`check`, `fix`, `test`, `watch`, `maintain`), plus conveniences that compose across tiers: `verify` — composing `check` + `test:unit` into one whole-project correctness gate (it deliberately uses `test:unit` rather than `test` because `check` already runs `check:tsc`, so the type checker runs exactly once); and the two scripts that bracket a unit of work and own its git state — `branch` — assert the precondition the branching model assumes, then create the branch (see [Branching model](#branching-model)); and `release` — the maintainer-only release front-door (see [Publishing workflow](#publishing-workflow)). Bare commands are how you invoke a tier; the `prefix:*` scripts are what those tiers are made of. +Separately, some top-level scripts are **bare** (no prefix): the entry points that either run a single tool (`build`, `clean`) or aggregate a `prefix:*` family (`check`, `fix`, `test`, `watch`, `maintain`), plus one convenience that composes across tiers: `verify` — composing `check` + `test:unit` into one whole-project correctness gate (it deliberately uses `test:unit` rather than `test` because `check` already runs `check:tsc`, so the type checker runs exactly once). Bare commands are how you invoke a tier; the `prefix:*` scripts are what those tiers are made of. ## Feedback tiers @@ -78,7 +79,7 @@ This is why every test in the suite pairs an `expectTypeOf(...)` with an `assert - **Base branch:** `main` - **Branch naming:** `feature/` / `fix/` / `chore/` -- **Starting work:** `npm run branch -- /`. It refuses, without changing anything, unless the working tree is clean (untracked files included), no merge/rebase/cherry-pick is in progress, `main` matches its upstream, and `npm run test` is green on `main` — so a later failure is always attributable to your edits. The prefix is still _your_ call, inferred from the task; the script validates it rather than guessing it. +- **Starting work:** `npm run create:branch -- /`. It refuses, without changing anything, unless the working tree is clean (untracked files included), no merge/rebase/cherry-pick is in progress, `main` matches its upstream, and `npm run test` is green on `main` — so a later failure is always attributable to your edits. The prefix is still _your_ call, inferred from the task; the script validates it rather than guessing it. - **Merging:** `git checkout main && git merge --no-ff ` (local PR — review the diff yourself before closing the branch). - CI runs `npm run check` + `npm run test:ci` on every push to `main` — this is the authoritative gate. - **Releases are NOT triggered by pushes.** Only the maintainer triggers a release (see [Publishing workflow](#publishing-workflow)). @@ -88,6 +89,6 @@ This is why every test in the suite pairs an `expectTypeOf(...)` with an `assert Publishing is CI-only by policy. Local `npm publish` is not supported. The maintainer triggers releases from `main`: 1. All intended changes are merged to `main` and passing CI. -2. The maintainer runs `npm run release` — an interactive prompt suggests a version (based on the latest CHANGELOG entry); the maintainer confirms or edits it. +2. The maintainer runs `npm run create:release` — an interactive prompt suggests a version (based on the latest CHANGELOG entry); the maintainer confirms or edits it. 3. `scripts/release.sh` creates a single release commit (changelog + package.json bump, amended into one commit), tags it, and pushes everything to Gitea. 4. CI runs on the push: `npm run check` + `npm run test:ci` on the commit; then the `publish` job fires on the tag: `build` → `publish:publint` → `publish:attw` → `npm publish --access public`. The publish-tier checks must pass before the artifact is published. diff --git a/package.json b/package.json index 27d3a3f..a9737e6 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,8 @@ "fix": "npm run fix:oxlint && npm run fix:oxfmt", "fix:oxfmt": "oxfmt ${LEFTHOOK_FILES:-.}", "fix:oxlint": "oxlint --fix src", - "branch": "./scripts/branch.sh", - "release": "./scripts/release.sh", + "create:branch": "./scripts/branch.sh", + "create:release": "./scripts/release.sh", "maintain": "npm run maintain:knip; npm run maintain:outdated", "maintain:knip": "knip --include dependencies,exports,files", "maintain:outdated": "check-outdated --ignore-pre-releases", diff --git a/scripts/branch.sh b/scripts/branch.sh index 572812a..0b01696 100755 --- a/scripts/branch.sh +++ b/scripts/branch.sh @@ -2,7 +2,7 @@ set -eu -# Branch front-door. Run as `npm run branch -- /`. +# Branch front-door. Run as `npm run create:branch -- /`. # # How we got here (short): the branching model says every change starts from a # clean, current `main`, and the type-driven loop only produces trustworthy @@ -12,17 +12,30 @@ set -eu # only appears if the assertions passed. Cheap checks run first, `npm run test` # runs last: the expensive gate is not paid on a tree that was never eligible. # -# Rejected: a new `flow:` / `git:` prefix (CONTRIBUTING.md § Script prefix -# convention forbids inventing one, and `use:git-commit-message` shows what -# happens when a prefix is added without amending the vocabulary that declares -# them); `check:*` (read-only and aggregated by `check`, so CI would run a -# command that mutates repo state); `fix:*` (its review surface is a file diff, -# not a branch); `maintain:*` (advisory, never a gate — the inverse of this); -# `start` (npm reserves it for running the package); a full git-flow CLI wrapping -# the merge too (merging ends in "review the diff yourself", which is judgment, -# and only the start half carries a verification burden); and reusing `pubv`'s -# preflight (it is release-shaped, third-party, and would make branch start pay -# a build + pack it has no use for). +# Rejected for the prefix name: `run:` / `perform:` (both mean only "do the +# thing named after them", so every script in the repo would fit under them and +# the taxonomy collapses); `git:` (names the tool, not the lifecycle moment, and +# advertises passthrough aliases); `start:` (describes this half, not the +# release); `cut:` (idiomatic for both, but it needs VCS slang to decode, and a +# signpost that has to be explained is not one); `flow:` (overloaded in a library +# about type-level matching); and the existing families — `check:*` is read-only +# and aggregated by `check`, so CI would run a command that mutates repo state; +# `fix:*`'s review surface is a file diff, not a branch; `maintain:*` is advisory +# and explicitly never a gate. +# +# `create:` was kept because both members really do create something: a branch, +# a release. It was added to both prefix lists in CONTRIBUTING.md in the same +# commit as its first members, because a prefix missing from those lists is +# invisible — which is the `use:` mistake this repo now carries in backlog.tasks. +# There is deliberately no bare `create` aggregator: "run all the workflows" +# describes nothing anyone wants, and `publish:*` already sets the precedent for +# a prefix without one. +# +# Also rejected: a full git-flow CLI wrapping the merge too (merging ends in +# "review the diff yourself", which is judgment, and only the start half carries +# a verification burden); and reusing `pubv`'s preflight (release-shaped, +# third-party, and it would make branch start pay a build + pack it has no use +# for). # # Every refusal is non-mutating except the baseline test, which runs on `main` # after we switch there — so a red `main` restores the branch you started on @@ -33,7 +46,7 @@ PREFIXES="feature fix chore" NAME="${1:-}" if [ -z "${NAME}" ]; then - echo "usage: npm run branch -- / (prefix: ${PREFIXES})" >&2 + echo "usage: npm run create:branch -- / (prefix: ${PREFIXES})" >&2 exit 2 fi diff --git a/scripts/release.sh b/scripts/release.sh index 7c86f66..f743e7e 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -2,7 +2,7 @@ set -eu -# Release front-door. Run as `npm run release`. +# Release front-door. Run as `npm run create:release`. # # How we got here (short): we want hand-written Keep-a-Changelog notes, an # [Unreleased] -> "## [x.y.z] - DATE" graduation, and a tag that marks the exact