# 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. ## Branching model **GitHub Flow (single-developer).** Every change — feature, fix, refactor — branches off `main` and is merged back via a local commit. There is no pull request workflow on Gitea yet: collaborative review through the Gitea UI is not in place, so Gitea is the lab. When something is tested and ready for production it will be promoted to GitHub. The contributor-facing steps are in [CONTRIBUTING.md § Branching model](../CONTRIBUTING.md#branching-model); what follows is why the front doors exist and what was rejected. #### Decision (2026-09) Work is opened and closed by `create:branch` / `create:finish` rather than by prose plus hand-written `git` commands. #### 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`. #### 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`. #### 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. ## 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. #### Decision (2026-09) `create:` is the prefix for workflow front doors, and there is 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:`). - `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 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. ## Feedback tiers The tier table and the rules for invoking it are in [CONTRIBUTING.md § Feedback tiers](../CONTRIBUTING.md#feedback-tiers); this section explains why the split is where it is. #### 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`. #### 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. #### 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. ## 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`, `: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 #...`. #### Decision (2026-09) Gitmoji subjects with imperative mood, wrapped 50/72, rather than 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. #### 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 [publishing.md](./publishing.md)).