Summary
Node IDs normalise a symbol name to [a-z0-9_], which strips Ruby's ! and ? suffixes. A bang or
predicate method therefore produces the same ID as its non-suffixed sibling defined in the same file,
and the dedup pass (if n['id'] not in seen) keeps whichever was encountered first. The other method
disappears from the graph entirely — no node, and no edges.
The ID rule in references/extraction-spec.md says the entity part is "the symbol name similarly
normalized", with [a-z0-9_] as the allowed set. save_it and save_it! both normalise to save_it.
Evidence
Measured on a Rails API-only codebase (graphifyy 0.9.48, undirected build, ~420 files):
def definitions found under app/: 631
- method nodes in
graph.json for app/: 626
- gap: exactly 5
All five are ID collisions — four foo / foo! pairs and one foo / foo? pair, spread over three model
files. They account for 100% of the extraction loss: once the collisions are excluded, every remaining
def in the corpus has a matching node, so there is no other failure mode at play.
Why it matters more than 0.8% suggests
The bias is directional. In Ruby the bang variant is conventionally the persisting / raising one, and
it is the public entry point that callers actually use:
def attach(thing)
self.thing = thing
end
def attach!(thing) # <- dropped from the graph
attach(thing)
save!
end
The graph keeps the private setter and hides the public API. Any traversal, explain, or path query
over such a class will miss the persisting method, and its callers show up as isolated nodes.
The same applies to predicates: the foo? guard is dropped in favour of the foo value method.
Reproduction
# app/models/thing.rb
class Thing < ApplicationRecord
def save_it
self.state = :done
end
def save_it!
save_it
save!
end
end
graphify . produces one node for save_it and none for save_it!.
Suggested fix
Transliterate rather than strip, in the entity part of the ID only:
! → _bang
? → _p (or _q)
= → _eq (setters have the same problem: foo vs foo=)
save_it! → app_models_thing_save_it_bang. This keeps the ID charset at [a-z0-9_], stays deterministic
from the label alone, and is a pure win — no existing non-suffixed ID changes, so previously built graphs
keep matching on --update for every method that has no suffixed sibling.
Ruby is the obvious case, but the same normalisation affects any language with suffixed identifiers
(Scheme/Clojure foo?/foo!, Ruby setters foo=).
Environment
- graphifyy 0.9.48, skill 0.9.20
- corpus: ~420 files, Rails API-only app, 1773 nodes / 2161 edges
- undirected build,
--update incremental
Summary
Node IDs normalise a symbol name to
[a-z0-9_], which strips Ruby's!and?suffixes. A bang orpredicate method therefore produces the same ID as its non-suffixed sibling defined in the same file,
and the dedup pass (
if n['id'] not in seen) keeps whichever was encountered first. The other methoddisappears from the graph entirely — no node, and no edges.
The ID rule in
references/extraction-spec.mdsays the entity part is "the symbol name similarlynormalized", with
[a-z0-9_]as the allowed set.save_itandsave_it!both normalise tosave_it.Evidence
Measured on a Rails API-only codebase (graphifyy 0.9.48, undirected build, ~420 files):
defdefinitions found underapp/: 631graph.jsonforapp/: 626All five are ID collisions — four
foo/foo!pairs and onefoo/foo?pair, spread over three modelfiles. They account for 100% of the extraction loss: once the collisions are excluded, every remaining
defin the corpus has a matching node, so there is no other failure mode at play.Why it matters more than 0.8% suggests
The bias is directional. In Ruby the bang variant is conventionally the persisting / raising one, and
it is the public entry point that callers actually use:
The graph keeps the private setter and hides the public API. Any traversal,
explain, orpathqueryover such a class will miss the persisting method, and its callers show up as isolated nodes.
The same applies to predicates: the
foo?guard is dropped in favour of thefoovalue method.Reproduction
graphify .produces one node forsave_itand none forsave_it!.Suggested fix
Transliterate rather than strip, in the entity part of the ID only:
!→_bang?→_p(or_q)=→_eq(setters have the same problem:foovsfoo=)save_it!→app_models_thing_save_it_bang. This keeps the ID charset at[a-z0-9_], stays deterministicfrom the label alone, and is a pure win — no existing non-suffixed ID changes, so previously built graphs
keep matching on
--updatefor every method that has no suffixed sibling.Ruby is the obvious case, but the same normalisation affects any language with suffixed identifiers
(Scheme/Clojure
foo?/foo!, Ruby settersfoo=).Environment
--updateincremental