Skip to content

fix(query): let context_filter reach heritage edges that carry no context - #5179

Closed
sunhouy wants to merge 1 commit into
Graphify-Labs:v8from
sunhouy:fix/context-filter-heritage-edges-5178
Closed

sunhouy wants to merge 1 commit into
Graphify-Labs:v8from
sunhouy:fix/context-filter-heritage-edges-5178

Conversation

@sunhouy

@sunhouy sunhouy commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

context_filter only ever looked at edge["context"], and the C#, Java and Python extractors emit inherits / implements with no context at all — only the TypeScript one tags them type. So no filter value could select inheritance in those languages: ["type"] reached TypeScript alone, and ["inherits"] matched nothing anywhere. On the reporter's C# graph a base class with ~550 subclasses came back as 1 nodes found, with nothing in the output to suggest the filter had matched zero edges.

Two changes in graphify/serve.py:

  • A heritage word in the filter now matches the edge's relation too. inherits / inherit / inheritance / subclass* / superclass map to inherits; implements / interface* to implements; extends to extends. _filter_graph_by_context keeps an edge whose relation is one of the heritage relations the filter names, as well as one whose context matches.
  • An explicit filter that selects nothing says so: Context: return_type (explicit, matched 0 edges). The old header read Context: return_type (explicit) next to 1 nodes found, which is indistinguishable from "these symbols really are unrelated".

type deliberately keeps meaning exactly context == "type". It is what the TypeScript extractor writes, and folding the heritage relations into it would silently change what a TS user's existing --context type returns.

Fixes #5178

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Tests or CI
  • Refactor
  • Security fix

Verification & Invariants

The invariant is that a context_filter selects edges, and a filter that silently selects none is a bug of its own — the traversal answer is the same shape either way, so the header has to carry the difference. Nothing here changes the graph on disk, the stored edge shape, or extraction; the filter is a read-only view built per query. No persisted or generated state is affected, so there is no cache or manifest migration.

  • Read the CONTRIBUTING.md guide.
  • Reproduced the issue and identified the invariant.
  • Made the smallest fix necessary.
  • Added a regression test (if bug fix) or isolated boundary test.
  • Kept the PR description synchronized with the final implementation.
  • Documented any limitations / unsupported cases explicitly.

Limitations, stated plainly:

  • Only the heritage family is bridged. Other relations that some extractors tag and others do not (references, contains) still need context on the edge; I did not widen those, because a references edge under an explicit inherits filter would be wrong and I have no report of it being needed.
  • mixes_in / embeds are left alone: they are heritage-adjacent but the issue only covers inherits / implements / extends, and I would rather add them once someone has a graph that needs them.
  • Nothing here re-labels existing graphs. A graph extracted before this change gains no context on its C#/Java/Python heritage edges; it just becomes reachable through the relation match.

How was this tested?

# new regression tests, before the fix (v8 @ 06aa51b)
$ pytest tests/test_serve.py tests/test_query_mcp_direction.py -k context_filter
4 failed, 9 passed

# after
$ pytest tests/test_serve.py tests/test_query_mcp_direction.py -k context_filter
12 passed

# query surfaces
$ pytest tests/test_serve.py tests/test_query_cli.py tests/test_query_mcp_direction.py \
         tests/test_query_seeds.py tests/test_query_induced_edges.py
295 passed, 1 skipped

# whole suite
$ pytest tests/ -q
10353 passed, 150 failed, 516 skipped, 1 xfailed

The 150 failures are all in test_terraform_modules.py, test_hook_*, test_skillgen.py and test_ext_tagged_id_clash.py, and fail identically on 06aa51b without this patch (86 of them in the four files I re-ran on the base). They are unrelated to serve.py.

End-to-end, on a corpus holding the same Square : Base, IShape hierarchy in four languages (graphify extract . --code-only --no-viz, then graphify query):

