fix(inbox): retire the orphaned index when a directory's last segment goes
Retiring a directory's last concept left its index.md standing, so the directory survived a rebuild that never creates it -- diff -r reports it as 'Only in ...'. Found by S7 once its fixture was made to actually retire a segment: with round 2 a superset of round 1, S7 stayed green with retirement disabled entirely.
This commit is contained in:
parent
91efd92612
commit
894ccdf42f
1 changed files with 29 additions and 11 deletions
|
|
@ -310,21 +310,37 @@ def _owned_concepts_by_source(bundle: Path, profile: BundleProfile) -> dict[str,
|
|||
return owned
|
||||
|
||||
|
||||
def _retire_stale_segments(bundle: Path, stale: set[str]) -> None:
|
||||
def _retire_stale_segments(bundle: Path, stale: set[str], profile: BundleProfile) -> None:
|
||||
"""Delete concepts this round's plan no longer names, and prune what empties.
|
||||
|
||||
An emptied directory is deleted too: a scratch rebuild never creates it, so
|
||||
leaving it behind is exactly the kind of one-sided difference `diff -r`
|
||||
reports and S7 refuses.
|
||||
An emptied directory is deleted too, and so is the index left standing in
|
||||
it. A scratch rebuild writes an index only where a concept lives, so an
|
||||
orphaned one is exactly the kind of one-sided difference `diff -r` reports
|
||||
as `Only in ...` -- measured 2026-09-01, when retiring a directory's last
|
||||
concept left its index behind and S7 caught the divergence.
|
||||
|
||||
The emptiness test is "holds no concept anywhere BENEATH it", not "holds no
|
||||
files": a directory whose own concepts are gone may still be an ancestor of
|
||||
ones that remain, and deleting its index would break the walk from the root.
|
||||
"""
|
||||
for relative in sorted(stale):
|
||||
target = bundle / relative
|
||||
target.unlink(missing_ok=True)
|
||||
(bundle / relative).unlink(missing_ok=True)
|
||||
|
||||
suffix = profile.paths.concept_suffix
|
||||
# Deepest first, so a parent is judged only after its children are gone.
|
||||
for relative in sorted(stale, key=lambda item: item.count("/"), reverse=True):
|
||||
parent = (bundle / relative).parent
|
||||
while parent != bundle and parent.is_dir() and not any(parent.iterdir()):
|
||||
parent.rmdir()
|
||||
parent = parent.parent
|
||||
directory = (bundle / relative).parent
|
||||
while directory != bundle and directory.is_dir():
|
||||
if any(
|
||||
path.is_file() and path.name != profile.index.name
|
||||
for path in directory.rglob(f"*{suffix}")
|
||||
):
|
||||
break
|
||||
(directory / profile.index.name).unlink(missing_ok=True)
|
||||
if any(directory.iterdir()):
|
||||
break
|
||||
directory.rmdir()
|
||||
directory = directory.parent
|
||||
|
||||
|
||||
def _check_segment_path(path: str) -> str:
|
||||
|
|
@ -711,7 +727,9 @@ def process_inbox(
|
|||
# Round N owns exactly what round N's plan names. Everything this
|
||||
# document owned before and does not now is retired HERE, after the
|
||||
# writes, so a failure above leaves the previous round intact.
|
||||
_retire_stale_segments(bundle, owned_by_source.get(path.name, set()) - set(targets))
|
||||
_retire_stale_segments(
|
||||
bundle, owned_by_source.get(path.name, set()) - set(targets), profile
|
||||
)
|
||||
|
||||
# §6 index — the last disk mutation, and only when something was written.
|
||||
if persisted:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue