Skip to content

Commit 512096a

Browse files
V48 (spec-impl): Codify 50/72 commit message law
Document subject soft ≤50, blank line, and body lines hard ≤72 in SPECIFYING §2.8, AGENTS, CONTRIBUTING, FAMILIARIZATION, and the operator skill. Enforce via scripts/check-bitcode-commit-msg.mjs (commit-msg hook).
1 parent 1ddcf28 commit 512096a

6 files changed

Lines changed: 153 additions & 19 deletions

File tree

.docs/AGENTS.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@
1313
- `(impl-only)` — Impl only (code, tests, tooling, scripts)
1414
- `(spec-impl)` — Spec + Impl in lockstep
1515
- **Never** write `(specification-only)`, `(implementation-only)`, or `(specification-implementation)` in a subject
16-
Examples: `V48 Gate 3 (impl-only): Rename deposit input to Obfuscations` or `V48 Gate 3 (spec-only): Record SDIVF in V48 notes`. A change that touches both kinds is one `(spec-impl)` commit or a split into `(spec-only)` + `(impl-only)` — never an unlabeled mix. This keeps Complete Implementation Derivability (`.specifications/BITCODE_SPECIFYING.md` §2.8) auditable commit by commit.
17-
- **Commit subject length:** keep the first line under **72 characters** (git default soft limit). Prefer a short imperative subject; put proof commands, file lists, and rationale in the **commit body** when needed. Do not stuff the subject with long parentheticals or run-on clauses.
16+
Examples: `V48 Gate 3 (impl-only): Rename Obfuscations` or `V48 Gate 3 (spec-only): Record SDIVF in notes`. A change that touches both kinds is one `(spec-impl)` commit or a split into `(spec-only)` + `(impl-only)` — never an unlabeled mix. This keeps Complete Implementation Derivability (`.specifications/BITCODE_SPECIFYING.md` §2.8) auditable commit by commit.
17+
- **Commit message shape (50/72 law)** — required for every commit and gate PR subject/body (see `.specifications/BITCODE_SPECIFYING.md` §2.8):
18+
- **Subject (first line) ≤ 50 characters** (soft) — keep the summary readable in compact logs
19+
- **Blank line** after the subject whenever a body follows
20+
- **Body lines ≤ 72 characters** each (hard) — wrap proof commands, file lists, and rationale
21+
Prefer a short imperative subject; do not stuff the subject with long parentheticals or run-on clauses.
1822
- Any inline code comment that cites an accepted QA finding shorthand must always carry the fully-qualified tag `[VERSION]-Gate[N]-F[ID]` (e.g. `V48-Gate3-F26-B`), never a bare `F26-B`-style tag. The same fully-qualified tag must always be discoverable in the specification/QA ledger file(s) (e.g. the finding's own `### V48-Gate3-F26` heading in `.qa/BITCODE_V48_QA.md`), so a reader can go from either direction — code comment to spec entry, or spec entry to every citing code comment — with a single grep. This keeps finding-to-fix traceability intact without requiring the surrounding file or commit to already establish which version/gate is active.
1923
- Once implementation starts on a gate branch, do not stop at partial progress unless blocked by missing external input or explicit user pause. A gate branch is ready to stop only when the gate's acceptance criteria are implemented, specified, tested, documented, committed, pushed, and pull-requested for closure into the version branch.
2024
- Treat gate and promotion workflow health as part of gate closure. Gate pull requests into version branches must be green through the maintained gate-quality checks, and repository-wide canon-quality checks must remain greenable during draft work. Version pull requests into `main` must pass the version promotion workflow, which performs promotion-grade validations and commits the standalone `BITCODE_SPEC.txt` pointer change only after those validations pass.

.docs/FAMILIARIZATION.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ from the active family alone — no silent inheritance from older specs or sourc
9090

9191
### 2.2 Commit law
9292

93-
Every commit / gate PR **subject** (≤72 chars) declares exactly one of:
93+
Every commit / gate PR **subject** declares exactly one of:
9494

9595
- `(spec-only)` — Spec only
9696
- `(impl-only)` — Impl only
@@ -99,9 +99,17 @@ Every commit / gate PR **subject** (≤72 chars) declares exactly one of:
9999
Use only these abbreviated parentheticals in commit/PR subjects — never
100100
`(specification-only)`, `(implementation-only)`, or `(specification-implementation)`.
101101

102-
Example: `V48 (impl-only): Extract ReadsNeedReviewPanel`.
102+
**50/72 message shape** (SPECIFYING §2.8): subject ≤ **50** chars (soft); blank
103+
line before body; each body line ≤ **72** chars (hard).
103104

104-
Put proof commands and longer rationale in the commit body.
105+
Example:
106+
107+
```text
108+
V48 (impl-only): Extract NeedReviewPanel
109+
110+
Move review chrome into ReadsNeedReviewPanel with
111+
co-located tests.
112+
```
105113

106114
### 2.3 Branches
107115

.specifications/BITCODE_SPECIFYING.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,10 +256,21 @@ that parenthetical.
256256
| `(implementation-only)` | `(impl-only)` |
257257
| `(specification-implementation)` | `(spec-impl)` |
258258

259-
Example: `V48 Gate 3 (impl-only): Rename deposit input to Obfuscations`.
259+
Example: `V48 Gate 3 (impl-only): Rename Obfuscations`.
260260

261-
**Subject length:** keep the first line under **72 characters**. Put proof
262-
commands, file lists, and longer rationale in the commit body when needed.
261+
**Commit message shape (50/72 law)** — applies to every commit and gate PR
262+
subject/body:
263+
264+
| Rule | Limit | Nature |
265+
| --- | --- | --- |
266+
| **Subject** (first line) |**50** characters | Soft limit — keep the summary readable in compact logs |
267+
| **Blank line** | Second line empty whenever a body follows | Required separator between title and body |
268+
| **Body lines** |**72** characters each | Hard limit for every subsequent line |
269+
270+
Put proof commands, file lists, and longer rationale in the **body**, wrapped at
271+
72. Prefer a short imperative subject; do not stuff subjects with long
272+
parentheticals or run-on clauses. Category labels (`(spec-only)` / `(impl-only)` /
273+
`(spec-impl)`) still apply on the subject.
263274

264275
A change that alters both kinds is committed as a single `(spec-impl)` commit,
265276
or split into a `(spec-only)` and an `(impl-only)` commit — never an unlabeled

CONTRIBUTING.md

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,31 +88,47 @@ main # protected; promoted canon only
8888
**Never** push product work straight to `main`. Expect PRs + verified signatures
8989
via the **Bitcode Core Contributions** ruleset.
9090

91-
### 2.3 Commit and PR subjects
91+
### 2.3 Commit and PR messages (50/72 law)
9292

93-
Every commit / gate PR **subject** (≤72 characters) declares exactly one of:
93+
Every commit / gate PR **subject** declares exactly one of:
9494

9595
| Token | Meaning |
9696
| --- | --- |
9797
| `(spec-only)` | Spec family / notes / roadmap only |
9898
| `(impl-only)` | Code, tests, tooling, scripts only |
9999
| `(spec-impl)` | Spec + implementation in lockstep |
100100

101+
**Message shape** (`.specifications/BITCODE_SPECIFYING.md` §2.8):
102+
103+
| Part | Rule |
104+
| --- | --- |
105+
| **Subject** (line 1) |**50** characters (soft) — readable in compact logs |
106+
| **Blank line** | Required between subject and body when a body is present |
107+
| **Body lines** |**72** characters each (hard) |
108+
101109
Examples:
102110

103111
```text
104-
V48 Gate 3 (impl-only): Wire deposit cancel into pipeline host
105-
V48 Gate 4 (spec-only): Record read settle acceptance rows
106-
V48 Gate 4 (spec-impl): Close read need panel parity
112+
V48 Gate 3 (impl-only): Wire deposit cancel
113+
114+
Wire cancel into the pipeline host and add the
115+
focused host route test.
116+
```
117+
118+
```text
119+
V48 Gate 4 (spec-only): Record read settle rows
120+
V48 Gate 4 (spec-impl): Close need panel parity
107121
```
108122

109123
**Never** use expanded forms like `(specification-only)` in subjects. Put proof
110-
commands and file lists in the **body**.
124+
commands and file lists in the **body** (wrapped at 72).
111125

112-
Gate PR titles: `V48 Gate N (impl-only): Short topical title`
126+
Gate PR titles follow the same subject shape:
127+
`V48 Gate N (impl-only): Short topical title`
113128
Version promotion PRs: uppercase version + “canonical promotion” wording.
114129

115-
Full craft rules: [`.docs/AGENTS.md`](.docs/AGENTS.md).
130+
Full craft rules: [`.docs/AGENTS.md`](.docs/AGENTS.md).
131+
Enforced by `.githooks/commit-msg``scripts/check-bitcode-commit-msg.mjs`.
116132

117133
### 2.4 QA finding tags
118134

apps/claude/skills/bitcode-operator/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ Never invent or dump protected raw source into user-facing artifacts. Packs carr
3131
## When to escalate
3232

3333
- Spec vs implementation drift → open `BITCODE_SPEC_V48_*` / active draft notes
34-
- Gate work → follow Agents.md gate branch + `(spec-impl)` / `(impl-only)` / `(spec-only)` commit labels (subject ≤72 chars)
34+
- Gate work → follow Agents.md gate branch + `(spec-impl)` / `(impl-only)` / `(spec-only)` commit labels; 50/72 commit message law (subject soft ≤50, blank line, body lines hard ≤72)

scripts/check-bitcode-commit-msg.mjs

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
#!/usr/bin/env node
2+
/**
3+
* commit-msg hook body — Spec/Impl labels + 50/72 commit message law
4+
* (`.specifications/BITCODE_SPECIFYING.md` §2.8, `.docs/AGENTS.md`).
5+
*/
26

37
import { execFileSync } from 'node:child_process';
48
import { readFileSync } from 'node:fs';
@@ -10,6 +14,11 @@ const __dirname = path.dirname(__filename);
1014
const defaultRepoRoot = path.resolve(__dirname, '..');
1115
const defaultQualityScript = path.join(defaultRepoRoot, 'scripts/run-bitcode-spec-quality.mjs');
1216

17+
/** Soft subject limit — compact-log readability (50/72 law). */
18+
export const COMMIT_SUBJECT_SOFT_MAX = 50;
19+
/** Hard wrap for body lines (and absolute subject ceiling). */
20+
export const COMMIT_BODY_LINE_MAX = 72;
21+
1322
/**
1423
* @param {string[]} argv
1524
* @returns {{ commitMessagePath: string | null, repoRoot: string, qualityScript: string }}
@@ -28,12 +37,38 @@ export function parseArgs(argv) {
2837
return { commitMessagePath, repoRoot, qualityScript };
2938
}
3039

40+
/**
41+
* Strip comment lines (`# …`) from a commit-msg file payload.
42+
* @param {string} raw
43+
* @returns {string}
44+
*/
45+
export function stripCommitMessageComments(raw) {
46+
return raw
47+
.split('\n')
48+
.filter((line) => !line.startsWith('#'))
49+
.join('\n')
50+
.replace(/\s+$/u, '');
51+
}
52+
3153
/**
3254
* @param {string} commitMessagePath
3355
* @returns {string}
3456
*/
3557
export function readCommitTitle(commitMessagePath) {
36-
return readFileSync(commitMessagePath, 'utf8').split('\n')[0].trim();
58+
const text = stripCommitMessageComments(readFileSync(commitMessagePath, 'utf8'));
59+
return text.split('\n')[0]?.trim() ?? '';
60+
}
61+
62+
/**
63+
* @param {string} commitMessagePath
64+
* @returns {{ subject: string, bodyLines: string[] }}
65+
*/
66+
export function readCommitMessageParts(commitMessagePath) {
67+
const text = stripCommitMessageComments(readFileSync(commitMessagePath, 'utf8'));
68+
const lines = text.split('\n');
69+
const subject = (lines[0] ?? '').trimEnd();
70+
const bodyLines = lines.slice(1);
71+
return { subject, bodyLines };
3772
}
3873

3974
/** Expanded forms are illegal in subjects; only abbreviated Spec/Impl labels. */
@@ -78,6 +113,64 @@ export function assertAbbreviatedCommitCategoryLabel(commitTitle) {
78113
}
79114
}
80115

