You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Re-measured 2026-09-07 at adab560, and half of this issue is done. The
committed CRLF artifact was renormalised by #361 on 3 September, before you
were pointed here. Nothing tracked is CRLF or mixed today:
What is left is the prevention half, which nobody has touched: the missing .gitattributes, the guard, and the CONTRIBUTING.md paragraph. The trap
named under "Tests first" has inverted. The guard now passes on its first
run, so a contributor who adds it and sees green has learned nothing about
whether it works. Prove it bites before you believe it.
Three line references below have moved and one figure is gone:
So record_test_baseline.py:65 is now 159-160 (#393 moved it), action_reference_doc.rs:154 is 162 (156 on 2026-09-07; #384 moved it again on 2026-09-09), and the "450 bytes of LF over 467 bytes
of CRLF" sentence describes a file that #393 replaced with a 193-byte schema.
The reasoning around each of them still holds; only the numbers moved.
The repository has no .gitattributes, and core.autocrlf is whatever the contributor's machine set. A Windows or WSL clone with autocrlf=true therefore gets CRLF in the working tree, and a commit back from that clone puts CRLF into the index. Nothing in the repo prevents either half, and nothing tells the contributor it happened.
That file arrived CRLF in 60af0ad, and it is LF in every earlier commit that touched it:
$ for c in 60af0ad 0640818 5867807 5a9450a; do
echo "$c $(git show $c:tests/evidence/workspace-tests.json | grep -c $'\r')"
done
60af0ad 17
0640818 0
5867807 0
5a9450a 0
scripts/record_test_baseline.py:65 writes the file with json.dumps(...) + "\n", so the generator never produces those bytes. They came from somebody's checkout.
Why it matters
It cost two contributors a day, in the same 24 hours.
crates/sysknife-daemon/tests/action_reference_doc.rs:154 compares the file on disk against a generated string byte for byte:
let committed = std::fs::read_to_string(&path).unwrap_or_default();assert_eq!(committed, generated,"docs/action-reference.md is out of date ...");
The generator emits \n. A CRLF working tree fails on line endings alone, and the message blames the action catalogue. #341 reported it as a pre-existing failure on clean main, and it passes on Linux:
The same PR carries three sed -i 's/\r$//' calls against files it copies out of the repo, which is the same symptom being worked around one file at a time.
Neither contributor did anything wrong. The repo let a mangled checkout look like a broken catalogue.
The committed artifact costs something separate, and quietly. scripts/check_evidence_claims.py still passes, because a carriage return is legal JSON whitespace:
$ python3 scripts/check_evidence_claims.py
Published figures match the evidence artifacts.
So no gate goes red. The next contributor who runs UPDATE_TEST_BASELINE=1 scripts/test_baseline.sh gets a whole-file diff instead of a one-line one, because the generator writes 450 bytes of LF over 467 bytes of CRLF.
Add .gitattributes with * text=auto eol=lf, plus -text for the binary paths already marked as such (icons, .ico, .png, cassette fixtures if any are binary).
Mark the executable shell scripts explicitly, since a CRLF shebang line makes bash report bad interpreter: /usr/bin/env bash^M, which is the next thing this bites.
Say in CONTRIBUTING.md what to run after cloning on Windows or WSL: git config core.autocrlf false, then git rm --cached -r . && git reset --hard to renormalise an already-mangled tree.
Tests first
git ls-files --eol is the assertion. A guard in tests/release/ that fails when any tracked file is i/crlf or i/mixed pins it, and it costs one git call:
The trap in this issue has inverted since it was written. That grep matched tests/evidence/workspace-tests.json when this was filed; #361 renormalised the file, so the guard now goes green the moment you add it. A guard that has only ever been observed passing is the defect this tracker keeps meeting. Commit a CRLF file on a scratch branch, watch the guard go red, drop the branch, and say in the PR that you did.
Prove it bites by committing a CRLF file in a scratch branch, not by trusting the grep.
Difficulty
easy. The whole change is one new file, one docs paragraph and one guard.
Getting started
CONTRIBUTING.md has the build and test commands. No CLA and no copyright waiver. The project is MIT.
The repository has no
.gitattributes, andcore.autocrlfis whatever the contributor's machine set. A Windows or WSL clone withautocrlf=truetherefore gets CRLF in the working tree, and a commit back from that clone puts CRLF into the index. Nothing in the repo prevents either half, and nothing tells the contributor it happened.Both halves have now happened. At
60af0ad:That file arrived CRLF in
60af0ad, and it is LF in every earlier commit that touched it:scripts/record_test_baseline.py:65writes the file withjson.dumps(...) + "\n", so the generator never produces those bytes. They came from somebody's checkout.Why it matters
It cost two contributors a day, in the same 24 hours.
crates/sysknife-daemon/tests/action_reference_doc.rs:154compares the file on disk against a generated string byte for byte:The generator emits
\n. A CRLF working tree fails on line endings alone, and the message blames the action catalogue. #341 reported it as a pre-existing failure on cleanmain, and it passes on Linux:The same PR carries three
sed -i 's/\r$//'calls against files it copies out of the repo, which is the same symptom being worked around one file at a time.Neither contributor did anything wrong. The repo let a mangled checkout look like a broken catalogue.
The committed artifact costs something separate, and quietly.
scripts/check_evidence_claims.pystill passes, because a carriage return is legal JSON whitespace:So no gate goes red. The next contributor who runs
UPDATE_TEST_BASELINE=1 scripts/test_baseline.shgets a whole-file diff instead of a one-line one, because the generator writes 450 bytes of LF over 467 bytes of CRLF.Scope
RenormaliseDone in fix(evidence): restore LF line endings in workspace-tests.json #361 on 3 September; there is nothing left to renormalise.tests/evidence/workspace-tests.jsonto LF..gitattributeswith* text=auto eol=lf, plus-textfor the binary paths already marked as such (icons,.ico,.png, cassette fixtures if any are binary).bashreportbad interpreter: /usr/bin/env bash^M, which is the next thing this bites.CONTRIBUTING.mdwhat to run after cloning on Windows or WSL:git config core.autocrlf false, thengit rm --cached -r . && git reset --hardto renormalise an already-mangled tree.Tests first
git ls-files --eolis the assertion. A guard intests/release/that fails when any tracked file isi/crlfori/mixedpins it, and it costs onegitcall:The trap in this issue has inverted since it was written. That grep matched
tests/evidence/workspace-tests.jsonwhen this was filed; #361 renormalised the file, so the guard now goes green the moment you add it. A guard that has only ever been observed passing is the defect this tracker keeps meeting. Commit a CRLF file on a scratch branch, watch the guard go red, drop the branch, and say in the PR that you did.Prove it bites by committing a CRLF file in a scratch branch, not by trusting the grep.
Difficulty
easy. The whole change is one new file, one docs paragraph and one guard.Getting started
CONTRIBUTING.md has the build and test commands. No CLA and no copyright waiver. The project is MIT.