Purpose
-------
One recursive-remove that survives a read-only directory, for every ``oct``
command that deletes a generated tree.
The defect this replaces: ``shutil.rmtree(path)`` raises
``PermissionError: [WinError 5] Access is denied`` when a directory carries the
Windows read-only attribute, because ``os.rmdir`` refuses it. Its
``ignore_errors=True`` form is no better -- it swallows the refusal and leaves
the tree standing while the caller reports success. On 2026-09-07 that let
``oct clean`` count 947 surviving directories as removed; on 2026-09-25 the
bare form ended ``oct docs --all`` in a traceback out of
``run_sphinx_build``'s pre-build clean.
The attribute is not exotic here (ENV-03): a Google Drive or OneDrive mirror
sets it on every directory it syncs, so an entire checkout under a mirror root
has it. Clearing the mirror is the real remedy and stays an operator action;
this module is what makes the tools survive until then.
Responsibilities
----------------
- :func:`remove_tree_force` -- remove a tree, clearing the read-only attribute
on whatever refuses, and report whether it is really gone.
- :func:`remove_tree_or_warn` -- the same, plus the one standard warning, so
six call sites do not word the same failure six ways.
- :data:`READ_ONLY_HINT` -- that wording, in one place.
Diagnostics
-----------
Domain: OCT-CORE
Levels:
L1 -- a tree that is still standing after the retry
L2 -- lifecycle (a tree removed)
L3 -- semantic details (the path, and the retry firing)
L4 -- deep tracing
Contracts
---------
- The verdict is the filesystem's, never the call's: the return value is a
fresh existence check, so a swallowed error cannot read as success. This is
the property ``ignore_errors=True`` lacks and the reason it is not used here.
- Failure is never raised. Every caller here is deleting generated output as
one step of a longer pipeline, and an unremovable cache directory must not
end the run with a traceback.
- ``False`` does NOT mean nothing was deleted. A recursive delete works
bottom-up, so a refusal at the final ``rmdir`` has already taken the files
and leaves the directory shell behind. A caller that plans to rebuild into
the path should copy or build into what remains (``exist_ok`` /
``dirs_exist_ok``) rather than treat the target as still intact.
- The attribute is cleared only on a path that has already refused removal and
only to delete it. Nothing is made writable and left behind.
- ``target`` that does not exist is success -- the postcondition ("it is not
there") already holds.
- stdlib plus the ``_dbg`` shim only; imports nothing of ``oct``'s beyond
diagnostics, so any module may call it.
Dependencies
------------
- oct.core.diagnostics (the ``_dbg`` shim)