Skip to content

fix(query): skip covered-term seed guarantee; relax starving heuristic context filters (#2507) - #2516

Open
filipechagas wants to merge 2 commits into
Graphify-Labs:v8from
lawnstarter:feat/seed-hygiene-2507
Open

fix(query): skip covered-term seed guarantee; relax starving heuristic context filters (#2507)#2516
filipechagas wants to merge 2 commits into
Graphify-Labs:v8from
lawnstarter:feat/seed-hygiene-2507

Conversation

@filipechagas

Copy link
Copy Markdown

Addresses the two failure modes of #2507 that remain after 0.9.35's relational-verb demotion. One commit on top of 9f25a3a; your demotion (_RELATIONAL_INTENT_TERMS, the inline terms-keyed fallback, the caller/callers hint entry, and the four #2507 tests) is byte-identical — this layers on it, it does not relitigate it.

The interaction worth stating plainly

0.9.35 added caller/callers to the call entry of _CONTEXT_HINTS, so "callers of X" now infers a call context filter. A class node owns no call edges — calls attach to its methods, and the class→member edge carries context=None — so the filtered traversal cannot leave a class-node seed. Measured on a 48.5k-node Laravel corpus where the queried service has exactly 3 grep-verified production callers: "callers of ChargeCustomerService" went from 147 nodes / all 3 callers present-but-buried (0.9.34) to 1 node / 0 of 3, delivered confidently (0.9.35). The existing _make_callers_graph fixture can't observe this because it hangs context="call" edges directly off the class node. The hint entry itself is good and is untouched here — the class-node stranding underneath it is what this PR repairs.

The two changes (both default-inert, opted into only by _query_graph_text)

  1. Covered-term guarantee skip — a new keyword-only, default-off _pick_seeds parameter. In the guarantee loop, a term that is a substring of an already-picked seed's normalized label (the scorer's own weakest match tier, judged on the label alone — a labelless alias-only node covers nothing) claims no additional seed: it is not starved, and starvation is the only thing the guarantee exists to prevent. Refined invariant: every term with any match is matched by at least one seed. Runs after the existing label-dedup gate, same sorted-term order. Without it, "what code uses ChargeCustomerService to charge a customer" seeds the Customer hub and .charge() on top of ChargeCustomerService and explodes: measured 2,286 → 138 nodes.
  2. Heuristic-filter starvation fallback — after traversal, when the active filters came from the heuristic (never from explicit arguments) and the traversal discovered no nodes beyond the seeds, retraverse unfiltered and annotate the header: Context: call (heuristic; relaxed — no matches beyond seeds). Zero-expansion threshold (no tuning constant, and pinned by a mutation-checked boundary test: exactly one node of expansion is NOT relaxed), identical in BFS/DFS, explicit filters always honored.

Measured, 9f25a3a → this branch (same corpus, CLI defaults: depth 2, 2,000-token budget)

Phrasing Nodes Callers shown Header
"Who calls ChargeCustomerService?" 1 → 128 0/3 → 3/3 call (heuristic; relaxed — no matches beyond seeds)
"callers of ChargeCustomerService" 1 → 128 0/3 → 3/3 same (was stranded at the seed)
"what code uses ChargeCustomerService to charge a customer" 2,286 → 138 0/3 → 3/3 none inferred

explain on a listener question is regression-free at an identical token cost — it shares none of the changed code (_find_node only).

Tests

  • tests/seeding_fixtures.py (new): a fixture wired the way extraction really wires things — calls attach to methods, the class→method edge carries context=None, one caller reachable only through a references edge, a verb-prefix decoy with a busy test-method neighborhood, a generic-noun hub, a doc-file prefix decoy. This is what makes the stranding observable in a test at all.
  • tests/test_serve_seeding.py (new): 14 tests, 7 red at 9f25a3a (both changes red-first), 7 green-by-design pins including regression pins for 0.9.35's demotion restated on extraction-shaped wiring. Both threshold/predicate mutants kill exactly their intended test.
  • Zero baseline edits: git diff --stat 9f25a3a..HEAD -- tests/ is two new files only; your four query: relational-intent verbs ("calls"/"uses") survive stopwording, prefix-match unrelated identifiers, and the #1445 per-term guarantee seats the junk match as a seed #2507 tests and the scoring benchmark are untouched and green. Full suite 4,006 → 4,020 passed (the 4 test_ollama_retry_cap.py failures are a local missing-openai env issue, present at base). Bench equality gate holds. ruff check clean.

Disclosures and questions for the maintainer

  • Vocabulary follow-ups, not changed here: usage (the only use-family member not demoted), the listen family (listeners of <Event> is the same question shape on an event-driven corpus), and invocation (already a _CONTEXT_FILTER_ALIASES key). Also a structural question: since 0.9.35 hardcodes the demotion list while _CONTEXT_HINTS grows independently (caller/callers was exactly such a growth), do you want the set derived from the hint/alias tables so they can't drift apart?
  • One test builds its pre-fix comparison baseline by importing _RELATIONAL_INTENT_TERMS (there is no named demotion helper to call). It asserts nothing about the set's contents, and the realistic vocabulary extensions above leave it green (measured); the known latent coupling is that demoting a test-query's own domain nouns (e.g. adding both "charge" and "customer" — contrary to the set's documented verbs-only rule) would collapse the comparison to equality. Happy to restructure if you'd rather extract the inline demotion into a helper.
  • Known residual, deliberately not chased: a generic term like "code" can still guarantee-seed a doc-file prefix decoy (coder.md, 10 of 138 traversed nodes above); your open work on test-path seed exclusion and bounded recovery would shrink it further.
  • The changelog entry carries no thanks @… line — it felt wrong to self-credit; add attribution as you see fit.

Prior art: the same two changes (plus an equivalent demotion) shipped on the LawnStarter fork as 0.9.36 (lawnstarter#37, PR lawnstarter#44) with matching graded numbers on the same corpus.

🤖 Generated with Claude Code

…c filters; bump to 0.9.36 (Graphify-Labs#2507)

Two follow-ups to 0.9.35's relational-verb demotion, on the same natural-language
impact question ("who calls X?") and graded on the same 48.5k-node PHP corpus.

- `_pick_seeds` gains a keyword-only, default-off `skip_covered_terms`: a term
  that is a substring of an already-picked seed's normalized label is not
  starved, so it claims no additional guaranteed seed. Layered after the Graphify-Labs#1766
  dedup gate, in the same sorted-term order. Coverage reads the seed's LABEL
  only, never the `or nid` dedup key, so a labelless (`_fold_node_aliases`)
  seed cannot declare unrelated terms covered through its path fragments.
  Opted into by `_query_graph_text` alone; `path`, `explain`, the
  legacy-equality property tests and the benchmark's arms are unchanged.

- `_query_graph_text` retraverses unfiltered when a *heuristically inferred*
  context filter discovers nothing beyond the seeds, and annotates the
  `Context:` header. A class node owns no call edges, so the `call` filter that
  "Who calls X?" infers strands a correctly-seeded class at exactly one node.
  Threshold is zero expansion (`nodes <= set(start_nodes)`), not a tuning
  constant; identical in BFS and DFS; explicit filters are never relaxed.
  0.9.35 added `caller`/`callers` to `_CONTEXT_HINTS`, so "callers of X" infers
  the same filter and hit the same stranding — measured on the corpus, both
  phrasings returned 1 node before this change and 128 after, with all three
  known callers in the shown portion.

New `tests/seeding_fixtures.py` carries a fixture wired the way extraction
really wires a service (calls land on the method; the class->method edge has no
context), which is what makes the stranding observable; `test_serve_seeding.py`
pins all three seeding behaviours, including regression pins for the landed
0.9.35 demotion.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.


Graphify review — findings

This PR bumps the version from 0.9.35 to 0.9.36 and modifies the query seeding logic in graphify/serve.py. It adds a skip_covered_terms option to _pick_seeds (opted into only by the natural-language query path) that suppresses the per-term seed guarantee when a term is already a substring of a picked seed's normalized label, and adds a fallback in _query_graph_text that retraverses unfiltered when a heuristically inferred context filter reaches nothing beyond the seeds, annotating the header accordingly. It also adds a new shared test fixture file (tests/seeding_fixtures.py) and corresponding test changes, plus changelog entries describing these two fixes (#2507).

Worth a look

  • Covered-term skip can starve a legitimately distinct term whose winner shares a substring with an unrelated seedgraphify/serve.py:758 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 649 functions depend on the 345 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: _query_graph_text() — 29 callers, 6 callees
  • worse: _score_query() — 21 callers, 5 callees
  • worse: _query_terms() — 22 callers, 3 callees
  • new: make_charge_fixture() — 14 callers, 3 callees
  • new: test_generic_noun_phrasing_seeds_no_hub_and_stays_bounded() — 0 callers, 11 callees
  • new: test_callers_of_phrasing_renders_all_callers_and_drops_junk_seed() — 0 callers, 7 callees
  • new: test_who_calls_phrasing_falls_back_when_heuristic_filter_strands_the_seed() — 0 callers, 7 callees
  • new: test_expanding_heuristic_filter_is_left_in_force() — 0 callers, 6 callees
  • …and 1 more

Verification — 649 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 509 function(s) in the blast radius were not formally verified this run

· 7 grounded finding(s) anchored inline below; 3 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/serve.py Outdated
Comment on lines +752 to +757
# Layered after that dedup gate, in this same sorted-term order, so
# the coverage check sees the gap-window seeds plus every guarantee
# seed appended so far.
if skip_covered_terms and any(term in _seed_norm_label(s) for s in seeds):
continue
seen_labels.add(key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered-term skip can starve a legitimately distinct term whose winner shares a substring with an unrelated seed — agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review

Graphify suggests a fix:

Suggested change
# Layered after that dedup gate, in this same sorted-term order, so
# the coverage check sees the gap-window seeds plus every guarantee
# seed appended so far.
if skip_covered_terms and any(term in _seed_norm_label(s) for s in seeds):
continue
seen_labels.add(key)
# Layered after that dedup gate, but only the TOP-ranked seed — the
# query's dominant match — can declare a term covered. A coincidental
# substring collision inside some unrelated, lower-ranked seed's label
# must not silently starve a distinct term's real winner.
if skip_covered_terms and seeds and term in _seed_norm_label(seeds[0]):
continue
seen_labels.add(key)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified and adopted in 234c5f0 — thanks. The finding reproduces: with a node labelled exactly port (its term's winner by ~7,300x in _score_query) and an unrelated ReportService in the gap window, the old predicate let ReportService absorb "port" and the real winner was starved, while the legacy picker seeds it correctly. I've taken the suggested seeds[0] refinement (guarded on seeds being non-empty), which restates the invariant as every term with any match is matched by the top-ranked seed or by a seed of its own, and added the repro as a regression test along with the docstring, comment and CHANGELOG corrections. Measured behaviour on the graded corpus is unchanged — all three phrasings return byte-identical output to the previous commit, since the covering seed there was always the top seed.

Comment thread tests/seeding_fixtures.py
confidence="EXTRACTED", _src=src, _tgt=tgt)


def make_charge_fixture(*, calls_symbol: bool = False) -> nx.Graph:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionmake_charge_fixture()

14 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intended shape rather than a regression: the 14 callers are the point of this fixture — all three behaviours (verb demotion, covered-term skip, filter relaxation) grade the same corpus instead of each test re-inventing a graph, which is what makes the phrasing results comparable across the suite. The fan-in is confined to tests/ and no production code depends on it.

_COVERED_AND_STARVED_QUESTION = "ChargeCustomerService customer code"


def test_generic_noun_phrasing_seeds_no_hub_and_stays_bounded():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressiontest_generic_noun_phrasing_seeds_no_hub_and_stays_bounded()

fans out to 11 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callees here are the shared fixture plus the small output-parsing helpers (start_labels, shown_nodes, label_of, caller_labels) that keep every assertion reading rendered text instead of internals. That indirection is deliberate — it's what makes these tests robust to vocabulary tweaks — and it's test-local; no production coupling changes.

# *explicit* filter is always honored. #
# --------------------------------------------------------------------------- #

def test_who_calls_phrasing_falls_back_when_heuristic_filter_strands_the_seed():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressiontest_who_calls_phrasing_falls_back_when_heuristic_filter_strands_the_seed()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the sibling threads: the fan-out is the shared fixture plus the header/output-parsing helpers that keep assertions on external behaviour (rendered Start: seeds, NODE lines, the Context: note) rather than internals. Test-local by design; no production coupling changes. No action taken — happy to restructure if the metrics gate treats this as blocking.

assert "relaxed" in context, f"header does not report the relaxation: {context!r}"


def test_callers_of_phrasing_renders_all_callers_and_drops_junk_seed():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressiontest_callers_of_phrasing_renders_all_callers_and_drops_junk_seed()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the sibling threads: the fan-out is the shared fixture plus the header/output-parsing helpers that keep assertions on external behaviour (rendered Start: seeds, NODE lines, the Context: note) rather than internals. Test-local by design; no production coupling changes. No action taken — happy to restructure if the metrics gate treats this as blocking.

)


def test_expanding_heuristic_filter_is_left_in_force():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressiontest_expanding_heuristic_filter_is_left_in_force()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the sibling threads: the fan-out is the shared fixture plus the header/output-parsing helpers that keep assertions on external behaviour (rendered Start: seeds, NODE lines, the Context: note) rather than internals. Test-local by design; no production coupling changes. No action taken — happy to restructure if the metrics gate treats this as blocking.

)


def test_single_node_expansion_is_not_starvation():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressiontest_single_node_expansion_is_not_starvation()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the sibling threads: the fan-out is the shared fixture plus the header/output-parsing helpers that keep assertions on external behaviour (rendered Start: seeds, NODE lines, the Context: note) rather than internals. Test-local by design; no production coupling changes. No action taken — happy to restructure if the metrics gate treats this as blocking.

…hify-Labs#2507)

Review follow-up. The covered-term skip let ANY picked seed absorb a term by
substring, so a coincidental collision inside an unrelated, lower-ranked seed's
label starved the term's real winner: `ReportService` contains "port", which
cost a corpus symbol literally named `port` the guaranteed seat that was its
only way into the seed list — Graphify-Labs#1597's concern one layer down.

Coverage asserts "the query's dominant match already answers this term", which
is a claim only the top-ranked seed is entitled to make, so the predicate now
reads `seeds[0]` alone (guarded on `seeds` being non-empty). `_seed_norm_label`
keeps its label-only semantics, so a labelless seed still covers nothing. The
refined invariant: every term with any match is matched by the top-ranked seed
or by a seed of its own.

Behaviour on the graded corpus is unchanged — all three phrasings return output
byte-identical to the previous commit's, because the covering seed there is
always the top seed.
@filipechagas

Copy link
Copy Markdown
Author

Review round addressed in 234c5f0: the covered-term advisory was verified with a reproducing execution (details in the thread) and the suggested top-ranked-seed refinement adopted, with the repro added as a regression test and the docstring/comment/CHANGELOG wording corrected. Both mutation probes still kill their intended tests, the full suite is green (4,021 passed; the 4 test_ollama_retry_cap.py failures are a local missing-openai env issue present at base), and all three graded phrasings return byte-identical output to the previous commit — on the measured corpus the covering seed was always the top seed. The coupling-metric threads on the test files are answered inline: the fan-in/fan-out is the shared fixture + output-parsing helpers, test-local by design.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.


Graphify review — findings

This PR bumps the package version from 0.9.35 to 0.9.36 and adds two related changes to the natural-language query seeding pipeline in graphify/serve.py. It introduces a skip_covered_terms option to _pick_seeds (opted into only by _query_graph_text) that skips the per-term seed guarantee when a term is a substring of the top-ranked seed's normalized label, and adds a fallback in _query_graph_text that retraverses unfiltered when a heuristically inferred context filter discovers nothing beyond the seeds, annotating the header accordingly. The change also adds a new shared test fixture file (tests/seeding_fixtures.py) and associated tests, plus CHANGELOG entries describing both fixes. The surface area is concentrated in seed selection, context-filter handling, header formatting, and test scaffolding.

Worth a look

  • Covered-term skip uses substring test, matching arbitrary short termsgraphify/serve.py:771 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 651 functions depend on the 347 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: _query_graph_text() — 29 callers, 6 callees
  • worse: _score_query() — 22 callers, 5 callees
  • worse: _query_terms() — 22 callers, 3 callees
  • new: make_charge_fixture() — 14 callers, 3 callees
  • new: test_generic_noun_phrasing_seeds_no_hub_and_stays_bounded() — 0 callers, 11 callees
  • new: test_callers_of_phrasing_renders_all_callers_and_drops_junk_seed() — 0 callers, 7 callees
  • new: test_who_calls_phrasing_falls_back_when_heuristic_filter_strands_the_seed() — 0 callers, 7 callees
  • new: test_expanding_heuristic_filter_is_left_in_force() — 0 callers, 6 callees
  • …and 1 more

Verification — 651 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 511 function(s) in the blast radius were not formally verified this run

· 6 grounded finding(s) anchored inline below; 3 more finding(s) on lines outside this diff (see the check run).

Comment thread tests/seeding_fixtures.py
confidence="EXTRACTED", _src=src, _tgt=tgt)


def make_charge_fixture(*, calls_symbol: bool = False) -> nx.Graph:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionmake_charge_fixture()

14 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

_COVERED_AND_STARVED_QUESTION = "ChargeCustomerService customer code"


def test_generic_noun_phrasing_seeds_no_hub_and_stays_bounded():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressiontest_generic_noun_phrasing_seeds_no_hub_and_stays_bounded()

fans out to 11 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

# *explicit* filter is always honored. #
# --------------------------------------------------------------------------- #

def test_who_calls_phrasing_falls_back_when_heuristic_filter_strands_the_seed():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressiontest_who_calls_phrasing_falls_back_when_heuristic_filter_strands_the_seed()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

assert "relaxed" in context, f"header does not report the relaxation: {context!r}"


def test_callers_of_phrasing_renders_all_callers_and_drops_junk_seed():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressiontest_callers_of_phrasing_renders_all_callers_and_drops_junk_seed()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

)


def test_expanding_heuristic_filter_is_left_in_force():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressiontest_expanding_heuristic_filter_is_left_in_force()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

)


