Summary
to_obsidian()'s safe_name() can emit a note whose filename is only punctuation (e.g. @.md) when a node label is all-punctuation after the unsafe-char strip (e.g. a @/* node extracted from a tsconfig.json paths entry). Such a filename is valid on disk, but any downstream tool that re-slugs the stem on word characters reduces it to empty and fails.
Concretely, it crashes qmd (the Quick Markdown Search CLI) on qmd update, because its handelize() reduces @ → "" and raises:
Error: handelize: path "graphify-out/obsidian/@.md" has no valid filename content
at handelize (.../@tobilu/qmd/dist/store.js:1493:15)
at reindexCollection (.../store.js:969:22)
Since qmd update indexes collections in a batch, this one generated file aborts the entire run — every other collection on the machine stops being reindexed too. Found on graphifyy==0.8.33.
Root cause
In graphify/export.py, safe_name() (used in both the node export and the community export of to_obsidian):
def safe_name(label: str) -> str:
cleaned = re.sub(r'[\\/*?:"<>|#^[\]]', "", label. ...).strip()
cleaned = re.sub(r"\.(md|mdx|qmd|markdown)$", "", cleaned, flags=re.IGNORECASE)
return _cap_filename(cleaned) if cleaned else "unnamed" # <-- "@" is truthy, slips through
For label @/*: the unsafe-char strip removes / and *, leaving @. @ is non-empty, so the else "unnamed" fallback never fires, and the stem becomes @ → file @.md.
Proposed fix
Require at least one word character; otherwise fall back (so an all-punctuation label can never produce a punctuation-only filename):
cleaned = re.sub(r"\.(md|mdx|qmd|markdown)$", "", cleaned, flags=re.IGNORECASE)
# A stem of only punctuation ("@", "*", "#") survives the unsafe-char strip but
# is empty once a downstream tool re-slugs on word chars (e.g. qmd's handelize()
# → crash). Require at least one word char; else fall back.
if not re.search(r"\w", cleaned, flags=re.UNICODE):
return "unnamed"
return _cap_filename(cleaned)
Applies to both occurrences of safe_name in export.py. The existing dedup logic already handles multiple unnamed collisions (unnamed_1, …).
Repro
A node whose label is all punctuation after the unsafe-char strip (e.g. @/* from a tsconfig.json paths key) → graphify-out/obsidian/@.md. Other @*.md files with a word char (@types/node → @typesnode.md) are fine; only the bare-punctuation case breaks.
Happy to send a PR if the fix looks right.
Summary
to_obsidian()'ssafe_name()can emit a note whose filename is only punctuation (e.g.@.md) when a node label is all-punctuation after the unsafe-char strip (e.g. a@/*node extracted from atsconfig.jsonpathsentry). Such a filename is valid on disk, but any downstream tool that re-slugs the stem on word characters reduces it to empty and fails.Concretely, it crashes
qmd(the Quick Markdown Search CLI) onqmd update, because itshandelize()reduces@→""and raises:Since
qmd updateindexes collections in a batch, this one generated file aborts the entire run — every other collection on the machine stops being reindexed too. Found ongraphifyy==0.8.33.Root cause
In
graphify/export.py,safe_name()(used in both the node export and the community export ofto_obsidian):For label
@/*: the unsafe-char strip removes/and*, leaving@.@is non-empty, so theelse "unnamed"fallback never fires, and the stem becomes@→ file@.md.Proposed fix
Require at least one word character; otherwise fall back (so an all-punctuation label can never produce a punctuation-only filename):
Applies to both occurrences of
safe_nameinexport.py. The existing dedup logic already handles multipleunnamedcollisions (unnamed_1, …).Repro
A node whose label is all punctuation after the unsafe-char strip (e.g.
@/*from atsconfig.jsonpathskey) →graphify-out/obsidian/@.md. Other@*.mdfiles with a word char (@types/node→@typesnode.md) are fine; only the bare-punctuation case breaks.Happy to send a PR if the fix looks right.