diff --git a/graphify/build.py b/graphify/build.py index 470f2c6..8efdcbd 100644 --- a/graphify/build.py +++ b/graphify/build.py @@ -210,9 +210,23 @@ def _fold_edge_aliases(edge: dict) -> None: is not provenance) and never a threshold mapping of the float. The ``confidence_score`` key itself is NOT popped: it is a legitimate companion field that the edge loop sanitizes and to_json round-trips. + + A NUMERIC ``confidence`` (pre-enum graphs stored the LLM pass's float — + 1.0/0.95/0.9/0.85 — directly in the field) normalizes to ``INFERRED``: + numeric confidences only ever came from the LLM semantic pass, and + LLM-derived edges are INFERRED by definition. The original float moves to + ``confidence_score`` unless an explicit one is already present (the + companion field is the authority). Without this fold, every reload of a + pre-enum graph re-warns once per legacy edge, forever. ``bool`` is + excluded despite subclassing ``int``: ``True`` is not a score. """ if not edge.get("relation") and isinstance(edge.get("type"), str) and edge["type"]: edge["relation"] = edge.pop("type") + _conf = edge.get("confidence") + if isinstance(_conf, (int, float)) and not isinstance(_conf, bool): + if edge.get("confidence_score") is None: + edge["confidence_score"] = float(_conf) + edge["confidence"] = "INFERRED" if not edge.get("confidence") and edge.get("confidence_score") is not None: edge["confidence"] = "INFERRED" diff --git a/tests/test_build.py b/tests/test_build.py index eec628a..b4c1dd1 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -172,6 +172,62 @@ def test_legacy_edge_type_confidence_score_aliases_folded(): assert "type" not in data +def test_legacy_numeric_confidence_normalized_to_inferred(capsys): + """Pre-enum graphs stored the LLM pass's float directly in `confidence` + (1.0/0.95/0.9/0.85). Reloading such a graph must not warn once per edge on + every load — the number normalizes to INFERRED (numeric confidences only + ever came from the LLM semantic pass, and LLM-derived edges are INFERRED + by definition) with the original float preserved in confidence_score.""" + ext = {"nodes": [{"id": "n1", "label": "A", "file_type": "code", "source_file": "a.py"}, + {"id": "n2", "label": "B", "file_type": "code", "source_file": "b.py"}, + {"id": "n3", "label": "C", "file_type": "code", "source_file": "c.py"}], + "edges": [{"source": "n1", "target": "n2", "relation": "calls", + "confidence": 0.95, "source_file": "a.py"}, + {"source": "n2", "target": "n3", "relation": "references", + "confidence": 1.0, "source_file": "b.py"}], + "input_tokens": 0, "output_tokens": 0} + G = build_from_json(ext) + err = capsys.readouterr().err + assert "invalid confidence" not in err + assert "Extraction warning" not in err + d12 = edge_data(G, "n1", "n2") + assert d12["confidence"] == "INFERRED" + assert d12["confidence_score"] == 0.95 + d23 = edge_data(G, "n2", "n3") + assert d23["confidence"] == "INFERRED" + assert d23["confidence_score"] == 1.0 + + +def test_legacy_numeric_confidence_existing_score_wins(): + """A numeric `confidence` next to an explicit `confidence_score` must not + overwrite the explicit score — the companion field is the authority.""" + ext = {"nodes": [{"id": "n1", "label": "A", "file_type": "code", "source_file": "a.py"}, + {"id": "n2", "label": "B", "file_type": "code", "source_file": "b.py"}], + "edges": [{"source": "n1", "target": "n2", "relation": "calls", + "confidence": 0.9, "confidence_score": 0.4, "source_file": "a.py"}], + "input_tokens": 0, "output_tokens": 0} + G = build_from_json(ext) + data = edge_data(G, "n1", "n2") + assert data["confidence"] == "INFERRED" + assert data["confidence_score"] == 0.4 + + +def test_legacy_numeric_confidence_links_spelling_reload(capsys): + """The on-disk shape of the defect: a NetworkX-serialized graph.json + (`links` spelling) from a pre-enum version reloads without a validator + warning per edge and its edges land INFERRED.""" + raw = {"nodes": [{"id": "n1", "label": "A", "file_type": "code", "source_file": "a.py"}, + {"id": "n2", "label": "B", "file_type": "code", "source_file": "b.py"}], + "links": [{"source": "n1", "target": "n2", "relation": "calls", + "confidence": 0.85, "source_file": "a.py"}]} + G = build_from_json(raw) + err = capsys.readouterr().err + assert "invalid confidence" not in err + data = edge_data(G, "n1", "n2") + assert data["confidence"] == "INFERRED" + assert data["confidence_score"] == 0.85 + + def test_node_alias_canonical_field_wins(): """#2194: when both the canonical field and its alias are present, the canonical value wins and the alias key is left untouched."""