π What
ScanService walks Library:MediaRoot with Directory.EnumerateFiles(root, β¦, RecurseSubdirectories = true). When the root contains a directory symlink that points at another directory under the same root (root/link β root/inner), every file under inner/ is yielded twice β once as root/link/x.mp3, once as root/inner/x.mp3. The catalog's identity is the path, so the second path is classified discovered and becomes a ghost row of the same physical file (the gh-#610 shape, one file at a time).
Found 2026-08-30 while proving F154.6 for T380 (the Gardener's file actions): a move into root/link left the row at the link path and the next tick reported 1 discovered. The planner now refuses moves into symlinked directories (FileActionRule.SymlinkedTarget), but the scan itself still double-counts any pre-existing alias.
π οΈ Options
- Enumerate with
AttributesToSkip |= ReparsePoint (skip directory symlinks entirely β simplest; an aliased tree is then reachable only by its real path).
- Resolve each yielded path with
ResolveLinkTarget(returnFinalTarget: true) and dedupe on the real path (keeps link-only layouts working; costs a stat per file).
- Document "no internal symlink aliases under MediaRoot" in DEPLOYMENT and leave the scan alone.
Fact pinning the current truth: Story379_FileActionExecutors.cs (AScanTickAfterTheSymlinkedMoveDiscoversAGhostDuplicateOfTheSamePhysicalFile).
https://claude.ai/code/session_01AmM47neuvsJph3ZT4brTCV
π What
ScanServicewalksLibrary:MediaRootwithDirectory.EnumerateFiles(root, β¦, RecurseSubdirectories = true). When the root contains a directory symlink that points at another directory under the same root (root/link β root/inner), every file underinner/is yielded twice β once asroot/link/x.mp3, once asroot/inner/x.mp3. The catalog's identity is the path, so the second path is classifieddiscoveredand becomes a ghost row of the same physical file (the gh-#610 shape, one file at a time).Found 2026-08-30 while proving F154.6 for T380 (the Gardener's file actions): a move into
root/linkleft the row at the link path and the next tick reported 1 discovered. The planner now refuses moves into symlinked directories (FileActionRule.SymlinkedTarget), but the scan itself still double-counts any pre-existing alias.π οΈ Options
AttributesToSkip |= ReparsePoint(skip directory symlinks entirely β simplest; an aliased tree is then reachable only by its real path).ResolveLinkTarget(returnFinalTarget: true)and dedupe on the real path (keeps link-only layouts working; costs a stat per file).Fact pinning the current truth:
Story379_FileActionExecutors.cs(AScanTickAfterTheSymlinkedMoveDiscoversAGhostDuplicateOfTheSamePhysicalFile).https://claude.ai/code/session_01AmM47neuvsJph3ZT4brTCV