♻️ Single-home the actionable rules and the rationale
development/ had restated the actionables that CONTRIBUTING.md owns: the branching step list, the script prefix list, the feedback-tier rule of thumb, the commit convention and the type-driven test loop. Those now live only in CONTRIBUTING.md; development/ keeps the decision blocks and links to the rule. development/README.md, CONTRIBUTING.md and AGENTS.md state the 'write each fact once' principle explicitly.
This commit is contained in:
1 parent
fe02317fc8
commit
95d73d11b6
5 files changed
+89
-126
No files matched your search
@@ -10,6 +10,9 @@ rules: read the relevant file here before changing an area, and update it when
|
||||
a decision changes, rather than leaving an obsolete reason behind. A rule and
|
||||
its reason drift apart when they live in one place and are maintained in two;
|
||||
keeping the rule in CONTRIBUTING.md and the reason here keeps each single-homed.
|
||||
**Each fact is written once**: the actionable rule in CONTRIBUTING.md, the
|
||||
reason here. Neither restates the other — when the same fact would be useful in
|
||||
both, one links to the other instead of copying it.
|
||||
|
||||
User-facing documentation lives in [README.md](../README.md). A `docs/` folder
|
||||
is deliberately not used yet: the convention is that `docs/` is the future
|
||||
|
||||
+10
-31
@@ -8,26 +8,9 @@ the way it is.
|
||||
|
||||
## Type-driven development
|
||||
|
||||
New behavior follows **type-driven development** (in Edwin Brady's sense):
|
||||
_treat the type as the plan for a program, and use the compiler and type checker
|
||||
as your assistant, guiding you to a complete program that satisfies the type_
|
||||
([idris-lang.org](https://www.idris-lang.org/)). Here that plan is the
|
||||
`expectTypeOf` assertion, written first. The loop is **type → red → green →
|
||||
refactor**:
|
||||
|
||||
1. **Type** — write the compile-time expectation first
|
||||
(`expectTypeOf(...).toEqualTypeOf<…>()`) and let `npm run check:tsc` fail on
|
||||
the _type_. The type error is the spec you want to hit before the runtime
|
||||
logic exists.
|
||||
2. **Red** — add the matching runtime assertion (`assert.*`) so
|
||||
`npm run test:unit` now fails on behavior.
|
||||
3. **Green** — implement in `src/*.ts` until both the type check and the test
|
||||
pass.
|
||||
4. **Refactor** — with the type system and the tests as the safety net, then
|
||||
`npm run verify` as the definition-of-done gate.
|
||||
|
||||
This is why every test in the suite pairs an `expectTypeOf(...)` with an
|
||||
`assert.*` — keep them together.
|
||||
The rules — the loop and the pairing rule — are in
|
||||
[CONTRIBUTING.md § Testing discipline (type-driven)](../CONTRIBUTING.md#testing-discipline-type-driven).
|
||||
What follows is why the loop is type-driven and what was rejected.
|
||||
|
||||
#### Decision (2026-09)
|
||||
|
||||
@@ -56,18 +39,14 @@ Per [AGENTS.md § Never do](../AGENTS.md#never-do), reach green honestly — fix
|
||||
types so both the type check and the runtime assertion pass, never suppress the
|
||||
ones you can't make pass.
|
||||
|
||||
## Test tiers
|
||||
The runner is `node --test --strip-types "src/**/*.test.ts"` and the tiers are
|
||||
listed in
|
||||
[CONTRIBUTING.md § Development commands](../CONTRIBUTING.md#development-commands).
|
||||
`c8` uses V8 coverage, so the `--strip-types` source is instrumented without a
|
||||
build step. The runner relies on the `.ts` import-extension convention (see
|
||||
[tooling.md](./tooling.md#source-imports-use-ts-extensions)).
|
||||
|
||||
- `npm run test:unit` — the test runner alone, for the fast local loop.
|
||||
- `npm test` — `check:tsc` + the unit suite; this is what pre-push and the
|
||||
baseline check run.
|
||||
- `npm run test:ci` — adds c8 coverage; used by CI. `c8` uses V8 coverage, so
|
||||
the `--strip-types` source is instrumented without a build step.
|
||||
|
||||
The runner is `node --test --strip-types "src/**/*.test.ts"`. It relies on the
|
||||
`.ts` import-extension convention (see [tooling.md](./tooling.md#source-imports-use-ts-extensions)).
|
||||
|
||||
#### Known issue
|
||||
## Known issues
|
||||
|
||||
- The type-aware linter misidentifies `expectTypeOf()` as a floating promise,
|
||||
so `src/index.test.ts` carries a file-level `oxlint-disable
|
||||
|
||||
+19
-79
@@ -13,28 +13,9 @@ 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.
|
||||
|
||||
- **Base branch:** `main`
|
||||
- **Branch naming:** `feature/<desc>` / `fix/<desc>` / `chore/<desc>`
|
||||
- **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:** `npm run create:finish` (on the branch). It asserts the same
|
||||
clean-tree / no-operation / current-`main` preconditions, fast-forwards a stale
|
||||
`main` (a true divergence is refused), merges the branch `--no-ff`, runs
|
||||
`npm run verify`, and deletes the branch only after the merge is green. The
|
||||
push is deliberately left to `create:release`, so the merge stays local and
|
||||
reviewable — read the diff yourself before finishing.
|
||||
- CI runs `npm run check` + `npm run test:ci` on every push to `main` — this is
|
||||
the authoritative gate. The one exception: a push headed by a release commit
|
||||
(`:rocket: Release x.y.z`) skips the full `build`/`maintain` jobs, because
|
||||
`create:release` pushes the tag for that exact commit right after and the tag
|
||||
run is the authoritative one (see `release-gate` in
|
||||
[.gitea/workflows/ci.yml](../.gitea/workflows/ci.yml)).
|
||||
- **Releases are NOT triggered by pushes.** Only the maintainer triggers a
|
||||
release (see [publishing.md](./publishing.md)).
|
||||
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)
|
||||
|
||||
@@ -79,51 +60,18 @@ prose plus hand-written `git` commands.
|
||||
|
||||
## Script prefix convention
|
||||
|
||||
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:
|
||||
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.
|
||||
|
||||
- `create:*` — front doors of the repo's own workflow; these mutate git state
|
||||
rather than the source. `create:branch` opens a unit of work, `create:finish`
|
||||
closes the branch half, `create:release` closes the release half
|
||||
(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.
|
||||
- `watch:*` — long-running watchers for the manual inner dev loop. Aggregated by
|
||||
`watch`; currently a single child (`watch:test`), and a future
|
||||
`watch:oxlint` / `watch:tsc` would run concurrently under that umbrella.
|
||||
- `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.
|
||||
- `setup:*` — one-time configuration of a fresh clone; mutates the local
|
||||
environment (git config, editor settings) rather than the repo source, so it
|
||||
is never part of a hook or CI step. Aggregated by `npm run setup` (the
|
||||
umbrella), run once after cloning.
|
||||
|
||||
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 [CONTRIBUTING.md](../CONTRIBUTING.md) in the same
|
||||
commit as its first member, otherwise the rule "reuse an existing prefix"
|
||||
silently develops an exception.
|
||||
|
||||
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`, `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.
|
||||
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)
|
||||
|
||||
@@ -155,11 +103,9 @@ aggregator.
|
||||
|
||||
## Feedback tiers
|
||||
|
||||
The tools are organized into a feedback ladder. Each tier catches different
|
||||
things at different costs; the rule of thumb is "earlier tiers fire more often,
|
||||
faster tiers catch less, slower tiers are more thorough". The tier table itself
|
||||
lives in [CONTRIBUTING.md](../CONTRIBUTING.md#feedback-tiers); this section
|
||||
explains why the split is where it is.
|
||||
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)
|
||||
|
||||
@@ -193,16 +139,10 @@ into pre-push and `verify`; and slow or network-bound scans into `maintain`.
|
||||
tier; `verify` is run by hand because the push is where the whole project is
|
||||
already checked.
|
||||
|
||||
Before pushing, run `npm run verify` — the one-shot correctness gate. Run
|
||||
`npm run maintain` only on a maintenance / update-deps branch.
|
||||
|
||||
## Commit messages
|
||||
|
||||
Gitmoji subject, imperative mood, 50/72 wrapping. The template is
|
||||
[commit-message-template](../commit-message-template); run
|
||||
`npm run setup:git-commit-message` once after cloning to register it as git's
|
||||
`commit.template` (or `npm run setup` to run every one-time clone step).
|
||||
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user