Files
graphify/tests
opnsrcntrbtr b9871d7019 Fix #563: prevent rationale-node leakage and preserve calls direction (#576)
* Fix #563: prevent rationale-node leakage and preserve calls direction

Two AST-extractor bugs were inflating god-node centrality:

1. Rationale leakage in cross-file resolvers
   _resolve_cross_file_imports indexed any node whose label didn't end in
   ')' or '.py' as an importable entity, so rationale nodes (whose labels
   are docstring text) ended up in both stem_to_entities and local_classes.
   Result: phantom '<file>_rationale_N --uses--> ImportedThing' edges for
   every imported entity in every file with docstrings.

   The same leak existed in cross-file call resolution via
   global_label_to_nid, where rationale labels could collide with callee
   names.

   Fix: skip nodes with file_type == 'rationale' in all three index loops.

2. Calls / rationale_for direction lost at export
   to_json() called nx.json_graph.node_link_data() on a NetworkX undirected
   Graph, which canonicalizes endpoint order. The build path already
   stashes _src and _tgt on every edge for exactly this case (with a
   comment to that effect at build.py:100), but the exporter never
   consulted them, so 'calls' and 'rationale_for' edges were emitted with
   arbitrary direction.

   Fix: in to_json(), restore link['source'] / link['target'] from
   _src / _tgt, then pop those internal fields before writing graph.json.

Repro on a minimal 3-file project (queue.py + contact_form.py + a test
file with multi-paragraph docstrings) showed:

  Before fix: 31 edges, 12 incident on SQLiteQueue, 7 phantom uses-edges
              from rationale nodes, 1 inverted calls edge.
  After fix:  24 edges, 5 incident on SQLiteQueue, 0 phantom edges,
              all calls edges go caller -> callee.

Adds tests/test_issue_563.py with 5 regression tests covering both bugs
plus a direction-pin on the existing sample_calls.py fixture. Full suite
passes (446/446).

* Address Copilot review on #576

- tests: identify rationale nodes by file_type metadata, not id substring
  (decouples regression from current `<stem>_rationale_<line>` naming)
- tests: assert contact_form -> SQLiteQueue.enqueue calls edge directly
  (previously only inversions were guarded; a dropped enqueue edge would
  have passed silently)
- extract.py: clarify cross-file import indexing comment — the filter
  intentionally indexes only class-level entities (function/method labels
  end in "()" and are excluded by design)

* Fix #563 (HTML export): restore arrow direction in graph.html

to_json was patched to consult _src/_tgt before serializing edge
endpoints, but to_html still read endpoints directly from G.edges(),
so calls and rationale_for arrows in the rendered graph.html still
pointed in canonicalized (often inverted) order.

Apply the same direction restore in to_html when building vis.js
edges. Use data.get (not pop) since G.edges(data=True) yields the
live attribute dict and other exporters may run after to_html.

Adds test_to_html_preserves_calls_and_rationale_for_direction:
parses RAW_EDGES out of the rendered HTML and pins both the
caller->callee assertions from #563 and the rationale->parent
direction. Without the fix, the test reports 11 flipped arrows
on the 3-file repro.
2026-05-02 14:15:06 +01:00
..
2026-04-04 18:52:44 +01:00