You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
symbol_resolution.normalise_callable_label (line 31) lowercases all labels
before building the call-resolution index. In Ruby — where casing has
semantic meaning (Foo is a class, foo is a method) — this collapses
distinct symbols into one bucket and emits high-confidence INFERRED calls
edges that don't reflect the source code.
I hit this on a Rails 8 / Packwerk codebase where Pundit's lowercase authorize record helper is called from every controller action, and the
codebase has a separate marker class Api::Guards::Authorize. The
case-insensitive resolver concluded that every .create() / .update() /
etc. action calls the marker class — 188 spurious INFERRED edges at
score 0.8, all to the same target.
Minimal repro
# app/lib/api/guards/authorize.rbmoduleApi::GuardsclassAuthorize < Basedefcall(_context)=Result.allowendend# app/controllers/foo_controller.rb (inherits from BaseController which# declares `guard Api::Guards::Authorize` at the class level)classFooController < BaseControllerdefcreatefoo=Foo.create!(params)authorizefoo# <-- Pundit::Authorization#authorize (lowercase)renderjson: fooendend
But FooController#create doesn't call the Authorize class. It calls
Pundit's authorize instance method, which is a different symbol entirely.
The marker class is referenced exactly once in the codebase (in BaseController via guard Api::Guards::Authorize), as a class-level DSL.
A whole community (35 nodes) is clustered around the phantom hub.
Community labels and "suggested questions" both surface this as the most
central concept, when it's actually a passive marker.
The "Are these N INFERRED edges correct?" verification question that
graphify suggests is well-targeted — but the edges' confidence_score
of 0.8 is misleadingly high given how systematic the false positive is.
The single-unique-candidate guard at line 330 (if len(candidates) != 1)
normally prevents this kind of thing, but it doesn't help when the
case-collision happens to produce exactly one candidate.
Proposed fix
A few options, roughly in order of effort:
Per-language casing policy. Add a case_sensitive_resolution flag
to LanguageConfig and set it True for Ruby, Go, C#, Swift, Kotlin
— languages where capitalized identifiers are conventionally distinct
types. normalise_callable_label would skip .lower() when the call
originates from a case-sensitive language. (Python keeps current
behavior to handle Class() constructor-as-call patterns.)
Skip resolution when target label case-differs from callee. Inside resolve_cross_file_raw_calls, after looking up candidates, drop any
whose original (un-lowercased) label doesn't share the callee's
first-char casing. Less invasive than (1), language-agnostic.
User-configurable denylist. Honor a .graphify.yml at the project
root with a resolution_exclude: list of labels that should never
participate as call targets. Useful for marker classes, sentinels, and
DSL helpers regardless of language.
(1) is the cleanest fix; (2) is the smallest patch; (3) is the most
flexible escape hatch. (1) and (3) compose well.
Repro environment: graphifyy 0.8.16, Python 3.11, Ruby AST via tree-sitter.
Summary
symbol_resolution.normalise_callable_label(line 31) lowercases all labelsbefore building the call-resolution index. In Ruby — where casing has
semantic meaning (
Foois a class,foois a method) — this collapsesdistinct symbols into one bucket and emits high-confidence INFERRED
callsedges that don't reflect the source code.
I hit this on a Rails 8 / Packwerk codebase where Pundit's lowercase
authorize recordhelper is called from every controller action, and thecodebase has a separate marker class
Api::Guards::Authorize. Thecase-insensitive resolver concluded that every
.create()/.update()/etc. action calls the marker class — 188 spurious INFERRED edges at
score 0.8, all to the same target.
Minimal repro
After
graphify extract:FooController#create --calls--> Api::Guards::Authorize(INFERRED, score 0.8)But
FooController#createdoesn't call theAuthorizeclass. It callsPundit's
authorizeinstance method, which is a different symbol entirely.The marker class is referenced exactly once in the codebase (in
BaseControllerviaguard Api::Guards::Authorize), as a class-level DSL.Impact
On the real codebase (a ~3,000-node Rails graph):
Authorizebecomes the v3: semantic query with embeddings #1 god node with 190 edges (188 INFERRED) —it should have ~3.
central concept, when it's actually a passive marker.
graphify suggests is well-targeted — but the edges'
confidence_scoreof 0.8 is misleadingly high given how systematic the false positive is.
The single-unique-candidate guard at line 330 (
if len(candidates) != 1)normally prevents this kind of thing, but it doesn't help when the
case-collision happens to produce exactly one candidate.
Proposed fix
A few options, roughly in order of effort:
Per-language casing policy. Add a
case_sensitive_resolutionflagto
LanguageConfigand set itTruefor Ruby, Go, C#, Swift, Kotlin— languages where capitalized identifiers are conventionally distinct
types.
normalise_callable_labelwould skip.lower()when the calloriginates from a case-sensitive language. (Python keeps current
behavior to handle
Class()constructor-as-call patterns.)Skip resolution when target label case-differs from callee. Inside
resolve_cross_file_raw_calls, after looking up candidates, drop anywhose original (un-lowercased) label doesn't share the callee's
first-char casing. Less invasive than (1), language-agnostic.
User-configurable denylist. Honor a
.graphify.ymlat the projectroot with a
resolution_exclude:list of labels that should neverparticipate as call targets. Useful for marker classes, sentinels, and
DSL helpers regardless of language.
(1) is the cleanest fix; (2) is the smallest patch; (3) is the most
flexible escape hatch. (1) and (3) compose well.
Repro environment: graphifyy 0.8.16, Python 3.11, Ruby AST via tree-sitter.