# 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 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. #### 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)).