def test_single_node_expansion_is_not_starvation():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressiontest_single_node_expansion_is_not_starvation()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@filipechagas

Copy link
Copy Markdown
Author

Status note, since #2507 is now closed as fixed and this PR is the part of it that remains open.

0.9.35 fixed direction 2 of the report — the relational-verb demotion — and this branch takes that commit byte-identical and does not relitigate it. The two failure modes from the original report that survive it are what is here:

  1. The per-term guarantee still seats a hub seed for a generic noun. "what code uses ChargeCustomerService to charge a customer" seeds the Customer hub and .charge() on top of the queried service and explodes to 2,286 nodes; with the covered-term skip, 138.
  2. A heuristic context filter can strand the traversal at its seed with no signal to the caller.

Worth separating out, because it is a regression rather than a leftover: 0.9.35 also added caller/callers to the call entry of _CONTEXT_HINTS. The hint entry itself is right, and this PR leaves it untouched — but a class node owns no call edges (they attach to its methods, and the class→member edge carries context=None), so the inferred filter cannot leave a class-node seed. Measured on a 48.5k-node Laravel corpus where the queried service has exactly 3 grep-verified production callers:

nodes callers shown
0.9.34 147 3/3, present but buried
0.9.35 1 0/3, delivered confidently
this branch 128 3/3, header marked call (heuristic; relaxed — no matches beyond seeds)

