mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-15 08:29:26 +00:00
fix: snap cross-repo resolver confidence scores to the INFERRED rubric (#4045)
Snap the off-rubric INFERRED confidence scores emitted by the cross-repo call/type resolvers and interface dispatch (0.8 and 0.9) to the nearest canonical rubric tier (0.85), so every INFERRED edge carries one of the documented discrete scores. Label-only change: no edge flips EXTRACTED<->INFERRED and no edge is produced or dropped. Replaces the substring-matching guard test with an AST-based one that scans every module for off-rubric scores. Squashed from #4045 (authorship preserved). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
7e7de7d08f
commit
b8fcd29cb1
10 files changed
+91
-25
No files matched your search
@@ -235,7 +235,7 @@ def link_cross_repo_member_calls(merged: "nx.Graph") -> int:
|
||||
relation="calls",
|
||||
context="cross_repo",
|
||||
confidence="INFERRED",
|
||||
confidence_score=0.8,
|
||||
confidence_score=0.85,
|
||||
source_file=str(caller_data.get("source_file") or ""),
|
||||
source_location=entry.get("line"),
|
||||
weight=1.0,
|
||||
|
||||
@@ -65,7 +65,7 @@ def link_shared_type_declarations(merged: "nx.Graph") -> int:
|
||||
relation=SHARED_TYPE_RELATION,
|
||||
context="cross_repo",
|
||||
confidence="INFERRED",
|
||||
confidence_score=0.9,
|
||||
confidence_score=0.85,
|
||||
source_file=str(merged.nodes[left].get("source_file") or ""),
|
||||
weight=1.0,
|
||||
_src=left,
|
||||
|
||||
+6
-6
@@ -3945,7 +3945,7 @@ def _resolve_swift_member_calls(
|
||||
"relation": relation,
|
||||
"context": "call",
|
||||
"confidence": "EXTRACTED" if type_qualified else "INFERRED",
|
||||
"confidence_score": 1.0 if type_qualified else 0.8,
|
||||
"confidence_score": 1.0 if type_qualified else 0.85,
|
||||
"source_file": rc.get("source_file", ""),
|
||||
"source_location": rc.get("source_location"),
|
||||
"weight": 1.0,
|
||||
@@ -4237,7 +4237,7 @@ def _resolve_typescript_member_calls(
|
||||
"relation": "calls",
|
||||
"context": "call",
|
||||
"confidence": "EXTRACTED" if type_qualified else "INFERRED",
|
||||
"confidence_score": 1.0 if type_qualified else 0.8,
|
||||
"confidence_score": 1.0 if type_qualified else 0.85,
|
||||
"source_file": rc.get("source_file", ""),
|
||||
"source_location": rc.get("source_location"),
|
||||
"weight": 1.0,
|
||||
@@ -4403,7 +4403,7 @@ def _resolve_cpp_member_calls(
|
||||
"relation": relation,
|
||||
"context": "call",
|
||||
"confidence": "EXTRACTED" if type_qualified else "INFERRED",
|
||||
"confidence_score": 1.0 if type_qualified else 0.8,
|
||||
"confidence_score": 1.0 if type_qualified else 0.85,
|
||||
"source_file": src_file,
|
||||
"source_location": rc.get("source_location"),
|
||||
"weight": 1.0,
|
||||
@@ -4625,7 +4625,7 @@ def _resolve_csharp_member_calls(
|
||||
"relation": "calls",
|
||||
"context": "call",
|
||||
"confidence": "EXTRACTED" if type_qualified else "INFERRED",
|
||||
"confidence_score": 1.0 if type_qualified else 0.8,
|
||||
"confidence_score": 1.0 if type_qualified else 0.85,
|
||||
"source_file": src_file,
|
||||
"source_location": rc.get("source_location"),
|
||||
"weight": 1.0,
|
||||
@@ -4835,7 +4835,7 @@ def _resolve_java_member_calls(
|
||||
"relation": "calls",
|
||||
"context": "call",
|
||||
"confidence": "EXTRACTED" if exact else "INFERRED",
|
||||
"confidence_score": 1.0 if exact else 0.8,
|
||||
"confidence_score": 1.0 if exact else 0.85,
|
||||
"source_file": raw_call.get("source_file", ""),
|
||||
"source_location": raw_call.get("source_location"),
|
||||
"weight": 1.0,
|
||||
@@ -5026,7 +5026,7 @@ def _resolve_objc_member_calls(
|
||||
"relation": relation,
|
||||
"context": "call",
|
||||
"confidence": "EXTRACTED" if type_qualified else "INFERRED",
|
||||
"confidence_score": 1.0 if type_qualified else 0.8,
|
||||
"confidence_score": 1.0 if type_qualified else 0.85,
|
||||
"source_file": src_file,
|
||||
"source_location": rc.get("source_location"),
|
||||
"weight": 1.0,
|
||||
|
||||
@@ -150,7 +150,7 @@ def resolve_interface_dispatch(
|
||||
"relation": DISPATCH_RELATION,
|
||||
"context": "call",
|
||||
"confidence": "INFERRED",
|
||||
"confidence_score": 0.9,
|
||||
"confidence_score": 0.85,
|
||||
"source_file": str(impl_node.get("source_file", "")),
|
||||
"source_location": impl_node.get("source_location"),
|
||||
"weight": 1.0,
|
||||
|
||||
@@ -98,6 +98,7 @@ def test_a_parked_call_binds_to_the_one_declaration_in_another_repo():
|
||||
data = G.edges["a::app_run", "b::greeter_greet"]
|
||||
assert data["relation"] == "calls"
|
||||
assert data["confidence"] == "INFERRED"
|
||||
assert data["confidence_score"] == 0.85
|
||||
assert data["context"] == "cross_repo"
|
||||
assert data["source_location"] == "L10"
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ def test_same_namespace_and_name_across_repos_are_linked(tmp_path):
|
||||
endpoints = {links[0]["source"], links[0]["target"]}
|
||||
assert endpoints == {"svc_a::evt", "svc_b::evt"}
|
||||
assert links[0]["confidence"] == "INFERRED"
|
||||
assert links[0]["confidence_score"] == 0.85
|
||||
|
||||
|
||||
def test_same_name_in_different_namespaces_is_not_linked(tmp_path):
|
||||
|
||||
@@ -76,6 +76,10 @@ _INJECTED = {
|
||||
def test_single_implementer_links_the_interface_method(tmp_path):
|
||||
dispatch, r = _extract(tmp_path, _INJECTED)
|
||||
assert (_find(r, ".Build()", "ireport"), _find(r, ".Build()", "report_report")) in dispatch
|
||||
for edge in r["edges"]:
|
||||
if edge["relation"] == "dispatches_to":
|
||||
assert edge["confidence"] == "INFERRED"
|
||||
assert edge["confidence_score"] == 0.85
|
||||
|
||||
|
||||
def test_chain_through_an_injected_dependency_becomes_reachable(tmp_path):
|
||||
|
||||
@@ -18,6 +18,7 @@ promote AST `uses` edges to EXTRACTED/1.0, which is a semantic judgement about
|
||||
what the extractor knows; snapping the scores onto the documented scale fixes
|
||||
the stated violation without making that call.
|
||||
"""
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -63,19 +64,78 @@ def test_extracted_and_ambiguous_defaults_are_unchanged():
|
||||
# No emission site ships an off-rubric literal
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize("rel_path", [
|
||||
"extract.py",
|
||||
"symbol_resolution.py",
|
||||
"extractors/engine.py",
|
||||
"extractors/resolution.py",
|
||||
def _literal_scores(expr):
|
||||
"""Read only emitted literals, including both branches of a ternary.
|
||||
|
||||
Calls and lookups are consumers of scores, not literal emission sites.
|
||||
Parsing Python also avoids matching examples in strings or comments.
|
||||
"""
|
||||
if isinstance(expr, ast.Constant) and type(expr.value) in (int, float):
|
||||
yield expr.value
|
||||
elif isinstance(expr, ast.IfExp):
|
||||
yield from _literal_scores(expr.body)
|
||||
yield from _literal_scores(expr.orelse)
|
||||
|
||||
|
||||
def _emitted_score_exprs(tree):
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Dict):
|
||||
for key, value in zip(node.keys, node.values):
|
||||
if isinstance(key, ast.Constant) and key.value == "confidence_score":
|
||||
yield value
|
||||
elif isinstance(node, ast.keyword) and node.arg == "confidence_score":
|
||||
yield node.value
|
||||
elif isinstance(node, ast.Assign) and any(
|
||||
isinstance(target, ast.Name) and target.id == "confidence_score"
|
||||
for target in node.targets
|
||||
):
|
||||
yield node.value
|
||||
elif isinstance(node, ast.AnnAssign) and (
|
||||
isinstance(node.target, ast.Name) and node.target.id == "confidence_score"
|
||||
) and node.value is not None:
|
||||
yield node.value
|
||||
|
||||
|
||||
def test_no_module_hardcodes_an_off_rubric_score():
|
||||
"""Cover every Python module and spelling, not a fixed list of resolvers.
|
||||
|
||||
EXTRACTED/1.0 and AMBIGUOUS/0.2 are valid too. Keep the separate
|
||||
runtime checks below to verify that INFERRED edges use the INFERRED set.
|
||||
"""
|
||||
allowed = RUBRIC | {1.0, 0.2}
|
||||
violations = []
|
||||
for path in sorted(SRC.rglob("*.py")):
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
for expr in _emitted_score_exprs(tree):
|
||||
for score in _literal_scores(expr):
|
||||
if score not in allowed:
|
||||
violations.append(f"{path.relative_to(SRC)}:{expr.lineno}: {score}")
|
||||
assert not violations, "Off-rubric emitted scores:\n" + "\n".join(violations)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", [
|
||||
'{"confidence_score": 0.8}',
|
||||
'emit(confidence_score=0.8)',
|
||||
'confidence_score = 0.8',
|
||||
'confidence_score: float = 0.8',
|
||||
'{"confidence_score": 1.0 if exact else 0.8}',
|
||||
])
|
||||
def test_no_module_hardcodes_an_off_rubric_inferred_score(rel_path):
|
||||
"""0.8 was the value in the tree and is not on the scale. Catch it and the
|
||||
forbidden 0.5 as literals, so a future edit cannot reintroduce either."""
|
||||
text = (SRC / rel_path).read_text(encoding="utf-8")
|
||||
for forbidden in ('"confidence_score": 0.8,', '"confidence_score": 0.5,',
|
||||
"confidence_score = 0.8\n", "confidence_score = 0.5\n"):
|
||||
assert forbidden not in text, f"{rel_path} still emits {forbidden.strip()}"
|
||||
def test_emission_guard_recognizes_all_literal_spellings(source):
|
||||
scores = [
|
||||
score
|
||||
for expr in _emitted_score_exprs(ast.parse(source))
|
||||
for score in _literal_scores(expr)
|
||||
]
|
||||
assert 0.8 in scores
|
||||
|
||||
|
||||
def test_emission_guard_ignores_comments_strings_and_score_consumers():
|
||||
tree = ast.parse(
|
||||
'# "confidence_score": 0.8\n'
|
||||
'example = \'{"confidence_score": 0.8}\'\n'
|
||||
'score = edge.get("confidence_score", 0.5)\n'
|
||||
)
|
||||
assert list(_emitted_score_exprs(tree)) == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -91,7 +91,7 @@ def test_swift_cross_file_member_calls_have_correct_confidence_and_resolve(tmp_p
|
||||
if e.get("relation") != "calls":
|
||||
continue
|
||||
if tgt_label in inferred_targets:
|
||||
assert e["confidence"] == "INFERRED" and e["confidence_score"] == 0.8
|
||||
assert e["confidence"] == "INFERRED" and e["confidence_score"] == 0.85
|
||||
assert e["target"] in node_ids and src_by_id.get(e["target"])
|
||||
seen_inferred.add(tgt_label)
|
||||
elif tgt_label in extracted_targets:
|
||||
@@ -295,7 +295,7 @@ def test_environment_attribute_typed_receiver_resolves(tmp_path: Path):
|
||||
and _label(result, e["target"]) == ".reset()"), None)
|
||||
assert edge is not None, "store.reset() must resolve to Store.reset"
|
||||
assert _label(result, edge["source"]) == ".go()"
|
||||
assert edge["confidence"] == "INFERRED" and edge["confidence_score"] == 0.8
|
||||
assert edge["confidence"] == "INFERRED" and edge["confidence_score"] == 0.85
|
||||
|
||||
|
||||
def test_environment_keypath_and_dotted_forms_are_skipped(tmp_path: Path):
|
||||
@@ -368,7 +368,7 @@ def test_factory_returned_receiver_resolves(tmp_path: Path):
|
||||
assert (".local()", "calls", ".go()") in calls # method-local receiver
|
||||
for e in result["edges"]:
|
||||
if e.get("relation") == "calls" and _label(result, e["target"]) == ".go()":
|
||||
assert e["confidence"] == "INFERRED" and e["confidence_score"] == 0.8
|
||||
assert e["confidence"] == "INFERRED" and e["confidence_score"] == 0.85
|
||||
|
||||
|
||||
def test_factory_receiver_resolves_through_cross_file_extension(tmp_path: Path):
|
||||
|
||||
@@ -105,7 +105,7 @@ def test_dispatch_edge_shape(tmp_path):
|
||||
_, r = _extract(tmp_path, _INJECTED)
|
||||
edge = next(e for e in r["edges"] if e["relation"] == "dispatches_to")
|
||||
assert edge["confidence"] == "INFERRED"
|
||||
assert edge["confidence_score"] == 0.9
|
||||
assert edge["confidence_score"] == 0.85
|
||||
assert edge["context"] == "call"
|
||||
assert edge["source_file"].endswith("RemoteStore.swift")
|
||||
|
||||
|
||||
Reference in new issue
Block a user