Context
check-block-alignment.mjs --fix (the mechanical block-alignment fixer) silently rewrites a valid multi-declarator const into a SyntaxError when a declarator is an object/array destructuring pattern that carries a default. Surfaced on PR #15060 (route-attribution seam, #15057): buildRouteAttributionRecords's const {blockedNodes = [], …} = focusContradiction, focusReasons = … was mangled to SyntaxError 387:139. It passed the author's local npx playwright test and only went red in CI — every unit spec importing the file threw, costing a full CI cycle.
The Problem
The aligner locates the assignment = with line.indexOf('=') — the first = on the line (splitAssignment / parseKeywordDeclaration). For a defaulted destructuring LHS the first = is the default inside the {…} pattern, not the assignment operator, so the fixer slices mid-pattern and eats tokens:
const {blockedNodes = [], focusCandidates = []} = focusContradiction,
focusReasons = [...new Set(...)];
const {blockedNodes = focusContradiction,
focusReasons = [...];Two-part trap: (1) the fixer corrupts the code, and (2) an author runs --fix manually — lint-staged runs check-block-alignment --staged (check-only, which tells you to run --fix) after the local test pass, so the corrupted form is never executed locally. Green locally, red in CI. Default-free destructuring ({record}) has no internal = and aligns fine — the #13908 behaviour, which must be preserved.
The Architectural Reality
buildScripts/util/check-block-alignment.mjs — DECL_BINDING regex + splitAssignment (line.indexOf('=')). Same "mechanical fixer emits wrong output" class as #13670 (--fix corrupts template-string JSON) and #14212 (--fix over-reaches).
- lint-staged
*.mjs chain (package.json): the block-align step is --staged (check-only, diff-scoped to added lines — #13720); it never runs --fix and never parse-checks its subject.
buildScripts/util/check-*.mjs — established sibling gate pattern (check-shorthand, check-jsdoc-types, check-ticket-archaeology).
The Fix
- Aligner guard (
check-block-alignment.mjs): DECL_BINDING excludes = from its bracket branches (\{[^}=]+\} / \[[^\]=]+\]), so a defaulted destructuring LHS no longer matches as a declaration — it breaks the run and is left byte-for-byte untouched. Default-free {record} still matches and aligns (#13908 preserved). One-line change + explanatory comment.
- Durable commit-time gate (new
buildScripts/util/check-parse.mjs, wired last in the *.mjs lint-staged chain): node --check every staged .mjs, fail the commit if any no longer parses. A mechanical fixer will always have edge cases; a parse gate catches every one at commit time — regardless of which fixer or hand-edit produced the break. V-B-A: all 2587 tracked .mjs pass node --check (zero false-positive risk).
Decision Record impact
none — build tooling; no ADR interaction.
Acceptance Criteria
Out of Scope
- Refactoring the aligner's
=-detection to align defaulted destructuring — deliberately excluded, not aligned (the conservative "never touch an un-alignable shape" stance).
- Switching lint-staged from
--staged (check) to --fix (auto-fix) — left unchanged.
Avoided Traps
- Blanket "skip all destructuring" would regress
#13908 (default-free {record}/{value} deliberately align). The exclusion is scoped to defaulted patterns only, verified against the existing test.
- In-tool
--fix self-guard (refuse to write unparseable output): considered, but post-guard the aligner is corruption-free for realistic inputs (fuzzed), making that path untriggerable/untestable. A commit-time parse gate is general (any source), testable, and directly implements "fail the commit if any no longer parses."
Related
- Origin seam: #15057 / PR #15060 (route-attribution record-seam).
- Sibling
--fix-corruption bugs: #13670, #14212. Diff-scope precedent: #13720. Destructuring alignment: #13908.
Handoff Retrieval Hints
query_raw_memories: "check-block-alignment --fix corrupts destructuring-with-defaults"
- Git anchor: the
fix(build): … commit on branch claude/exciting-bhaskara-4872e6.
Live latest-open sweep: checked latest 20 open issues at 2026-07-12; no equivalent found.
Context
check-block-alignment.mjs --fix(the mechanical block-alignment fixer) silently rewrites a valid multi-declaratorconstinto aSyntaxErrorwhen a declarator is an object/array destructuring pattern that carries a default. Surfaced on PR #15060 (route-attribution seam, #15057):buildRouteAttributionRecords'sconst {blockedNodes = [], …} = focusContradiction, focusReasons = …was mangled toSyntaxError 387:139. It passed the author's localnpx playwright testand only went red in CI — every unit spec importing the file threw, costing a full CI cycle.The Problem
The aligner locates the assignment
=withline.indexOf('=')— the first=on the line (splitAssignment/parseKeywordDeclaration). For a defaulted destructuring LHS the first=is the default inside the{…}pattern, not the assignment operator, so the fixer slices mid-pattern and eats tokens:// INPUT (valid): const {blockedNodes = [], focusCandidates = []} = focusContradiction, focusReasons = [...new Set(...)]; // --fix OUTPUT (SyntaxError "Unexpected token, expected ','"): const {blockedNodes = focusContradiction, focusReasons = [...];Two-part trap: (1) the fixer corrupts the code, and (2) an author runs
--fixmanually — lint-staged runscheck-block-alignment --staged(check-only, which tells you to run--fix) after the local test pass, so the corrupted form is never executed locally. Green locally, red in CI. Default-free destructuring ({record}) has no internal=and aligns fine — the#13908behaviour, which must be preserved.The Architectural Reality
buildScripts/util/check-block-alignment.mjs—DECL_BINDINGregex +splitAssignment(line.indexOf('=')). Same "mechanical fixer emits wrong output" class as#13670(--fixcorrupts template-string JSON) and#14212(--fixover-reaches).*.mjschain (package.json): the block-align step is--staged(check-only, diff-scoped to added lines —#13720); it never runs--fixand never parse-checks its subject.buildScripts/util/check-*.mjs— established sibling gate pattern (check-shorthand,check-jsdoc-types,check-ticket-archaeology).The Fix
check-block-alignment.mjs):DECL_BINDINGexcludes=from its bracket branches (\{[^}=]+\}/\[[^\]=]+\]), so a defaulted destructuring LHS no longer matches as a declaration — it breaks the run and is left byte-for-byte untouched. Default-free{record}still matches and aligns (#13908preserved). One-line change + explanatory comment.buildScripts/util/check-parse.mjs, wired last in the*.mjslint-staged chain):node --checkevery staged.mjs, fail the commit if any no longer parses. A mechanical fixer will always have edge cases; a parse gate catches every one at commit time — regardless of which fixer or hand-edit produced the break. V-B-A: all 2587 tracked.mjspassnode --check(zero false-positive risk).Decision Record impact
none— build tooling; no ADR interaction.Acceptance Criteria
check-block-alignment --fixbyte-identical andnode --check-valid.--fixit).#13908) is preserved.check-parse.mjsexits 1 (naming the file + SyntaxError) on any staged.mjsthat failsnode --check; exits 0 otherwise; non-.mjsargs ignored.check-parse.mjswired as the final*.mjslint-staged step; a corrupted staged.mjsis blocked at commit (verified via lint-staged dry-run).#15057regression incheck-block-alignment.spec.mjs+ newcheck-parse.spec.mjs.Out of Scope
=-detection to align defaulted destructuring — deliberately excluded, not aligned (the conservative "never touch an un-alignable shape" stance).--staged(check) to--fix(auto-fix) — left unchanged.Avoided Traps
#13908(default-free{record}/{value}deliberately align). The exclusion is scoped to defaulted patterns only, verified against the existing test.--fixself-guard (refuse to write unparseable output): considered, but post-guard the aligner is corruption-free for realistic inputs (fuzzed), making that path untriggerable/untestable. A commit-time parse gate is general (any source), testable, and directly implements "fail the commit if any no longer parses."Related
--fix-corruption bugs:#13670,#14212. Diff-scope precedent:#13720. Destructuring alignment:#13908.Handoff Retrieval Hints
query_raw_memories: "check-block-alignment --fix corrupts destructuring-with-defaults"fix(build): …commit on branchclaude/exciting-bhaskara-4872e6.Live latest-open sweep: checked latest 20 open issues at 2026-07-12; no equivalent found.