88ef99016e
`ls -R` on a directory holding a nested mount omitted the mount's own row while still printing its group, so `ls -R /base` reported `top.txt` and then `/base/nested:` for a mountpoint it had never named. GNU (coreutils 9.7, pinned over a real tmpfs) lists a mountpoint as an ordinary directory entry of its parent, `-R` or not. The generic conflated two different things: listing a mount root as a row, and descending into it. `-R` withheld the whole `child_mounts` merge on a backend-served listing, on the theory that the cross-mount fan-out contributed the nested mount whole. It contributes the group, not the parent's row, so the row survived only by accident -- whenever the parent's own backend happened to hold a key of that name. Splitting the two: the merge is unconditional, and descent stops at a mount root, read from the mount table (`MountView.is_root`) rather than inferred from `child_mounts` being empty. A namespace-only directory *above* a mount root is still descended, because nothing else renders it. Same cause, three more shapes. `ls -R /` dropped `ghost` and both the `/ghost:` and `/ghost/very:` groups whenever a backend served `/`; `/base/sub`'s group dropped a mount at `/base/sub/deep`; and a relative operand leaked shadowed content, printing `base/inner:` twice -- once with the parent's hidden `leftover.txt`, once with the mount's own listing -- because `_drop_shadowed_ls_groups` only recognizes an absolute header. That group is now never produced rather than produced and filtered. So `ls` takes `mounts` beside `links` and `child_mounts`, and it names the boundaries *this walk's readdir cannot cross*, not the boundaries that exist. The distinction is the whole contract, because the two entry points differ: `ls_generic` is bound to one backend and stops there, while the cross-mount relay's readdir routes per path, crosses a boundary itself, and must -- nothing runs behind a relay to contribute that group the way the fan-out does. Python said this by omission already, calling the generic with explicit keywords; the TypeScript relay hands the generic a whole `CommandOpts`, so it now builds one carrying every name-plane fact except the boundaries. Without that, `ls -R /base /other` dropped `/base/inner:` entirely. Two unit tests that passed a bare `child_mounts` now supply a mount table, since the fact moved to its real source, and both relay suites gain a case that hands the relay a full namespace. `integ/crossmount/nested/basic.json` seq 950006 pinned the bug; it now pins GNU's answer. A mountpoint is not always a directory, so the row is stat'd, not synthesized. Every workspace mounts `/.bash_history` as a whole mount serving one file, and a hardcoded `FileType.DIRECTORY` made `ls -aF /` print `.bash_history/` and `ls -l` render it `drwxr-xr-x`. No backend can answer that question -- the parent's cannot see into the child, and the child's own names its root `/` -- so it goes to the dispatcher via `opts.stat_path`, which routes to whichever mount owns the path, and the row is renamed the way the relay renames one. Directory stays the fallback for a caller with no dispatcher, which is the only thing that absence can mean. The same fact settles what `-R` descends: the ls fan-out no longer emits a block for a mount that is not a directory, which otherwise printed `.bash_history` a second time as its own group. Only a *confirmed* non-directory is dropped, so a root the dispatcher cannot stat keeps its block rather than vanishing on a failed probe. GNU is pinned the same way, over a `mount --bind` of one file onto another: one ordinary row in the parent, no group of its own.