Task 14: Case-collision detection #14
Labels
No labels
data-integrity
engine
platform
procurement
remote
scaffold
ui
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Cordy/cairn-desktop#14
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Depends on Task 13.
Goal
Detect paths that differ only in letter case and refuse to sync them, rather than letting one silently overwrite the other.
Why this is data loss, not a nuisance
APFS and NTFS are case-insensitive by default; ext4 is case-sensitive. A Cairn server on Linux can legitimately hold
Report.pdfandreport.pdf. A Mac or Windows client cannot represent both. Downloading them naively means the second overwrites the first — data loss with no error message.Files
internal/sync/casefold.go,internal/sync/casefold_test.gointernal/sync/engine.goProduces
Keyed by the lowercased path, valued with the two-or-more real paths that fold onto it. Non-colliding paths are absent. The same path listed twice is not a collision.
Steps
DetectCaseCollisions. Group bystrings.ToLower, using a set per group so duplicates of the same exact path collapse.SyncOnce. After building the union of paths and before the per-path loop, compute the collided set once. Inside the loop, before theIllegalOncheck, skip any collided path with a reason explaining that another path differs only in letter case and cannot coexist on a case-insensitive filesystem.TestConvergencemust still pass — it uses distinct lowercase names, so it introduces no collisions.git commit -s -m "feat(sync): detect and quarantine case-only path collisions"Acceptance criteria
Done
778708291c— feat(sync): detect and quarantine case-only path collisions464cabcafe— fix(sync): fold case collisions the way APFS and NTFS compare namesWhat was built
DetectCaseCollisions(paths []string) map[string][]stringgroups paths that fold onto the same case-insensitive key, keyed by the folded path, 2+ real paths per group.SyncOnce: the collided set is computed once after the path union and before the loop; each collided path is skipped, beforeIllegalOn/observe/Decide/any FS,Remote,State call, so it can never be downloaded, uploaded, deleted, or counted as missing.strings.ToLower(strings.ToUpper(cases.Fold().String(p)))(golang.org/x/text/cases), so Greek final-sigma, Turkish dotless-ı, long-s and ß/ss twins are caught too, not just ASCII — plainToLoweralone missed those and left the exact silent-overwrite bug open.Tests
CI run #14 (http://192.168.10.245/Cordy/cairn-desktop/actions/runs/14), Go 1.25.5, linux/arm64 —
go vet ./...andgo test -count=1 ./...green acrossinternal/remote,internal/state,internal/sync,internal/vfs. Locally: TDD RED/GREEN cycles forTestDetectCaseCollisions*,TestSyncSkipsCaseCollisions,TestSyncNeverTouchesCaseCollisions,TestDetectCaseCollisionsFoldsBeyondASCII, plusTestConvergence(50 seeds) all pass;gofmtclean;go mod tidybyte-identical.Acceptance criteria
TestSyncSkipsCaseCollisionsandTestSyncNeverTouchesCaseCollisionsconfirm no op, no download, no state write.continue; both engine tests asserterr == nil.Rulings
git commit -s, carryingCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>— DCO non-negotiable, attribution trailer per session policy.golang.org/x/text/casesinstead of plainstrings.ToLower, since spec §4 says "Detect and quarantine rather than clobber" and plain ToLower left non-ASCII twins unguarded (observed on this Mac's APFS volume).7787082(build failure, then behavioural failures, then all green) — satisfied.Deferred
report.pdf→Report.pdfon macOS) is quarantined every pass rather than propagated as a rename.Avsa) are flagged but their children still sync individually, so the skip message and behaviour don't fully match.Implemented and reviewed by Claude (subagent-driven), landed on main after review and green CI.