Skip to content

fix(export): handle APFS unicode filename collisions - #2379

Open
Rhyoss wants to merge 3 commits into
Graphify-Labs:v8from
Rhyoss:fix/2282-apfs-unicode-complete
Open

fix(export): handle APFS unicode filename collisions#2379
Rhyoss wants to merge 3 commits into
Graphify-Labs:v8from
Rhyoss:fix/2282-apfs-unicode-complete

Conversation

@Rhyoss

@Rhyoss Rhyoss commented Aug 1, 2026

Copy link
Copy Markdown

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:

  • uv run pytest tests/test_export.py -k 'unicode_equivalent or case_collision' -q
  • uv run pytest -q
  • uv run ruff check graphify/export.py tests/test_export.py

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.

Rishet11 and others added 3 commits August 1, 2026 02:56
…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.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 idsgraphify/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 IDsgraphify/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 connectedgraphify/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.

Comment thread graphify/export.py
return False


def to_obsidian(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionto_obsidian()

fans out to 8 callees (efferent coupling); 31 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants