ADR-016: File selection and path guard¶
Status¶
Accepted.
Decision¶
D1. The guard is src/obelize/fsutil.py, and it ships with T2.
refusal(root, candidate) returns why a path must not be opened, or None.
The walker asks it before reading and fsutil's writer before writing, so both
get one answer without transforms/ importing the scanner.
D2. git's listing is used only when git rev-parse --show-toplevel resolves
to the root itself. Otherwise the fallback walk runs, so a directory inside
another checkout, such as a fixture tree, never gets that checkout's index and
.gitignore.
D3. A user's exclude prunes no directory. Only the always-excluded set
does. exclude filters files, even a list with no ! pattern, because
ADR-010 F-2 names every excluded file that still imports the legacy
distribution, and a pruned directory cannot be named file by file.
D4. Every refusal is a published reason. SCAN_VOCABULARY.md ยง11's seven
values, closed and asserted against obelize.models both ways, so the evidence
writer invents none; run.json's limitations[].code is a superset. A name
that is not valid UTF-8 is refused as unusable_name, never scanned mangled.
D5. The walker opens nothing. It lists, stats and matches; the prefilter,
max_file_bytes and the parse gates belong to the reader, so a file is read
once. Walk.unincluded names the paths include did not take that can hold
Python (*.ipynb, *.pyi, *.pyw): a stub and a .pyw are not analysed, a notebook's code cells
are read and never written, and F-2 prefilters their bytes
(ADR-021 D3 and D14).
D6. A listed path that is not on disk is missing, not an escape. An
ordinary rm of a tracked file leaves its name in the index; calling it
outside_root would turn a security signal into noise.
D7. A root that is not a readable directory exits 2. A missing root, a
file, or a directory that cannot be searched raises ConfigError, as an
unusable .obelize.yml does (ADR-015 D1): the root is part of how the command
was invoked. A root that can be searched but not listed is walked, and reported
unreadable.
Consequences¶
- This is TM-2's read half: containment with both sides resolved, a separate
symlink guard,
followlinks=False, a mandatoryis_file(), and submodules pruned and reported. - The two listings disagree about gitignored files on purpose, and git is
right; only the fallback walk prunes a directory holding
pyvenv.cfg. - Under the default
include(**/*.py) the guard is never asked about a submodule or a linked directory;include: "**/*"hands both over. tests/fixtures/walker/_build.pybuilds the fixture tree and its answer key, since a nested.git, a symlink loop and an unreadable directory cannot be committed.- Unmeasured: a tree where most files are rejected, where the guard's
resolveruns and the parse does not.