Summary
detect_incremental uses current_mtime > stored (a strict greater-than) on the legacy manifest schema and does no content-hash verification. Files whose mtime moved backwards — after git checkout <older-commit>, tar -xf restore, rsync --times, or a manual timestamp adjustment — are silently treated as unchanged and never re-extracted. The graph then reflects the newer content while the corpus on disk holds the older content.
The parallel dict-schema branch handles this correctly (!= + hash-verify). The legacy-schema branch does not.
Where
graphify/detect.py:1583-1610:
# Legacy manifest: plain float value — treat as ast_hash only
if isinstance(stored, (int, float)):
changed = stored is None or current_mtime > stored
elif isinstance(stored, dict):
# …
if stored_mtime is None or current_mtime != stored_mtime:
# mtime bumped — verify with content hash before re-extracting
changed = _md5_file(Path(f)) != stored_hash
else:
changed = False
Two issues in the float branch:
> misses backwards mtime motion. git checkout <older-commit> restores older file timestamps; those files' current_mtime < stored and the check reports changed = False. The stored graph is now stale relative to what's on disk. The dict branch's != catches this.
stored is None is dead code. The enclosing isinstance(stored, (int, float)) already excludes None.
The legacy branch also skips the _md5_file verification the dict branch performs, so even when the float mtime does differ it re-extracts without checking whether the AST hash actually changed — the reverse cost of the same asymmetry.
Reproduction
- Build a graph from a clean checkout at commit
A.
- Copy the manifest into legacy form (single float mtime per file — the shape written by graphify prior to the dict-schema migration).
git checkout <older-commit-B> where at least one tracked file's content differs and its mtime is older than at A.
- Run
graphify update (which calls detect_incremental).
- Observe:
new_files does not contain the reverted file; its cached AST from A is retained. The graph now describes commit A's version of a file whose bytes are commit B.
Suggested fix
Align the float branch with the dict branch: compare with != and content-hash-verify before re-extracting:
if isinstance(stored, (int, float)):
if current_mtime != stored:
# mtime moved (either direction) — verify with content hash
changed = _md5_file(Path(f)) != "" # legacy schema stored no hash;
# re-extract on any mtime change since we have no hash to compare against
changed = True if current_mtime != stored else False
else:
changed = False
Or, cleaner — normalise legacy floats up-front (already done in the dict branch for the {mtime, hash} shape) so a plain float is treated as {"mtime": stored, "ast_hash": "", "semantic_hash": ""} and falls through to the dict logic, which then correctly re-extracts (empty stored_hash → changed = True) and rewrites the entry in the new schema on the next save.
Happy to open a PR with either shape and a regression test that emulates a backwards-mtime checkout.
Summary
detect_incrementalusescurrent_mtime > stored(a strict greater-than) on the legacy manifest schema and does no content-hash verification. Files whose mtime moved backwards — aftergit checkout <older-commit>,tar -xfrestore,rsync --times, or a manual timestamp adjustment — are silently treated as unchanged and never re-extracted. The graph then reflects the newer content while the corpus on disk holds the older content.The parallel dict-schema branch handles this correctly (
!=+ hash-verify). The legacy-schema branch does not.Where
graphify/detect.py:1583-1610:Two issues in the float branch:
>misses backwards mtime motion.git checkout <older-commit>restores older file timestamps; those files'current_mtime < storedand the check reportschanged = False. The stored graph is now stale relative to what's on disk. The dict branch's!=catches this.stored is Noneis dead code. The enclosingisinstance(stored, (int, float))already excludesNone.The legacy branch also skips the
_md5_fileverification the dict branch performs, so even when the float mtime does differ it re-extracts without checking whether the AST hash actually changed — the reverse cost of the same asymmetry.Reproduction
A.git checkout <older-commit-B>where at least one tracked file's content differs and its mtime is older than at A.graphify update(which callsdetect_incremental).new_filesdoes not contain the reverted file; its cached AST from A is retained. The graph now describes commit A's version of a file whose bytes are commit B.Suggested fix
Align the float branch with the dict branch: compare with
!=and content-hash-verify before re-extracting:Or, cleaner — normalise legacy floats up-front (already done in the dict branch for the
{mtime, hash}shape) so a plain float is treated as{"mtime": stored, "ast_hash": "", "semantic_hash": ""}and falls through to the dict logic, which then correctly re-extracts (empty stored_hash →changed = True) and rewrites the entry in the new schema on the next save.Happy to open a PR with either shape and a regression test that emulates a backwards-mtime checkout.