From 072698a1762b240f4d2c51b1e9c134647a1da11a Mon Sep 17 00:00:00 2001 From: Sfahad7 Date: Mon, 27 Jul 2026 03:33:08 +0530 Subject: [PATCH] fix(detect): NFC both sides of relpath for portable manifests on macOS Stamped keys can already be NFC while Path(root).resolve() is NFD, so os.path.relpath treated in-root files as ../ and kept them absolute. Normalize both operands inside _to_relative_for_storage (and the join in _to_absolute_from_storage). --- graphify/detect.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/graphify/detect.py b/graphify/detect.py index 8fc6766..d243641 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -1486,12 +1486,18 @@ def _to_relative_for_storage(key: str, root: Path) -> str: under its own name. Resolving the key would point the stored entry at the symlink target, and the original key would then miss on reload and re-extract on every incremental run. + + Both sides of ``relpath`` are NFC'd first: stamped keys may already be + NFC while ``Path(root).resolve()`` is NFD on macOS, and a mixed-form + compare would mark an in-root file as ``../…`` and keep it absolute + (#2221 / #777). """ p = Path(key) if not p.is_absolute(): return key try: - rel = os.path.relpath(p, Path(root).resolve()) + base = _nfc(str(Path(root).resolve())) + rel = os.path.relpath(_nfc(str(p)), base) except (ValueError, OSError): return key # outside root (e.g. Windows cross-drive) # ``os.path.relpath`` happily produces ``../foo`` for paths outside @@ -1510,11 +1516,15 @@ def _to_absolute_from_storage(key: str, root: Path) -> str: that newly-loaded manifests from before this change remain readable. Uses ``Path(root).resolve()`` so the produced absolute path matches what :func:`detect` returns (which also resolves the scan root). + NFC both sides so a relative key and an NFD-resolved root still join + to the same string form the rest of the manifest path uses (#2221). """ p = Path(key) if p.is_absolute(): return str(p) - return str(Path(root).resolve() / p) + # NFC the joined result so an NFD-resolved root + relative key lands on + # the same form load_manifest / detect_incremental compare against. + return _nfc(str(Path(root).resolve() / p)) def load_manifest(