fix(export): handle APFS unicode filename collisions - #2379
Conversation
…nsensitive filesystems (Graphify-Labs#2282) _owned_write compared target.exists() — which is case-insensitive on APFS and NTFS — against an exact-string lookup in the JSON manifest, so a note graphify itself wrote as AGORA.md looked pre-existing when a later run computed agora.md. The write was refused as a user file and the stale-prune step, which saw the name in neither _written nor _skipped, then deleted the original: two nodes, one note, plus a false warning. Probe once per to_obsidian call whether the output directory is actually case-insensitive (assuming case-sensitive when the probe cannot run, which preserves today's Linux behavior where Agora.md and agora.md are genuinely two files), and key the ownership comparison and the stale-prune through that. The manifest still stores real filenames. _dedup_node_filenames now iterates in sorted node order so suffix assignment does not drift between runs for an unchanged node set, which is what made the collision fire. The pre-existing-file protection is unchanged for a genuinely user-authored file: it is still skipped and still warned about.
…em case-sensitivity The four new Graphify-Labs#2282 tests forced the case-insensitive probe to True unconditionally, which is incoherent on Linux/ext4 (a real case-sensitive fs) and caused CI failures there. Compute the real probe result once and branch expectations on it instead of contradicting it, gate the case-sensitive-only scenarios with skipif, and make the user-file collision test use the exact filename graphify computes so it's collision on every filesystem.
There was a problem hiding this comment.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify review — findings
This PR removes the tier-aware merge and non-string-id coercion machinery from graphify/build.py, deleting helpers like _is_ast_tier, _coerce_id, and _coerce_non_string_ids, and reverting related call sites to a stricter _origin == "ast" check. It also strips the corresponding unreleased 0.9.32 entries from the CHANGELOG and updates README wording to point to a waitlist instead of early-access links. The touched surface spans build/dedup/extract/detect/watch/export/serve modules plus their associated tests (detect, watch, build, export, ruby resolution). Reviewers should note this appears to walk back a set of previously described incremental-extraction, id-coercion, and language-resolution changes rather than add new functionality.
Worth a look
- Removed numeric id coercion re-exposes TypeError/'<' crash on non-string ids —
graphify/build.py:549· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- build_from_json no longer accepts numeric node IDs —
graphify/build.py:549· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- build_from_json no longer keeps numeric edge endpoints connected —
graphify/build.py:549· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3348 functions depend on the 1806 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
to_obsidian()— 31 callers, 8 callees
Verification — 3348 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 3127 function(s) in the blast radius were not formally verified this run
· 1 grounded finding(s) anchored inline below.
| return False | ||
|
|
||
|
|
||
| def to_obsidian( |
There was a problem hiding this comment.
to_obsidian()
fans out to 8 callees (efferent coupling); 31 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Completes #2282 on APFS/macOS by applying one filesystem-equivalence key across Obsidian filename dedupe, manifest ownership, and stale pruning.
This follows #2357: that PR fixes case-only collisions such as AGORA.md/agora.md. This follow-up covers Unicode-equivalent filenames such as Café in NFC vs NFD, which APFS resolves to the same path.
Validation:
Note: uv run ruff format --check wants to reformat existing unrelated sections in these files, so I left formatting untouched to keep the diff focused.