---
name: a-case-only-filename-difference-is-one-file-on-ntfs
description: "Two scratch files whose names differ only by case (CHANGELOG.md vs changelog.md) are ONE file on Windows/NTFS — the second write silently replaces the first and a downstream copy carries the wrong content; a marker scan cannot see it, only a content-identity check can."
metadata: 
  node_type: memory
  type: feedback
  originSessionId: e5345397-2986-4a4f-85d5-7c0beec52778
  modified: 2026-09-10T03:12:11.246Z
---

2026-09-10 doyle, #295 merge into assembly/v0.69.0 (head 7): I spooled three conflict blobs from
`git merge-tree` to scratch as `c295-$(basename $f).conf`. Two of the conflicted paths were
`CHANGELOG.md` and `docs-site/src/changelog.md` — same basename, different case. On this NTFS box
`c295-CHANGELOG.md.conf` and `c295-changelog.md.conf` are the SAME file, so the docs-site page
(with its "> **Generated** … do not edit by hand" banner and NO treqs tags) overwrote the root
changelog's conflict blob, and my resolver then wrote the docs-site content into the root
`CHANGELOG.md`. `git add` accepted it. The changed-blob marker scan read 0 — it was a clean,
marker-free, WRONG file.

**What caught it:** the per-head `[0.68.0]`-block byte-identity check (sha256 prefix went from
`406dd3cdd2a37844` to `4c62c54a…`) and the in-section grep for the two `[doc->REQ-…]` tags, which
both read ABSENT. deployah runs the same block check per head; that is why it exists.

**Why:** a marker scan proves only that no conflict TEXT survived; it says nothing about WHICH
content was chosen. Filename collisions on a case-insensitive filesystem leave no error, no
warning, and no second file to notice — the only evidence is content that belongs to the other
path.

**How to apply:**
- Never key scratch files by `basename` — key them by the full path with separators replaced
  (`tr '/' '__'`), or by an explicit per-file name you chose.
- After ANY conflict resolution, assert content identity against the parent for every region
  the merge should not have touched (the `[0.68.0]` block here), and assert the presence of
  every tag that must survive IN ITS SECTION. Marker count 0 is necessary, not sufficient.
- Treat "the second write won" as the default failure shape whenever two names collapse — the
  same mechanism as [[a-local-copy-uploaded-over-a-staged-remote-script-erases-its-remote-only-fix]]
  and the CRLF `=======\r` miss in the same session (exact-line compare on a CRLF working copy;
  resolve from the LF `merge-tree` blob, never the checked-out working copy).
