Skip to content

Obsidian export: .obsidian/graph.json colorGroups are built from the raw community label, so any community with a non-tag-safe character gets a dead color group #2204

Description

@irvinolave-alt

Found on graphifyy 0.9.26 (uv tool install, Python 3.11, Linux), running graphify <dir> --backend claude-cli followed by graphify export obsidian --graph graphify-out/graph.json --labels graphify-out/.graphify_labels.json --dir <vault> on a small mixed corpus (4 code files + 2 markdown docs, Spanish source, 54 nodes / 78 edges / 18 communities).

Summary

to_obsidian() sanitizes community names with _obsidian_tag() when it writes the tag into each note, but writes the .obsidian/graph.json colorGroups query from label.replace(' ', '_') — unsanitized. The two disagree for any community name containing a character outside [a-zA-Z0-9_\-/], and that colorGroup silently matches zero notes. The community renders uncolored in graph view while its siblings are colored, with no warning printed.

Where

graphify/export.py

# line 99 — used for the tag inside each note
def _obsidian_tag(name: str) -> str:
    return re.sub(r"[^a-zA-Z0-9_\-/]", "", name.replace(" ", "_"))

# line 735 — used for the graph-view color group. Not sanitized.
"query": f"tag:#community/{label.replace(' ', '_')}",

Reproduction

Any corpus where the community labeler produces a name with &, +, (, ) or a diacritic. In my run, community Product & Vale Lookup:

  • note frontmatter and inline tag: #community/Product__Vale_Lookup
  • .obsidian/graph.json: "query": "tag:#community/Product_&_Vale_Lookup"

Standalone check of the two code paths:

import re
def obsidian_tag(n): return re.sub(r'[^a-zA-Z0-9_\-/]', '', n.replace(' ', '_'))
def colorgroup(n):   return n.replace(' ', '_')

for name in ['Product & Vale Lookup', 'C++ Parser', 'Auth (v2)', 'Créditos', 'I/O Layer']:
    print(f'{name!r:24} note={obsidian_tag(name):22} config={colorgroup(name):22} '
          f'{"ok" if obsidian_tag(name) == colorgroup(name) else "MISMATCH"}')
'Product & Vale Lookup'   note=Product__Vale_Lookup   config=Product_&_Vale_Lookup   MISMATCH
'C++ Parser'              note=C_Parser               config=C++_Parser              MISMATCH
'Auth (v2)'               note=Auth_v2                config=Auth_(v2)               MISMATCH
'Créditos'                note=Crditos                config=Créditos                MISMATCH
'I/O Layer'               note=I/O_Layer              config=I/O_Layer               ok

Four of five realistic community names break. & in particular is very common in LLM-generated community names (Product & Vale Lookup, Auth & Session, Parsing & Validation).

Expected

Every colorGroup query matches the tag actually written into the notes.

Suggested fix

One line — reuse the existing sanitizer:

"query": f"tag:#community/{_obsidian_tag(label)}",

A regression test asserting that every colorGroups[].query in the emitted .obsidian/graph.json matches at least one #community/... tag present in the emitted notes would pin this permanently, and would also catch the reverse drift if _obsidian_tag ever changes.

Note on diacritics

Créditos → Crditos is a separate cosmetic wart of _obsidian_tag (it deletes accented characters rather than transliterating them). Obsidian accepts Unicode letters in tags, so Créditos would be a legal tag as-is. _strip_diacritics() already exists in the same module. Not filing separately — mentioning in case you want to widen the allowed set while touching this function.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions