diff --git a/CHANGELOG.md b/CHANGELOG.md index c029b472..f750348e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,11 @@ Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases) -## 0.9.45 (unreleased) +## 0.9.46 (unreleased) + +- Fix: when two edges connect the same node pair with different relations, the graph builder now keeps the more specific relation (`calls`, `imports`, `inherits`, ...) instead of letting a generic `references`/`uses`/`mentions` overwrite it; previously a real `calls` could be downgraded to `references` and then dropped from the call graph (#2803, thanks @abhay-codes07). + +## 0.9.45 (2026-08-16) - Fix: `graphify install ` now advances the `.graphify_version` stamp only for the platform it actually (re)writes, instead of stamping every installed platform as current; a platform whose skill content was left untouched keeps its old stamp so its staleness warning stays truthful (#2694, thanks @ousamabenyounes). This completes #2694 (the CLAUDE_CONFIG_DIR half shipped in 0.9.44). - Fix: an incremental rebuild no longer collapses the whole graph when the `.graphify_root` marker records a subfolder while stored `source_file` paths are relative to the repo root; the marker is validated against the stored paths before it is trusted as their anchor, so a mismatched marker can't make every unchanged source look deleted (#2603, thanks @catpotd). A genuinely deleted source is still evicted, and incremental ids stay identical to a cold build. diff --git a/tests/test_relation_collapse_precedence.py b/tests/test_relation_collapse_precedence.py index 28c21446..2ab15104 100644 --- a/tests/test_relation_collapse_precedence.py +++ b/tests/test_relation_collapse_precedence.py @@ -157,3 +157,28 @@ def test_directed_graphs_get_the_same_protection(): directed=True) assert G.is_directed() assert edge_data(G, "a", "b").get("relation") == "calls" + + +def test_specific_edge_numeric_metadata_survives_the_demotion(): + """When the generic edge is skipped, the surviving specific edge keeps its OWN + weight/confidence, not the demoted generic one's.""" + G = build_from_json(_extraction([ + _edge("calls", weight=3.0, confidence="EXTRACTED", confidence_score=1.0), + _edge("references", weight=9.0, confidence="INFERRED", confidence_score=0.2), + ])) + d = edge_data(G, "a", "b") + assert d.get("relation") == "calls" + assert d.get("weight") == 3.0 + assert d.get("confidence") == "EXTRACTED" + assert d.get("confidence_score") == 1.0 + + +def test_unknown_relation_is_treated_as_specific(): + """A relation not on the generic denylist counts as specific: a generic edge + must not overwrite it, and it must not be demoted by the guard — so the + denylist can't silently drift into an ordering (either arrival order keeps + the unknown one).""" + for order in ([_edge("custom_rel"), _edge("references")], + [_edge("references"), _edge("custom_rel")]): + G = build_from_json(_extraction(order)) + assert _relation(G) == "custom_rel", f"order {[e['relation'] for e in order]}"