116+
/**
117+
* Enforce 50/72 commit message law (SPECIFYING §2.8).
118+
* - Subject ≤ 50 soft (warn); > 72 hard fail
119+
* - Blank line after subject when body content follows
120+
* - Each body line ≤ 72 hard fail
121+
*
122+
* @param {{ subject: string, bodyLines: string[] }} parts
123+
* @param {{ warn?: (message: string) => void }} [options]
124+
*/
125+
export function assertCommitMessageFiftySeventyTwo(parts, options = {}) {
126+
const warn = options.warn ?? ((message) => process.stderr.write(`${message}\n`));
127+
const subject = parts.subject ?? '';
128+
const subjectLen = subject.length;
129+
130+
if (!subject.trim()) {
131+
throw new Error('Commit subject (first line) must not be empty.');
132+
}
133+
134+
if (subjectLen > COMMIT_BODY_LINE_MAX) {
135+
throw new Error(
136+
`Commit subject is ${subjectLen} characters (hard max ${COMMIT_BODY_LINE_MAX}). ` +
137+
`Keep the first line ≤ ${COMMIT_SUBJECT_SOFT_MAX} (soft 50/72 law) and put detail in the body.`,
138+
);
139+
}
140+
141+
if (subjectLen > COMMIT_SUBJECT_SOFT_MAX) {
142+
warn(
143+
`Bitcode 50/72: subject is ${subjectLen} characters (soft max ${COMMIT_SUBJECT_SOFT_MAX}). ` +
144+
`Prefer a shorter first line; put detail in the body (≤ ${COMMIT_BODY_LINE_MAX} per line).`,
145+
);
146+
}
147+
148+
const bodyLines = parts.bodyLines ?? [];
149+
// Trailing empty lines after a body are fine; detect non-empty body content.
150+
const firstNonEmptyBodyIndex = bodyLines.findIndex((line) => line.trim().length > 0);
151+
if (firstNonEmptyBodyIndex === -1) {
152+
return;
153+
}
154+
155+
// When a body follows, line immediately after the subject must be blank.
156+
if (bodyLines.length === 0 || bodyLines[0].trim().length > 0) {
157+
throw new Error(
158+
'Commit message 50/72 law: leave the second line completely empty to separate ' +
159+
'the subject from the body.',
160+
);
161+
}
162+
163+
for (let index = 0; index < bodyLines.length; index += 1) {
164+
const line = bodyLines[index];
165+
if (line.length > COMMIT_BODY_LINE_MAX) {
166+
throw new Error(
167+
`Commit body line ${index + 2} is ${line.length} characters ` +
168+
`(hard max ${COMMIT_BODY_LINE_MAX} under 50/72 law). Wrap the body.`,
169+
);
170+
}
171+
}
172+
}
173+
81174
/**
82175
* @param {{ commitMessagePath: string | null, repoRoot: string, qualityScript: string }} options
83176
*/
@@ -86,8 +179,10 @@ export function runCommitMessageCheck({ commitMessagePath, repoRoot, qualityScri
86179
throw new Error('A commit message file path is required.');
87180
}
88181

89-
const commitTitle = readCommitTitle(commitMessagePath);
182+
const parts = readCommitMessageParts(commitMessagePath);
183+
const commitTitle = parts.subject.trim();
90184
assertAbbreviatedCommitCategoryLabel(commitTitle);
185+
assertCommitMessageFiftySeventyTwo(parts);
91186
execFileSync(
92187
process.execPath,
93188
[

0 commit comments

Comments
 (0)