Skip to content

Node identity derived from source_file silently drops the defining page's own node #3352

Description

@PrettyGarbage

Summary

When two files mention the same entity, graphify keeps the node from whichever file was extracted first and drops the other:

[graphify] WARNING: node 'X' is minted by two different files — keeping 'X' from 'A.md',
dropping 'X' from 'B.md'. ... the dropped node is lost.

The practical effect is that the page that actually defines a concept can lose its own node, and the surviving node is attributed to a file that merely mentions the concept in passing.

Concrete case

Corpus: an Obsidian vault of ~60 markdown notes, graphify extract wiki/ --mode deep.

Page that defines the concept concepts/작업 단위 폴더 + README 진입점 컨벤션.md (filename == concept name)
Page that only mentions it concepts/LLM Schema - CLAUDE.md를 가이드로 활용하기.md
Node's source_file in graph.json the mentioning page
Nodes attributed to the defining page none

The concept itself is in the graph with 4 edges — only the ownership is wrong.

Why it matters

The documented navigation pattern is "read GRAPH_REPORT.md → identify relevant nodes → fetch only those source files". When source_file points at a page that merely mentions the concept, that workflow routes the reader to the wrong file. The authoritative page is invisible to the graph.

The documented workaround does not apply here

The warning suggests:

To keep them distinct, run 'graphify extract' per subfolder and merge with 'graphify merge-graphs'.

Both files above live in the same folder (concepts/), so splitting by subfolder cannot separate them.

Post-hoc correction is also blocked

Rewriting source_file in graph.json to point at the defining page does not work, because node ids are re-derived from it — build.py::_semantic_id_remap:

the new id is computed from source_file in code, never trusted from the fragment's own id string

So rewriting source_file causes ids to be re-derived, collide with other nodes, and get silently merged — reproducing the very problem. The resulting graph shrank 146 → 145 nodes, which then tripped the shrink guard in export.py:

[graphify] WARNING: new graph has 145 nodes but existing graph.json has 146 (net -1).
Refusing to overwrite.
graph.json NOT written: ... GRAPH_REPORT.md ... left untouched.

Both graphify label and graphify cluster-only refuse to write, so GRAPH_REPORT.md can no longer be regenerated and goes stale. GRAPHIFY_FORCE=1 is honored by extract but not by label / cluster-only, so there is no CLI escape hatch (the warning text says "pass force=True", which is only reachable from the Python API).

Suggestion

When resolving a mint collision, prefer the file whose name matches the entity name as the owner — or more generally, prefer a "defining" page over a "mentioning" one — rather than first-extracted-wins. Failing that, exposing a --force flag on label / cluster-only would at least let users recover a stale report.

Environment

  • graphify 0.9.53 (pipx install graphifyy, plus pipx inject graphifyy openai for the gemini backend)
  • macOS, Python 3.14.7
  • Backend: gemini
  • Corpus: ~60 markdown docs, no code files

Activity

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

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