# C#, before: every filter returned 2 nodes, seed + file, and no edge
$ graphify query "src/Shapes.cs::Square" --context inherits
Graph: ... (23 nodes) | Start: ['Square', 'Shapes.cs'] | Context: inherits (explicit) | 3 nodes found
EDGE Square --inherits [EXTRACTED]--> Base at=src/Shapes.cs:L1

$ graphify query "src/Shapes.cs::Square" --context implements
... | 3 nodes found
EDGE Square --implements [EXTRACTED]--> IShape at=src/Shapes.cs:L1

# TypeScript keeps its own filter; nothing new leaks into it
$ graphify query "src/Shapes.cs::Square" --context type
... | Context: type (explicit) | 2 nodes found

Java and Python behave the same, and the TS edges still show context=type when they are reached through inherits.

Graphify-specific checklist

  • I updated generated skill artifacts (uv run python -m tools.skillgen --bless) when changing their source fragments. — not applicable, no skill fragment or CLI help text changed.
  • I confirmed that AST/structural extraction remains deterministic (no ambient state dependencies like ENV variables). — extraction is untouched; the filter is a pure function of the graph and the filter list.
  • I reviewed changes for security implications (no unsafe interpolation into shell/Python). — the new header text interpolates only the already-normalized filter strings, which are lowercased, diacritic-stripped and drawn from a fixed alias map.
  • I confirmed no API keys or local-only graph data are included.
  • (If applicable) I disclosed AI authorship in my commit messages. — not applicable here.

@sunhouy
sunhouy requested a review from safishamsi as a code owner November 12, 2026 13:27
@github-actions

Copy link
Copy Markdown

Thanks for the pull request, @sunhouy. A maintainer will review it soon.

Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions.

A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic.

@github-actions github-actions Bot added the contributor Pull request from a returning community contributor label Nov 12, 2026
…Graphify-Labs#5178)

`context_filter` only matched `edge["context"]`, and the C#, Java and Python
extractors emit `inherits`/`implements` with no context at all (only the
TypeScript one tags them `type`). So no filter value could select inheritance
in those languages: `["type"]` worked for TS alone, `["inherits"]` matched
nothing anywhere, and the traversal came back with the seeds and zero edges.

- map the heritage words (inherit(s)/inheritance/subclass/superclass,
  implements/interface(s), extends) onto the edge relations they name, and let
  a heritage filter match the edge's `relation` as well as its `context`;
- `type` keeps meaning exactly `context == "type"` -- widening it would drop
  `implements` edges from a TypeScript graph;
- an explicit filter that selects no edge is now reported in the header as
  `Context: <f> (explicit, matched 0 edges)`, so an empty answer is not read as
  "these symbols are unrelated".

Verified on a four-language corpus (same Square : Base, IShape hierarchy in
C#, TS, Java, Python): `--context inherits` now reaches the C#/Java/Python
edges, and `--context implements` the interface ones, while `type` still
returns only what TS tagged.
@sunhouy
sunhouy force-pushed the fix/context-filter-heritage-edges-5178 branch from 84c9ed5 to 17fc2a6 Compare November 12, 2026 13:28

@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.

No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.

Not checked: tests were not run; no formal proof of the changed code.


No blocking issues. I didn't find anything I'd hold the merge for.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 0 functions depend on the 0 functions this change touches.

Health — grade A; no new coupling hotspots.

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

Diff could not be reliably attributed to this PR: the base/head merge-base was unreachable in the shallow clone, so detailed findings were skipped (rather than emit a two-dot diff, which would misattribute base-branch changes).

Gate & verification

graphify gate

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

Test selection

Test selection

0 of 559 test file(s) selected (0%) via static blast radius.

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

@safishamsi

Copy link
Copy Markdown
Member

Landed on v8 as 9762d8b via authorship-preserving cherry-pick, released in graphify 0.9.108. Thanks @sunhouy.

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

Labels

contributor Pull request from a returning community contributor

Projects

None yet

2 participants