Skip to content

4 files pinned eol=lf in .gitattributes have non-LF index blobs — no gate covers the pinned set #1814

Closed as not planned

Description

@bradygaster

Measured on HEAD 8086c717 (.gitattributes 45 lines, identical to origin/dev) while verifying an unrelated Step 0 post-condition for the 2026-08-21 E2E series.

.gitattributes pins 173 tracked files to eol=lf. Four of them have a non-LF index blob, meaning the rule was added without renormalizing. No existing gate detects this.

Measured

git ls-files --eol | grep 'attr/text eol=lf' | grep -v 'i/lf'
i/none  w/none   attr/text eol=lf   docs/.astro/content-assets.mjs
i/none  w/none   attr/text eol=lf   docs/.astro/content-modules.mjs
i/mixed w/mixed  attr/text eol=lf   docs/src/pages/rss.xml.js
i/crlf  w/crlf   attr/text eol=lf   packages/squad-cli/src/remote-ui/app.js

Census: pinned=173, i/lf=169, not-i/lf=4, sum 173 — no file uncovered by the partition.

docs/src/pages/rss.xml.js is the same file that carries exactly one CRLF line in the working tree (documented in the E2E runbook). This is the index-side instance of it.

Why no gate catches these

scripts/check-shebang-eol.mjs is green (exit=0) and honest about why. Its selector is shebang detection, not .gitattributes:

Shebang EOL check passed: all 45 shebanged files are pinned to LF.

45 files checked, not 173. The script's own header documents the boundary explicitly:

SCOPE BOUNDARY -- read this before assuming a green run means "no EOL bugs". There are two CRLF failure classes and this lint covers exactly one:

  1. STRICT PARSE (covered). ... #! is a cheap, exact, static signature, so the check is sound and has no false negatives.
  2. TOOL REWRITE (NOT covered). ... Class 2 has no cheap static signature ... Extending this lint to guess would trade a sound check for an unsound one, so the boundary is deliberate.

That is a correct and deliberate scope decision — this issue is not a bug report against that lint. The lint's header even names the exact state these four files are in:

a rule that was added without renormalizing is still a broken repo

...and then correctly scopes itself out of finding it for non-shebanged files. The gap is real; the lint declining to cover it is not the defect.

Impact

packages/squad-cli/src/remote-ui/app.js is source, committed CRLF, pinned LF. Any tool that rewrites it with LF leaves the tree perpetually dirty, which is the precondition for a broad git add sweeping in unrelated changes -- the failure mode the pinning was introduced to prevent.

The two docs/.astro/*.mjs files are generated build output at i/none w/none; whether they should be tracked at all is a separate question.

Suggested fix

git add --renormalize docs/.astro/content-assets.mjs \
                      docs/.astro/content-modules.mjs \
                      docs/src/pages/rss.xml.js \
                      packages/squad-cli/src/remote-ui/app.js
git commit

Then verify the partition returns empty:

git ls-files --eol | grep 'attr/text eol=lf' | grep -v 'i/lf'

Suggested gate

The partition above is a complete, cheap, sound check over the pinned set and has no false negatives -- a pinned file either has an LF index blob or it does not. It complements the shebang lint rather than extending it, so it does not trade soundness for coverage:

Every path with attr/text eol=lf must report i/lf.

Note grep -v 'i/lf' rather than grep 'i/crlf' -- the tree contains i/mixed and i/none cases that an i/crlf-only filter misses. Three of the four above would be missed.

Tree-dependence -- read before reproducing

Do not treat the file list or the count as fixed. A second worktree at the same 45-line .gitattributes generation reported pinned=92 with a disjoint 3-file set (docs/pagefind.yml, samples/storage-provider-azure/scripts/{create,delete}-storage.sh), all three of which are i/lf w/lf clean here. Index state is per-worktree.

Pair any reported count with git rev-parse --short HEAD and the worktree, per the existing convention.

Not claimed

  • Not measured: how each blob came to be committed non-LF, or in which commit.
  • Not measured: whether docs/.astro/ should be tracked.
  • Not blocking the 2026-08-21 E2E series. Pre-existing, unrelated to the wave.

Activity

  1. added
    squadSquad triage inbox — Lead will assign to a member
    bugSomething isn't working
    on Aug 21, 2026
  2. bradygaster commented on Aug 21, 2026

    @bradygaster
    CollaboratorAuthor

    Retracted — all four cited files were measured from a stale tree. Zero real instances.

    Closing. The measurement in the issue body is wrong on all four paths, in two distinct ways. Recording the correction rather than deleting it, because the failure mode is the point.

    Root cause: HEAD was 6 commits behind origin/dev at filing time

    8086c717 on bradygaster-gh-aw-triage-pre-e2e, 6 behind. I never fetched before measuring.

    Two of the four were never defects

    docs/.astro/content-assets.mjs    25 bytes   LF=0  CR=0
    content: 'export default new Map();'
    

    Both docs/.astro/*.mjs files contain no line endings at all. i/none is the correct and expected report for a newline-free file — eol=lf on it is vacuous. These are not renormalization candidates and never were.

    This is a false-positive class in the proposed filter. The issue body suggested grep -v 'i/lf' specifically to catch i/mixed and i/none that an i/crlf-only filter would miss. i/mixed is a real defect state; i/none is not. The correct filter is:

    git ls-files --eol | grep 'attr/text eol=lf' | grep -E 'i/(crlf|mixed)'

    Measured on the same tree: -v i/lf returns 4, i/(crlf|mixed) returns 2. The two it drops are the vacuous ones.

    The other two are already renormalized on dev

    path HEAD blob origin/dev blob CRLF in dev blob
    docs/src/pages/rss.xml.js 75bae942 7073178a 0
    packages/squad-cli/src/remote-ui/app.js 5000d0c6 1521e5f2 0

    Both differ from HEAD and both dev blobs are clean. They were fixed in commits this worktree did not have. Blob extraction done via cmd.exe redirection rather than a PowerShell pipe, which re-encodes.

    Also incorrect: the tree-dependence note

    The body cites a second worktree at pinned=92 and calls it "the same 45-line .gitattributes generation." It was not — that worktree was 13 commits behind at a 27-line generation. Meanwhile:

    .gitattributes  HEAD        1a581426f5a3bd22b09d5175d389de797eed95aa  2456 bytes
    .gitattributes  origin/dev  1a581426f5a3bd22b09d5175d389de797eed95aa  2456 bytes
    

    Byte-identical. The 92 vs 173 spread is explained entirely by stale checkouts, not by the pinning definition moving. Line-counting the file through a pipe produced a third number; hashing the blob is decisive and should have been the instrument from the start.

    What survives

    The gate proposal is still cheap and sound — every path with attr/text eol=lf must report i/lf or i/none is a complete partition over the pinned set with no false negatives. But it now has zero known instances: three candidates found on another branch were renormalized by #1790, two here were renormalized on dev, and two were never defects.

    A gate proposal with no instance is speculative, so this closes rather than staying open as backlog. If a real i/(crlf|mixed) pinned file appears on dev, reopen with git rev-parse --short HEAD beside it.

    The scope-boundary analysis of scripts/check-shebang-eol.mjs in the body is accurate and unaffected — that lint's shebang-only selector is deliberate and correctly documented in its own header. Nothing here is a defect in it.

    Filing rule this cost

    Every count in the original body was real output from a real command. None of them measured the repository. git fetch plus the blob SHA of the file defining the set were each one command away.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsquadSquad triage inbox — Lead will assign to a member

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions