♻️ Tighten the development/ prose

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.
This commit is contained in:
tmu committed 2026-09-15 13:56:49 +00:00
1 parent 7ea84b66fa
commit 74c39e1346
7 files changed
+341 -409

No files matched your search

+77 -98
View File
@@ -1,164 +1,143 @@
# Workflow
How work moves through the repository: the branching model, the `npm run`
script taxonomy, the feedback tiers, and commit messages. The contributor-facing
steps are in [CONTRIBUTING.md](../CONTRIBUTING.md); this file records why they
are shaped the way they are.
How work moves through the repository. The rules are in
[CONTRIBUTING.md](../CONTRIBUTING.md); this file records why they are shaped the
way they are.
## Branching model
The contributor-facing steps are in
[CONTRIBUTING.md § Branching model](../CONTRIBUTING.md#branching-model). Two
bits of context behind the model: collaborative review through the Gitea UI is
not in place, so Gitea is the lab; and the project will be promoted to GitHub
once it is tested and ready for production. What follows is why the front doors
exist and what was rejected.
The model is GitHub Flow (single-developer); the steps are in
[CONTRIBUTING.md § Branching model](../CONTRIBUTING.md#branching-model). Context
behind it: Gitea has no collaborative review UI in use, so it is the lab, and
the project moves to GitHub once it is tested and ready.
#### Decision (2026-09)
Work is opened and closed by `create:branch` / `create:finish` rather than by
prose plus hand-written `git` commands.
Work is opened and closed by `create:branch` / `create:finish`, not by prose
plus hand-written `git`.
#### Why
- The branching model's preconditions were prose. Prose rots silently — a rule
nobody checks is a suggestion. A script asserts, then acts, and the branch or
merge only happens if the assertions passed.
- The type-driven loop only produces trustworthy results if the baseline was
green before the first edit. Cheap checks run first and `npm run test` last, so
the expensive gate is not paid on a tree that was never eligible.
- `create:finish` owns the merge-side preconditions and the post-merge
`npm run verify`, so a merge cannot land unverified. The push stays with
`create:release` so the merge remains local and reviewable until then.
- Every check failure is non-mutating, except the baseline test which runs on
`main` after switching there — a red `main` restores the branch you started on
rather than stranding you on it. A merge conflict aborts and returns to the
feature branch, so a failed finish never leaves a half-merged `main`.
- The preconditions were prose, and prose rots: a rule nobody checks is a
suggestion. A script asserts, then acts, so the branch or merge only exists if
the assertions passed.
- Type-driven work is only trustworthy if the baseline was green before the
first edit. Cheap checks run first and `npm run test` last, so the expensive
gate is not paid on an ineligible tree.
- The merge half owns the post-merge `npm run verify`, so a merge cannot land
unverified. The push stays with `create:release` so the merge is reviewed
locally first.
- Every failure is non-mutating except the baseline test, which runs on `main`
after switching there: a red `main` restores the branch you started on, and a
merge conflict aborts back to the feature branch rather than stranding a
half-merged `main`.
#### Rejected
- Hand-written `git switch -c` / `git merge`: same rules, no enforcement.
- Reusing `pubv`'s preflight for `create:branch`: it is release-shaped and
third-party, and would make starting a branch pay for a build and pack it has
no use for.
- Leaving the merge as reviewer judgment: that judgment still exists, but it now
happens when the maintainer reviews the handover _before_ invoking
`create:finish`, rather than being encoded in a command that anyone can run
from a dirty tree.
- `--no-ff` rather than a fast-forward: it keeps each unit of work visible in
`git log`.
- Reusing `pubv`'s preflight for `create:branch`: release-shaped, third-party,
and it would pay for a build and pack a new branch has no use for.
- Leaving the merge to reviewer judgment: that judgment moved earlier, to the
handover review before `create:finish`, rather than living in a command anyone
can run from a dirty tree.
- Fast-forward instead of `--no-ff`: `--no-ff` keeps each unit of work visible
in `git log`.
#### Known issue
- `create:finish` deliberately does not push, so `main` is ahead of
`origin/main` between a merge and the next push or release. `create:branch`
requires `main` to match its upstream and will refuse until it is pushed. Push
`main` before starting the next branch.
- `create:finish` does not push, so `main` is ahead of `origin/main` between a
merge and the next push. `create:branch` requires `main` to match its upstream
and refuses until it is pushed; push `main` before starting the next branch.
## Script prefix convention
The prefix taxonomy is the rule, and it lives in
[CONTRIBUTING.md § Script prefix convention](../CONTRIBUTING.md#script-prefix-convention).
What follows is the design rationale and the `create:` decision.
One part of the taxonomy is not a prefix rule: 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`, `setup`), 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.
The design behind it: bare scripts are the tier entry points — a single tool
(`build`, `clean`) or an aggregator of a `prefix:*` family (`check`, `fix`,
`test`, `watch`, `maintain`, `setup`) — while `verify` composes `check` +
`test:unit` into the whole-project gate (it uses `test:unit`, not `test`,
because `check` already runs `check:tsc`).
#### Decision (2026-09)
`create:` is the prefix for workflow front doors, and there is no bare `create`
`create:` is the prefix for workflow front doors, with no bare `create`
aggregator.
#### Why
- Both members really do create something: a branch, a release.
- The prefix was added to both lists in [CONTRIBUTING.md](../CONTRIBUTING.md) in
the same commit as its first members, because a prefix missing from those
lists is invisible — which was the `use:` mistake this repo previously
carried (since retired into `setup:`).
- Both members create something real: a branch, a release.
- It joined both lists in [CONTRIBUTING.md](../CONTRIBUTING.md) alongside its
first members, so it could not go invisible the way the retired `use:` prefix
did.
- `publish:*` already set the precedent for a prefix without an aggregator.
#### Rejected
- `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
- `run:` / `perform:`: they mean only "do the named thing", so every script fits
and the taxonomy collapses.
- `git:`: names the tool, not the lifecycle moment, and implies passthrough
aliases.
- `start:` — describes the branch 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.
- 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.
- `start:`: describes the branch half, not the release.
- `cut:`: idiomatic but needs VCS slang to decode.
- `flow:`: overloaded in a type-level matching library.
- The existing families: `check:*` is read-only (CI would run a state-mutating
command), `fix:*` reviews as a diff not a branch, `maintain:*` is advisory and
never a gate.
## Feedback tiers
The tier table and the rules for invoking it are in
The table and invocation rules are in
[CONTRIBUTING.md § Feedback tiers](../CONTRIBUTING.md#feedback-tiers); this
section explains why the split is where it is.
section explains the split.
#### Decision (2026-09)
Split fast, offline, staged-file checks into pre-commit; whole-project test runs
into pre-push and `verify`; and slow or network-bound scans into `maintain`.
Fast, offline, staged-file checks sit in pre-commit; whole-project test runs in
pre-push and `verify`; slow or network-bound scans under `maintain`.
#### Why
- **`watch:*` is a manual tier, not a hook.** The developer starts it on demand
(it has to be killed with Ctrl-C) and it runs in a dedicated terminal pane. It
sits as the earliest tier in the ladder, catching failures the moment a file is
saved — before staging, before commit.
- **`check:tsc`, `check:oxlint`, `check:oxfmt`, `check:cspell`** are in
pre-commit because they are fast (~0.2–0.5s each), fully offline, and naturally
scope to staged files via the `LEFTHOOK_FILES` env var convention. They give
instant feedback on what you typed.
- **`test` (and the `tsc` it includes) is in pre-push** because it runs the whole
test suite across the whole project. The pre-commit `LEFTHOOK_FILES` convention
doesn't apply to the test runner, so pre-commit isn't the right home. Pre-push
runs after all commits are made but before the push leaves the machine,
catching regressions that span multiple commits.
- `watch:*` runs until killed, in its own pane, so it is the earliest tier,
firing on save before staging or commit.
- `check:tsc` / `check:oxlint` / `check:oxfmt` / `check:cspell` are fast
(~0.2–0.5s each), offline, and scope to staged files via `LEFTHOOK_FILES`, so
pre-commit gives instant feedback on what you typed.
- `test` (and its `tsc`) runs the whole suite over the whole project, and the
staged-file convention does not apply to the test runner, so it belongs in
pre-push, after the commits exist but before the push leaves the machine.
#### Rejected
- Running `maintain:*` in `check` or pre-commit: advisory, whole-project and
network-bound scans are not correctness gates and would make the fast tier
slow.
- Treating a green pre-commit as the definition of done: it only sees staged
files, which is why `npm run verify` exists as the one-shot whole-project gate.
- A separate `git push` hook for `verify`: it would duplicate the pre-push test
tier; `verify` is run by hand because the push is where the whole project is
already checked.
- `maintain:*` in `check` or pre-commit: advisory, whole-project and
network-bound scans are not correctness gates and would slow the fast tier.
- Treating a green pre-commit as the definition of done: it sees only staged
files, hence `npm run verify`.
- A separate `git push` hook for `verify`: the pre-push test tier already covers
it.
## Commit messages
The convention is in
[CONTRIBUTING.md § Commit messages](../CONTRIBUTING.md#commit-messages).
Examples from history: `:sparkles: Add watch tier with watch:test child`,
Examples: `:sparkles: Add watch tier with watch:test child`,
`:recycle: Move type-aware config to .oxlintrc.json; use source-level disable
directives`, `:memo: Restore unique maintainer content as CONTRIBUTING.md`. The
body explains _what and why_, not _how_; link issues with `Resolves #...`.
body explains what and why, not how; link issues with `Resolves #...`.
#### Decision (2026-09)
Gitmoji subjects with imperative mood, wrapped 50/72, rather than Conventional
Commits.
Gitmoji subjects, imperative mood, wrapped 50/72, not Conventional Commits.
#### Why
- The history is gitmoji, not Conventional, and it predates any commit-lint
tooling; switching would rewrite the convention for no gain.
- The body carries reasoning, which is what a reviewer needs; the subject is a
signpost, not a semantic key.
- The history is gitmoji and predates any commit-lint tooling; switching would
rewrite the convention for no gain.
- The body carries the reasoning a reviewer needs; the subject is a signpost,
not a semantic key.
#### Rejected
- Conventional Commits: the release flow uses hand-written Keep-a-Changelog
notes, not generated ones, so the prefix carries no automation value here (see
notes, not generated ones, so the prefix has no automation value here (see
[publishing.md](./publishing.md)).