Confidently empty is the part that concerns us more than the burying did. The existing _make_callers_graph fixture cannot observe it, because it hangs context="call" edges directly off the class node rather than off its methods — which is why the new fixture in this PR wires calls the way extraction actually wires them.

Branch status: rebased on 9f25a3a, automated review round addressed in 234c5f0, full suite green, zero baseline test edits (git diff --stat 9f25a3a..HEAD -- tests/ is two new files).

No urgency from our side — we run these on our fork. Happy to split the two changes into separate PRs, or to take the structural question from the description (deriving the demotion vocabulary from _CONTEXT_HINTS / _CONTEXT_FILTER_ALIASES so the two cannot drift as _CONTEXT_HINTS grows — caller/callers was exactly such a growth) as a separate change afterwards. Whichever is easier to review.

@filipechagas

Copy link
Copy Markdown
Author

One more consolidated note, on the automated coupling threads (there are six distinct ones, each posted twice). I answered these inline in the earlier round; collecting the argument in one place since they all reduce to the same thing.

Every flag on this PR targets test code, and all of it is in the two new files this PR adds:

thread metric
tests/seeding_fixtures.py:114 make_charge_fixture() 14 callers (afferent)
tests/test_serve_seeding.py:168 test_generic_noun_phrasing_... 11 callees (efferent)
tests/test_serve_seeding.py:349 test_who_calls_phrasing_... 7
tests/test_serve_seeding.py:373 test_callers_of_phrasing_... 7
tests/test_serve_seeding.py:400 test_expanding_heuristic_filter_... 6
tests/test_serve_seeding.py:472 test_single_node_expansion_... 6

These counts are the fixture pattern working, not coupling debt. tests/seeding_fixtures.py is new in this PR, so there is no prior fan-in to regress from — and the 14 callers are the design: every test grades the same corpus so results stay comparable across phrasings, which is precisely what per-test hand-rolled graphs would destroy. That comparability is load-bearing here rather than stylistic: the whole reason this PR needed a new fixture is that the existing _make_callers_graph hangs context="call" edges directly off the class node, so it cannot observe the class-node stranding at all. A fixture that wires calls the way extraction really wires them is the thing that makes the bug visible in a test.

The efferent counts on the individual tests are that same shared fixture plus the small output-parsing helpers (start_labels, shown_nodes, label_of, caller_labels, _context_segment) that keep assertions on rendered text rather than on internals — which is what makes them robust to the vocabulary changes this area keeps getting.

No production symbol's coupling changed on this PR. Happy to restructure if the metrics gate treats this as blocking, but I'd be trading a deliberate design for a number.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant