Skip to content

explain: fails to match labels containing punctuation (asymmetric normalization vs norm_label) #1704

Description

@ryanhelms

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions