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