# Workflow 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 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`, not by prose plus hand-written `git`. #### Why - 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`: 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` 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. ## Changelog notes The rule is in [CONTRIBUTING.md § Rules the tools don't enforce](../CONTRIBUTING.md#rules-the-tools-dont-enforce). #### Decision (2026-09) A merged branch carries its own summary under `[Unreleased]` in [CHANGELOG.md](../CHANGELOG.md), added before `create:finish`; `create:release` graduates it into the tagged section (see [publishing.md](./publishing.md)). #### Why - `create:release` derives the bump heuristic from the `[Unreleased]` body, so the notes must exist before release day. - The contributor has fresh context; at release day the intent of a branch is only its diff. - Gitmoji subjects are signposts, not semantic keys, so notes cannot be derived from the history. #### Rejected - Generating notes from subjects at release time: subjects carry no parseable type/scope (see § Commit messages). - The maintainer writing one summary during `create:release`: reconstruction after the fact. - Enforcing it in `create:finish`: the front doors assert git state, not content — and _notable_ is exactly the judgment a tool cannot make. ## Script prefix convention The prefix taxonomy is the rule, and it lives in [CONTRIBUTING.md § Script prefix convention](../CONTRIBUTING.md#script-prefix-convention). 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, with no bare `create` aggregator. #### Why - 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:`: 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 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 table and invocation rules are in [CONTRIBUTING.md § Feedback tiers](../CONTRIBUTING.md#feedback-tiers); this section explains the split. #### Decision (2026-09) 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:*` 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 - `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: `: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, imperative mood, wrapped 50/72, not Conventional Commits. #### Why - 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 has no automation value here (see [publishing.md](./publishing.md)).