You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The release tooling partitions commits into release trains by Conventional Commit scope: tools/trains.sh maps each scope to a train (lib, cli, dum), and tools/packaging/release-notes.sh + tools/changelog/collect-prs.sh build each train's GitHub Release notes and changelog from the commits whose scope belongs to it. A commit with no scope matches no train and is dropped ([ -z "$scope_group" ] && continue). That drop is deliberate for infrastructure (ci:, chore:, build: …).
But commit-lint currently makes the scope optional for every type (the (scope) group is ? in tools/commit-lint/lint-commit-message.sh). So a contributor can write a genuinely user-facing feat: or fix:without a scope — and it is then silently dropped from both the release notes and the changelog. The change ships in the binaries but never appears in the release record.
#231 fixed the documentation of this (CONTRIBUTING now states the rule). This issue is about enforcing it mechanically, so it can't be forgotten.
Impact
A user-facing feature or bug fix can silently disappear from the public release record on a real release. It is invisible — no error, no warning — and only surfaces later as a gap a user notices. Documentation mitigates but relies on the author remembering; the linter can make the mistake impossible.
Direction
Make commit-lint (the commit-msg hook + the CI check, single source lint-commit-message.sh) require the scope group for the two version-driving types, feat and fix, and keep it optional for the non-version-driving types (build, chore, ci, docs, perf, refactor, revert, style, test) — the legitimate unscoped/infrastructure case. Reject an unscoped feat:/fix: at write time and in CI, with a message that names the consequence ("…or it won't appear in any release train").
Decisions for the maintainer (this is a behaviour change to an enforced contract, not a silent tweak):
Is there any legitimate unscoped feat:/fix:? (A user-facing change to the FirstClassErrors.Usage samples, or a genuinely cross-cutting change?) If yes, decide how it is expressed — a dedicated scope, or an explicit escape hatch.
Applies from adoption on; prior history is not rewritten (consistent with CONTRIBUTING → "Adoption").
Proposed ADR
This changes an enforced, cross-cutting contract, so per CLAUDE.md / AGENTS.md it warrants an ADR — drafted Proposed, and decided by the maintainer (I propose, I never accept). Sketch of the decision it would record:
The commit scope is a load-bearing part of the release contract for version-driving types.feat:/fix: MUST carry a scope, because the release tooling partitions by scope and drops the unscoped; the scope is therefore not "readability only" (a notion already corrected in the docs by #231) but a required routing key.
The ADR file itself belongs in the implementing PR (drafted Proposed, indexed, linked), not here.
Acceptance criteria
commit-lint rejects a feat:/fix: with no scope, identically at the commit-msg hook and in CI, with a clear message naming the release-record consequence.
The non-version-driving types remain scope-optional.
The new rejection is covered by a test case if commit-lint has a test harness.
An ADR is drafted Proposed and linked from the implementing PR.
Context
Follow-up to #231 (which fixed the documentation drift; docs PR #292). The underlying footgun was surfaced by the 2026-07-20 architecture, design & ecosystem audit. Relevant files: tools/trains.sh, tools/packaging/release-notes.sh, tools/changelog/collect-prs.sh (routing + the unscoped drop), and tools/commit-lint/lint-commit-message.sh (the optional-scope regex).
Problem
The release tooling partitions commits into release trains by Conventional Commit scope:
tools/trains.shmaps each scope to a train (lib,cli,dum), andtools/packaging/release-notes.sh+tools/changelog/collect-prs.shbuild each train's GitHub Release notes and changelog from the commits whose scope belongs to it. A commit with no scope matches no train and is dropped ([ -z "$scope_group" ] && continue). That drop is deliberate for infrastructure (ci:,chore:,build:…).But
commit-lintcurrently makes the scope optional for every type (the(scope)group is?intools/commit-lint/lint-commit-message.sh). So a contributor can write a genuinely user-facingfeat:orfix:without a scope — and it is then silently dropped from both the release notes and the changelog. The change ships in the binaries but never appears in the release record.#231 fixed the documentation of this (CONTRIBUTING now states the rule). This issue is about enforcing it mechanically, so it can't be forgotten.
Impact
A user-facing feature or bug fix can silently disappear from the public release record on a real release. It is invisible — no error, no warning — and only surfaces later as a gap a user notices. Documentation mitigates but relies on the author remembering; the linter can make the mistake impossible.
Direction
Make
commit-lint(thecommit-msghook + the CI check, single sourcelint-commit-message.sh) require the scope group for the two version-driving types,featandfix, and keep it optional for the non-version-driving types (build,chore,ci,docs,perf,refactor,revert,style,test) — the legitimate unscoped/infrastructure case. Reject an unscopedfeat:/fix:at write time and in CI, with a message that names the consequence ("…or it won't appear in any release train").Decisions for the maintainer (this is a behaviour change to an enforced contract, not a silent tweak):
feat:/fix:? (A user-facing change to theFirstClassErrors.Usagesamples, or a genuinely cross-cutting change?) If yes, decide how it is expressed — a dedicated scope, or an explicit escape hatch.Proposed ADR
This changes an enforced, cross-cutting contract, so per CLAUDE.md / AGENTS.md it warrants an ADR — drafted
Proposed, and decided by the maintainer (I propose, I never accept). Sketch of the decision it would record:The ADR file itself belongs in the implementing PR (drafted
Proposed, indexed, linked), not here.Acceptance criteria
commit-lintrejects afeat:/fix:with no scope, identically at thecommit-msghook and in CI, with a clear message naming the release-record consequence.commit-linthas a test harness.Proposedand linked from the implementing PR.Context
Follow-up to #231 (which fixed the documentation drift; docs PR #292). The underlying footgun was surfaced by the 2026-07-20 architecture, design & ecosystem audit. Relevant files:
tools/trains.sh,tools/packaging/release-notes.sh,tools/changelog/collect-prs.sh(routing + the unscoped drop), andtools/commit-lint/lint-commit-message.sh(the optional-scope regex).Filed as a follow-up to #231 by Claude Code.