♻️ Group workflow scripts under a create: prefix
`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`.
This commit is contained in:
1 parent
8c2855aa92
commit
844e80d650
5 files changed
+37
-23
No files matched your search
@@ -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 -- <prefix>/<desc>`, 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 -- <prefix>/<desc>`, 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 — <branch>
|
||||
|
||||
+7
-6
@@ -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 `<prefix>:<name>` script is implicitly aggregated by a `<prefix>` 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/<desc>` / `fix/<desc>` / `chore/<desc>`
|
||||
- **Starting work:** `npm run branch -- <prefix>/<desc>`. 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 -- <prefix>/<desc>`. 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 <branch>` (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.
|
||||
+2
-2
@@ -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",
|
||||
|
||||
+26
-13
@@ -2,7 +2,7 @@
|
||||
|
||||
set -eu
|
||||
|
||||
# Branch front-door. Run as `npm run branch -- <prefix>/<desc>`.
|
||||
# Branch front-door. Run as `npm run create:branch -- <prefix>/<desc>`.
|
||||
#
|
||||
# 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>/<desc> (prefix: ${PREFIXES})" >&2
|
||||
echo "usage: npm run create:branch -- <prefix>/<desc> (prefix: ${PREFIXES})" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in new issue
Block a user