Refs D#17085
Operator-framed: "write code any way you like → pre-commit hook reformats, done. Unless we need to iron out parts inside the auto-formatter." We do need to iron out one part, and it is the reason this was never wired up.
Problem
check-block-alignment.mjs already knows how to fix what it rejects — --fix exists and rewrites files. The pre-commit hook runs it in check mode (--staged) and blocks instead, so the author is told to run a fixer the tool could have run itself.
Measured cost in one session: it blocked commits repeatedly, and each time the resolution was mechanically identical — run --fix on the same files, re-stage, retry. No judgment was exercised at any point. That is a check standing where a fix belongs.
Why it was not simply wired in — the real gap
The two modes disagree about scope, and only one of them is safe:
const added = gitRoot ? getStagedAddedLines(file, gitRoot) : null;
const reported = added ? allViolations.filter(v => added.has(v.lineIndex + 1)) : allViolations;
Check mode is line-scoped: a grandfathered misalignment on an untouched line never blocks an unrelated commit. --fix ignores that scoping entirely. Wiring today's --fix into the hook would reformat lines the author never touched, spraying unrelated changes into every commit — worse than the friction it removes, and it would make every diff a review hazard.
So this is not an oversight. It is a formatter that is correct for a deliberate whole-file pass and unsafe for an automatic one.
The Architectural Reality
- The scoping function the fixer needs already exists and is already correct:
getStagedAddedLines(file, gitRoot). Check mode applies it to the report set; fix mode needs it applied to the rewrite set.
lint-staged already re-stages files modified by tasks — every commit this session logged Staging changes from tasks…. No new plumbing.
- The guarantee is unchanged either way: the same violations are detected by the same code. Only the disposition changes, from reject to repair.
- Fail-closed behaviour must survive the change. Check mode reports the whole file when the git read fails, deliberately; a scoped fixer must refuse to rewrite in that case rather than fall back to whole-file, or a git hiccup becomes an unrelated reformat.
Why this is its own disposition verb, and worth naming
D#17085 has been sorting gates into keep / retire. This one is neither: it is a convert. The enforcement is right, the incident class is live, the cost is pure friction — the correct move is to keep the guarantee and delete the interaction.
A gate that can compute the correct output should never ask the author to produce it. Where a fix is deterministic, rejecting is strictly worse than repairing: identical guarantee, one round-trip more, and it spends the author's attention on something no judgment was required for.
The Fix
- Apply the existing staged-line scoping to
--fix, so it rewrites only lines the author touched.
- Make the scoped fixer fail closed: no reliable staged-line set ⇒ report, never rewrite.
- Run
--fix --staged from the pre-commit hook and let lint-staged re-stage; keep pure --fix as today's deliberate whole-file pass.
Out of Scope
- The alignment rules themselves. This changes disposition, not house style.
- Other check-only guards that could become fixers (
check-whitespace is the obvious sibling). Worth the same treatment, and worth doing after this one proves the shape.
Acceptance Criteria
Evidence class
Read at source on dev 2026-08-15: check-block-alignment.mjs — --fix write path, the getStagedAddedLines scoping in check mode, and the in-file comment stating fix mode is whole-file. Friction observed live across one session: the hook blocked commits repeatedly, resolved every time by the identical manual --fix + re-stage cycle, with no judgment exercised. lint-staged's re-stage step observed in every commit run this session.
Live latest-open sweep at 2026-08-15T18:31:40Z; nothing equivalent open. A2A sweep: no competing claim.
Decision Record impact
none.
🖖 Authored by Grace (Claude Opus 5, Claude Code). Session b17338dd-b474-494f-b08c-683044de2ddb. Operator-framed during the D#17085 re-pricing conversation; filed as the first instance of the convert verb rather than keep-or-retire.
Refs D#17085
Operator-framed: "write code any way you like → pre-commit hook reformats, done. Unless we need to iron out parts inside the auto-formatter." We do need to iron out one part, and it is the reason this was never wired up.
Problem
check-block-alignment.mjsalready knows how to fix what it rejects —--fixexists and rewrites files. The pre-commit hook runs it in check mode (--staged) and blocks instead, so the author is told to run a fixer the tool could have run itself.Measured cost in one session: it blocked commits repeatedly, and each time the resolution was mechanically identical — run
--fixon the same files, re-stage, retry. No judgment was exercised at any point. That is a check standing where a fix belongs.Why it was not simply wired in — the real gap
The two modes disagree about scope, and only one of them is safe:
// check mode: only drift the author actually introduced const added = gitRoot ? getStagedAddedLines(file, gitRoot) : null; const reported = added ? allViolations.filter(v => added.has(v.lineIndex + 1)) : allViolations;// and the file says so plainly: // (gitRoot is set only in --staged check mode; --fix always rewrites whole-file.)Check mode is line-scoped: a grandfathered misalignment on an untouched line never blocks an unrelated commit.
--fixignores that scoping entirely. Wiring today's--fixinto the hook would reformat lines the author never touched, spraying unrelated changes into every commit — worse than the friction it removes, and it would make every diff a review hazard.So this is not an oversight. It is a formatter that is correct for a deliberate whole-file pass and unsafe for an automatic one.
The Architectural Reality
getStagedAddedLines(file, gitRoot). Check mode applies it to the report set; fix mode needs it applied to the rewrite set.lint-stagedalready re-stages files modified by tasks — every commit this session loggedStaging changes from tasks…. No new plumbing.Why this is its own disposition verb, and worth naming
D#17085 has been sorting gates into keep / retire. This one is neither: it is a convert. The enforcement is right, the incident class is live, the cost is pure friction — the correct move is to keep the guarantee and delete the interaction.
A gate that can compute the correct output should never ask the author to produce it. Where a fix is deterministic, rejecting is strictly worse than repairing: identical guarantee, one round-trip more, and it spends the author's attention on something no judgment was required for.
The Fix
--fix, so it rewrites only lines the author touched.--fix --stagedfrom the pre-commit hook and letlint-stagedre-stage; keep pure--fixas today's deliberate whole-file pass.Out of Scope
check-whitespaceis the obvious sibling). Worth the same treatment, and worth doing after this one proves the shape.Acceptance Criteria
--fix --stagedrewrites only violations on staged-added lines; a grandfathered misalignment on an untouched line in the same file is left exactly as it was, proven by a fixture containing both.--fixstill performs the whole-file pass, unchanged, for deliberate invocation.Evidence class
Read at source on
dev2026-08-15:check-block-alignment.mjs—--fixwrite path, thegetStagedAddedLinesscoping in check mode, and the in-file comment stating fix mode is whole-file. Friction observed live across one session: the hook blocked commits repeatedly, resolved every time by the identical manual--fix+ re-stage cycle, with no judgment exercised.lint-staged's re-stage step observed in every commit run this session.Live latest-open sweep at 2026-08-15T18:31:40Z; nothing equivalent open. A2A sweep: no competing claim.
Decision Record impact
none.🖖 Authored by Grace (Claude Opus 5, Claude Code). Session b17338dd-b474-494f-b08c-683044de2ddb. Operator-framed during the D#17085 re-pricing conversation; filed as the first instance of the convert verb rather than keep-or-retire.