Summary
graphify explain "<label>" fails to find a node whose stored label contains
punctuation that \w+ doesn't match (e.g. ., -), even when the label is
typed exactly as stored and other commands (path, and presumably query)
resolve the identical string correctly. Root cause is an asymmetry between how
the query term is tokenized and how norm_label is stored on the node.
Repro
Given a graph containing a file node with label: "blockStream.ts" /
norm_label: "blockstream.ts":
$ graphify explain "blockStream.ts"
No node matching 'blockStream.ts' found.
$ graphify explain "blockStream.test.ts"
No node matching 'blockStream.test.ts' found.
$ graphify explain "BlockStream" # a different node (a class), no punctuation
Node: BlockStream
ID: lib_blockstream_blockstream
...
But the exact same literal string resolves fine through path:
$ graphify path "blockStream.ts" "_stream.py"
Shortest path (3 hops):
blockStream.ts <--imports_from-- blockStream.test.ts --shares_data_with--> test_stream.py --imports_from--> _stream.py
Root cause
graphify/serve.py::_find_node (used by the explain subcommand) builds its
search term with:
def _search_tokens(text: str) -> list[str]:
return re.findall(r"\w+", _strip_diacritics(str(text)).lower())
term = " ".join(_search_tokens(label))
For label = "blockStream.ts", \w+ splits on the ., giving tokens
["blockstream", "ts"], so term == "blockstream ts" (space where the period
was).
Meanwhile the node's stored norm_label (written at graph-build time) is
"blockstream.ts" — lowercased, but punctuation left intact, not tokenized.
_find_node's three-tier match (term == norm_label, norm_label.startswith(term),
term in norm_label) then never succeeds, because "blockstream ts" (with a
space) is never equal to, a prefix of, or a substring of "blockstream.ts"
(with a period) — there's no space character in the stored label at all.
Confirmed directly against a graph.json:
{"id": "lib_blockstream", "label": "blockStream.ts", "norm_label": "blockstream.ts"}
{"id": "lib_blockstream_test", "label": "blockStream.test.ts", "norm_label": "blockstream.test.ts"}
explain "BlockStream" works only because that label has no \w-breaking
punctuation, so _search_tokens produces a single token and the asymmetry
never surfaces.
By contrast, the path subcommand's source/target resolution uses a
different, apparently fuzzy/scored matcher (src_scored/tgt_scored in
__main__.py's path handler) that tolerates this and resolves the same
literal string correctly — so this is specific to explain's resolver, not a
graph-data problem.
Suggested fix
Make _find_node's normalization symmetric with how norm_label is stored —
either:
- tokenize
norm_label the same way at comparison time (e.g. also run it
through _search_tokens and join with a space) before comparing, or
- stop replacing punctuation with a space in the query term (just lowercase +
strip diacritics, matching how norm_label is actually produced).
Alternatively, route explain's node resolution through the same
fuzzy/scored matcher path already uses, so behavior is consistent across
subcommands given the same input label.
Environment
graphifyy 0.8.36 (pipx install)
- Reproduced against a ~92k-node graph built from a real monorepo
(graphify update . / full /graphify . pipeline)
Summary
graphify explain "<label>"fails to find a node whose stored label containspunctuation that
\w+doesn't match (e.g..,-), even when the label istyped exactly as stored and other commands (
path, and presumablyquery)resolve the identical string correctly. Root cause is an asymmetry between how
the query term is tokenized and how
norm_labelis stored on the node.Repro
Given a graph containing a file node with
label: "blockStream.ts"/norm_label: "blockstream.ts":But the exact same literal string resolves fine through
path:Root cause
graphify/serve.py::_find_node(used by theexplainsubcommand) builds itssearch term with:
For
label = "blockStream.ts",\w+splits on the., giving tokens["blockstream", "ts"], soterm == "blockstream ts"(space where the periodwas).
Meanwhile the node's stored
norm_label(written at graph-build time) is"blockstream.ts"— lowercased, but punctuation left intact, not tokenized._find_node's three-tier match (term == norm_label,norm_label.startswith(term),term in norm_label) then never succeeds, because"blockstream ts"(with aspace) is never equal to, a prefix of, or a substring of
"blockstream.ts"(with a period) — there's no space character in the stored label at all.
Confirmed directly against a
graph.json:{"id": "lib_blockstream", "label": "blockStream.ts", "norm_label": "blockstream.ts"} {"id": "lib_blockstream_test", "label": "blockStream.test.ts", "norm_label": "blockstream.test.ts"}explain "BlockStream"works only because that label has no\w-breakingpunctuation, so
_search_tokensproduces a single token and the asymmetrynever surfaces.
By contrast, the
pathsubcommand's source/target resolution uses adifferent, apparently fuzzy/scored matcher (
src_scored/tgt_scoredin__main__.py'spathhandler) that tolerates this and resolves the sameliteral string correctly — so this is specific to
explain's resolver, not agraph-data problem.
Suggested fix
Make
_find_node's normalization symmetric with hownorm_labelis stored —either:
norm_labelthe same way at comparison time (e.g. also run itthrough
_search_tokensand join with a space) before comparing, orstrip diacritics, matching how
norm_labelis actually produced).Alternatively, route
explain's node resolution through the samefuzzy/scored matcher
pathalready uses, so behavior is consistent acrosssubcommands given the same input label.
Environment
graphifyy0.8.36 (pipx install)(
graphify update ./ full/graphify .pipeline)