LearnNewsExamplesServices
Frontmatter
id17226
titleScoped block-alignment repair maps index line numbers onto the working tree
stateClosed
labels
bugaibuild
assigneesneo-opus-grace
createdAtAug 16, 2026, 1:42 AM
updatedAtAug 16, 2026, 11:00 AM
githubUrlhttps://github.com/neomjs/neo/issues/17226
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 16, 2026, 11:00 AM

Scoped block-alignment repair maps index line numbers onto the working tree

neo-opus-grace
neo-opus-grace commented on Aug 16, 2026, 1:42 AM

Context

Found while reviewing PR #17223, which converts the block-alignment pre-commit gate from reject to repair (--fix --staged). The PR meets all five of its ACs on the path it ships, and this is a distinct defect in a sibling invocation path — filed rather than raised as a blocker, and owned by me because I found it.

The Problem

getStagedAddedLines derives its line numbers from git diff --cached (stagedDiff.mjs:59) — the index side. processFile reads and rewrites the working tree file. When those two differ, the owned-line set is applied to the wrong positions.

Reproduced at PR #17223 head e7b438dff3 in a scratch repo. A file with a staged misaligned object block, plus three later unstaged comment lines inserted above it:

<h1 class="neo-h1" data-record-id="4">staged:  const zz / const obj block at index lines 1-5</h1>

<h1 class="neo-h1" data-record-id="5">worktree: 3 unstaged comment lines prepended, shifting everything by 3</h1>

$ node buildScripts/util/check-block-alignment.mjs --fix --staged f.mjs
Aligned 1 line(s) in f.mjs — left 1 untouched-line violation(s) as-is
exit=0

Result:

// unstaged line A
// unstaged line B
// unstaged line C
const zz  = 1;      // ← REWRITTEN. Not staged, not touched by the staged change.
const obj = {
    id: 1,          // ← the actually-staged drift, left unrepaired
    namelong: 2
};

Two failures in one pass: it wrote to a line the author did not stage, and it did not repair the drift the author did stage. The message reads as success and the exit code is 0.

The Architectural Reality

  • buildScripts/util/stagedDiff.mjs:59git diff --cached --unified=0 -- <file>, so the returned set is new-side index line numbers.
  • buildScripts/util/check-block-alignment.mjsprocessFile reads the file from disk (working tree) and, in the scoped-repair branch, indexes applied[violation.lineIndex] with those index-derived numbers.
  • The mismatch is pre-existing — check mode (--staged, shipped in #13720) has always had it. What PR #17223 changes is the consequence: the same divergence turned from reporting the wrong line number into rewriting the wrong line, silently.

Exposure is bounded, and that is why this is not a blocker on #17223:

  • The hook path is safe. lint-staged stashes unstaged changes for partially-staged files before running tasks, so index and working tree agree there. PR #17223's live dogfood exercises exactly this path.
  • The check-mode failure message steers users to --fix <files> (whole-file), not --fix --staged, so the documented remedy does not lead here.
  • The exposure is the manual invocation of --fix --staged, which the script's own usage header advertises.

Impact is whitespace-only — it cannot change program semantics — but it is a silent write to a line the author does not own, which is the exact harm the scoped repair's fail-closed design exists to prevent.

The Fix

Extend the existing fail-closed rule to cover the precondition it currently assumes. Before applying a scoped repair, verify the file has no unstaged changes (git diff --quiet -- <file>, non-zero exit ⇒ working tree differs from index). On divergence, take the path the guard already has for an unreliable staged set: report the violations, write nothing, return 'unfixable'.

That reuses the disposition PR #17223 introduced rather than adding a new one, and keeps the philosophy the file already states: "a git hiccup must never silently become a whole-file reformat inside an author's commit." A partially-staged file is the same hazard by a different route.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
check-block-alignment.mjs --fix --staged #17201 AC1/AC2 as shipped in PR #17223 Refuses to rewrite when the working tree differs from the index for that file Reports violations, exit 1, file byte-identical — the existing 'unfixable' path script usage header scratch-repo reproduction above at e7b438dff3
--staged check mode #13720 Unchanged — misreported line numbers are pre-existing and non-destructive n/a n/a same divergence, reporting-only consequence

Acceptance Criteria

  • With unstaged changes present in a file, --fix --staged reports and writes nothing; the file is byte-identical afterwards, proven by a fixture whose unstaged edit shifts staged line numbers.
  • With no unstaged changes, the scoped repair behaves exactly as it does today — proven by the existing #17201 specs staying green without modification.
  • The refusal is distinguishable from the git-read-failure refusal in its message, so an author can tell "stage or stash your other edits" from "git is broken".
  • Pure --fix (whole-file, deliberate) is unaffected by the new precondition.

Out of Scope

  • Changing check mode's line-number source, or teaching the scoped repair to translate index line numbers onto working-tree positions. Refusing is correct here: a partially-staged file has no single authoritative content for the repair to target.
  • Any change to the detector or to lint-staged wiring.

Avoided Traps

  • Translate index positions to working-tree positions. Rejected: it requires diffing the working tree against the index and remapping, which reintroduces the same class of error with more machinery, for a case where refusing costs the author one git stash.
  • Read the file from the index instead of disk. Rejected: the repair must write the working tree, so reading the index would fix the line numbers and then write them to the wrong content.
  • Treat it as pre-existing and leave it. Rejected: the divergence is pre-existing but its consequence is not — reporting a wrong line number and rewriting a wrong line are different severities.

Decision Record impact

none.

Related

  • Sibling: #17201 (the convert-to-repair ticket) / PR #17223
  • Origin of the scoping helper: #13720
  • Origin of the aligner: #13556

Live latest-open sweep: checked latest 20 open issues at 2026-08-16T00:05Z; no equivalent found.

Origin Session ID: b17338dd-b474-494f-b08c-683044de2ddb

Retrieval Hint: check-block-alignment scoped repair index vs working tree partially staged getStagedAddedLines line number divergence

tobiu referenced in commit 49e3d70 - "fix(build): the scoped repair checks the coordinates it was assuming (#17226) (#17232) on Aug 16, 2026, 11:00 AM
tobiu closed this issue on Aug 16, 2026, 11:00 AM