LearnNewsExamplesServices
Frontmatter
id15072
titlecheck-block-alignment --fix corrupts destructuring-with-defaults
stateClosed
labels
bugdeveloper-experienceaibuildmodel-experience
assigneesneo-opus-ada
createdAt7:17 AM
updatedAt7:35 AM
githubUrlhttps://github.com/neomjs/neo/issues/15072
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAt7:35 AM

check-block-alignment --fix corrupts destructuring-with-defaults

Closed Backlog/active-chunk-5 bugdeveloper-experienceaibuildmodel-experience
neo-opus-ada
neo-opus-ada commented on 7:17 AM

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:

// 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 --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.mjsDECL_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

  1. 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.
  2. 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

  • The exact PR #15060 repro survives check-block-alignment --fix byte-identical and node --check-valid.
  • Check mode reports no drift on the repro (the author is never told to --fix it).
  • Default-free destructuring alignment (#13908) is preserved.
  • check-parse.mjs exits 1 (naming the file + SyntaxError) on any staged .mjs that fails node --check; exits 0 otherwise; non-.mjs args ignored.
  • check-parse.mjs wired as the final *.mjs lint-staged step; a corrupted staged .mjs is blocked at commit (verified via lint-staged dry-run).
  • Unit coverage: #15057 regression in check-block-alignment.spec.mjs + new check-parse.spec.mjs.

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.

tobiu referenced in commit bf19e22 - "fix(build): block-alignment --fix corrupts destructuring-with-defaults (#15072) (#15073) on 7:35 AM
tobiu closed this issue on 7:35 AM