Repository navigation
Bundle files are not byte-stable across platforms: text-mode writes plus autocrlf break recorded digests #36
Description
Problem
Bundle files are written in text mode without newline="", so on Windows the writers turn \n into \r\n. Bundle digests are SHA-256 over file bytes, so the same trace then produces different bytes and a different digest per platform.
This came out of adding the cross-platform smoke job (#35): the job passes on Windows, but it does not read any committed bundle, and while checking that I could see the format itself is not byte-stable across platforms.
Where
Writers in src/stepfork/trace/storage.py, none of which opt out of newline translation:
_write_manifest—path.write_text(...)_write_events—path.open("w", ...)forevents.jsonl_write_redactions—path.write_text(...)
Readers of those bytes:
trace/integrity.py::_hash_file—path.open("rb"), SHA-256 over the bytesexport/receipt.py::bundle_digest/file_sha256— same, per file in sorted order
Two consequences
1. A bundle written on Windows does not reproduce a digest recorded elsewhere. Measured here by rewriting a committed bundle's files as CRLF in a temp copy, which is what a Windows write or a Windows checkout produces:
bundle: legacy_v01.sftrace
files rewritten to CRLF: 3
digest before: 4cdc805dda5e69e84d8327355d16d9eb8b631b15a3c43d078897511eabb7ec5a
digest after : 2d8f91b50e6266e95de9d6c72103aa6d56bfa5c2db66eb3e82fbb080ec3c9dea
DIGEST CHANGED: True
2. A Windows checkout corrupts the committed bundles before any test runs. There is no .gitattributes in the repo, so git's default core.autocrlf=true on Windows converts them at checkout. That is user-visible through stepfork validate --verify-integrity, which is opt-in and therefore unaffected on Linux CI:
# as committed (LF)
$ stepfork validate --verify-integrity langgraph_failure.sftrace
Validation passed.
# after rewriting the same bundle as CRLF, i.e. a Windows checkout
$ stepfork validate --verify-integrity langgraph_failure.sftrace
✗ integrity_mismatch
SHA-256 mismatch.
redactions.json
Validation failed: 3 issues. (exit 1)
Scope
- 35 committed files live under a
.sftracedirectory (26.json, 9.jsonl), acrosstests/golden(27),examples/real-llm-case-study(4) andexamples/langgraph_agent/regression(4). - 8 of those bundles carry an
integrity.json, so they are the ones--verify-integritycan reject today:tests/golden/{diff_baseline,diff_changed,failed_llm_run,failed_tool_run,partial_run,successful_run},examples/langgraph_agent/regression/langgraph_failure,examples/real-llm-case-study/traces/incident-triage. All eight validate on LF on this machine. events.jsonlis JSONL, so a CRLF file is still parseable line by line — the breakage is in the digests, not in parsing. Worth stating because "it still loads" is not evidence of no problem.
Fix
Two parts, both in the linked PR:
newline=""on all three writers, so the bytes written are LF on every platform.- A
.gitattributesrule so a checkout cannot rewrite the committed bundles:**/*.sftrace/** text eol=lf.
On the pattern: *.sftrace/* and *.sftrace/**/* do not match nested files (checked with git check-attr), **/*.sftrace/** does. Narrow on purpose — other files in the repo keep git's default behaviour, and none of the 35 bundle files changes in the working tree as a result, so there is no renormalisation commit hiding in this.
Test note
Two of the three new tests can pass on POSIX with or without the fix, and I would rather say that than imply coverage I do not have:
- "saved bundle files contain no CRLF" is the assertion that fails on Windows if
newline=""is dropped; on POSIX the translation is a no-op, so it is a guard for a platform CI can reach but this machine cannot. - "bundle digest changes when a file gains CRLF" describes the stake, not the fix, and passes either way.
- The
.gitattributestest does fail on every platform when the rule is removed — verified by deleting the file and watching it go red.
Activity