muse commit's snapshot directories field goes stale when a directory's last file is removed
muse commit's snapshot directories field goes stale when a directory's last file is removed
Found while fixing #95
While fixing #95 (empty directories left behind by switch/checkout/merge/pull), found a related but distinct bug: an ordinary muse commit that removes the last file in a directory does not update that commit's own snapshot directories field to drop the now-empty directory.
Reproduction
rm -rf /tmp/muse_stale_dirs && mkdir -p /tmp/muse_stale_dirs && cd /tmp/muse_stale_dirs
muse init --json
mkdir -p tools/plugin
echo "docs" > tools/plugin/README.md
muse code add . --json
muse commit -m "add tools/plugin" --json
rm tools/plugin/README.md
muse code add tools/ --json
muse commit -m "chore: remove stale tools/plugin" --json
# The commit's own manifest correctly excludes tools/plugin/README.md --
# but its directories field still lists it:
muse read --json --manifest | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['directories'])"
# → ['tools', 'tools/plugin'] -- WRONG, tools/plugin has zero files
Why this matters
This directly undermined the fix for #95: apply_manifest's new target_directories protection parameter is meant to let a genuinely intentional empty directory (e.g. one with a .musekeep marker) survive pruning. But if sourced from a commit's own stale directories field, it would incorrectly protect a directory that the SAME commit just emptied out — defeating the fix entirely for the fast-forward/merge path. The #95 fix worked around this by deriving target_directories from directories_from_manifest(target_manifest) (purely file-derived) everywhere instead of trusting any stored directories field — safe, but it means a genuinely intentional empty directory (one with only a .musekeep marker, no other files) is not actually protected by any of those call sites today, since directories_from_manifest cannot derive directories that have zero files.
Scope
Find where muse commit constructs the snapshot's directories field (muse/cli/commands/commit.py, plugin.snapshot() in muse/plugins/code/plugin.py) and confirm whether it's a fresh walk each time or inherits/merges with a prior value. Fix so a directory that loses its last file (and has no .musekeep marker) is correctly dropped from directories at commit time, not just left stale until some unrelated future snapshot recomputes it.
Acceptance criteria
- The reproduction above shows
directories: [](or withouttools/tools/plugin) after the second commit. .musekeep-marked intentionally-empty directories are unaffected — they continue to appear indirectoriesfor as long as the marker file exists.- Once fixed, revisit #95's fix: the
target_directoriescall sites that derive fromdirectories_from_manifest(target_manifest)could then safely switch to using a loadedSnapshotRecord.directoriesdirectly where one is already in scope, correctly protecting genuinely-intentional empty directories that the current file-derived approach cannot see.