v2: semantic similarity edges, scored higher in surprising connections

This commit is contained in:
Safi
2026-04-05 23:12:47 +01:00
parent 0f11f6a32d
commit e6fefa8a2c
6 changed files with 216 additions and 4 deletions
+5
View File
@@ -166,6 +166,11 @@ def _surprise_score(
score += 1
reasons.append("bridges separate communities")
# 4b. Semantic similarity bonus - non-obvious conceptual links score higher
if data.get("relation") == "semantically_similar_to":
score = int(score * 1.5)
reasons.append("semantically similar concepts with no structural link")
# 5. Peripheral→hub: a low-degree node connecting to a high-degree one
deg_u = G.degree(u)
deg_v = G.degree(v)
+2 -1
View File
@@ -66,8 +66,9 @@ def generate(
conf_tag = f"INFERRED {cscore:.2f}"
else:
conf_tag = conf
sem_tag = " [semantically similar]" if relation == "semantically_similar_to" else ""
lines += [
f"- `{s['source']}` --{relation}--> `{s['target']}` [{conf_tag}]",
f"- `{s['source']}` --{relation}--> `{s['target']}` [{conf_tag}]{sem_tag}",
f" {files[0]}{files[1]}" + (f" _{note}_" if note else ""),
]
else:
+7 -1
View File
@@ -207,6 +207,12 @@ Image files: use vision to understand what the image IS - do not just OCR.
DEEP_MODE (if --mode deep was given): be aggressive with INFERRED edges - indirect deps,
shared assumptions, latent couplings. Mark uncertain ones AMBIGUOUS instead of omitting.
Semantic similarity: if two concepts in this chunk solve the same problem or represent the same idea without any structural link (no import, no call, no citation), add a `semantically_similar_to` edge marked INFERRED with a confidence_score reflecting how similar they are (0.6-0.95). Examples:
- Two functions that both validate user input but never call each other
- A class in code and a concept in a paper that describe the same algorithm
- Two error types that handle the same failure mode differently
Only add these when the similarity is genuinely non-obvious and cross-cutting. Do not add them for trivially similar things.
If a file has YAML frontmatter (--- ... ---), copy source_url, captured_at, author,
contributor onto every node from that file.
@@ -218,7 +224,7 @@ confidence_score rules:
- AMBIGUOUS edges: score 0.1-0.3
Output exactly this JSON (no other text):
{"nodes":[{"id":"filestem_entityname","label":"Human Readable Name","file_type":"code|document|paper|image","source_file":"relative/path","source_location":null,"source_url":null,"captured_at":null,"author":null,"contributor":null}],"edges":[{"source":"node_id","target":"node_id","relation":"calls|implements|references|cites|conceptually_related_to|shares_data_with","confidence":"EXTRACTED|INFERRED|AMBIGUOUS","confidence_score":1.0,"source_file":"relative/path","source_location":null,"weight":1.0}],"input_tokens":0,"output_tokens":0}
{"nodes":[{"id":"filestem_entityname","label":"Human Readable Name","file_type":"code|document|paper|image","source_file":"relative/path","source_location":null,"source_url":null,"captured_at":null,"author":null,"contributor":null}],"edges":[{"source":"node_id","target":"node_id","relation":"calls|implements|references|cites|conceptually_related_to|shares_data_with|semantically_similar_to","confidence":"EXTRACTED|INFERRED|AMBIGUOUS","confidence_score":1.0,"source_file":"relative/path","source_location":null,"weight":1.0}],"input_tokens":0,"output_tokens":0}
```
**Step B3 - Collect, cache, and merge**
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "graphifyy"
version = "0.1.12"
version = "0.1.13"
description = "Claude Code skill - turn any folder of code, docs, papers, images, or tweets into a queryable knowledge graph"
readme = "README.md"
license = { text = "MIT" }
+7 -1
View File
@@ -207,6 +207,12 @@ Image files: use vision to understand what the image IS - do not just OCR.
DEEP_MODE (if --mode deep was given): be aggressive with INFERRED edges - indirect deps,
shared assumptions, latent couplings. Mark uncertain ones AMBIGUOUS instead of omitting.
Semantic similarity: if two concepts in this chunk solve the same problem or represent the same idea without any structural link (no import, no call, no citation), add a `semantically_similar_to` edge marked INFERRED with a confidence_score reflecting how similar they are (0.6-0.95). Examples:
- Two functions that both validate user input but never call each other
- A class in code and a concept in a paper that describe the same algorithm
- Two error types that handle the same failure mode differently
Only add these when the similarity is genuinely non-obvious and cross-cutting. Do not add them for trivially similar things.
If a file has YAML frontmatter (--- ... ---), copy source_url, captured_at, author,
contributor onto every node from that file.
@@ -218,7 +224,7 @@ confidence_score rules:
- AMBIGUOUS edges: score 0.1-0.3
Output exactly this JSON (no other text):
{"nodes":[{"id":"filestem_entityname","label":"Human Readable Name","file_type":"code|document|paper|image","source_file":"relative/path","source_location":null,"source_url":null,"captured_at":null,"author":null,"contributor":null}],"edges":[{"source":"node_id","target":"node_id","relation":"calls|implements|references|cites|conceptually_related_to|shares_data_with","confidence":"EXTRACTED|INFERRED|AMBIGUOUS","confidence_score":1.0,"source_file":"relative/path","source_location":null,"weight":1.0}],"input_tokens":0,"output_tokens":0}
{"nodes":[{"id":"filestem_entityname","label":"Human Readable Name","file_type":"code|document|paper|image","source_file":"relative/path","source_location":null,"source_url":null,"captured_at":null,"author":null,"contributor":null}],"edges":[{"source":"node_id","target":"node_id","relation":"calls|implements|references|cites|conceptually_related_to|shares_data_with|semantically_similar_to","confidence":"EXTRACTED|INFERRED|AMBIGUOUS","confidence_score":1.0,"source_file":"relative/path","source_location":null,"weight":1.0}],"input_tokens":0,"output_tokens":0}
```
**Step B3 - Collect, cache, and merge**
+194
View File
@@ -0,0 +1,194 @@
"""Tests for semantically_similar_to edge support."""
import networkx as nx
import pytest
from graphify.build import build_from_json
from graphify.analyze import surprising_connections, _surprise_score
from graphify.report import generate
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_extraction_with_semantic_edge():
"""Two nodes in separate files connected by a semantically_similar_to edge."""
return {
"nodes": [
{"id": "a_validate_input", "label": "validate_input", "file_type": "code",
"source_file": "auth/validators.py", "source_location": "L5"},
{"id": "b_check_input", "label": "check_input", "file_type": "code",
"source_file": "api/checks.py", "source_location": "L12"},
],
"edges": [
{
"source": "a_validate_input",
"target": "b_check_input",
"relation": "semantically_similar_to",
"confidence": "INFERRED",
"confidence_score": 0.82,
"source_file": "auth/validators.py",
"source_location": None,
"weight": 0.82,
}
],
"input_tokens": 100,
"output_tokens": 50,
}
def _make_graph_with_semantic_edge():
return build_from_json(_make_extraction_with_semantic_edge())
def _make_two_edge_graph():
"""Graph with one semantically_similar_to edge and one references edge, both cross-file."""
G = nx.Graph()
for nid, label, src in [
("a", "ValidateInput", "auth/validators.py"),
("b", "CheckInput", "api/checks.py"),
("c", "LoadConfig", "config/loader.py"),
("d", "ReadConfig", "utils/reader.py"),
]:
G.add_node(nid, label=label, source_file=src, file_type="code")
# semantically_similar_to edge
G.add_edge("a", "b", relation="semantically_similar_to", confidence="INFERRED",
confidence_score=0.82, source_file="auth/validators.py", weight=0.82,
_src="a", _tgt="b")
# plain references edge (same confidence tier)
G.add_edge("c", "d", relation="references", confidence="INFERRED",
confidence_score=0.7, source_file="config/loader.py", weight=0.7,
_src="c", _tgt="d")
return G
# ---------------------------------------------------------------------------
# Test 1: semantically_similar_to passes through build_from_json without being dropped
# ---------------------------------------------------------------------------
def test_semantic_edge_survives_build_from_json():
G = _make_graph_with_semantic_edge()
assert G.number_of_edges() == 1
u, v, data = next(iter(G.edges(data=True)))
assert data["relation"] == "semantically_similar_to"
def test_semantic_edge_nodes_present():
G = _make_graph_with_semantic_edge()
assert "a_validate_input" in G.nodes
assert "b_check_input" in G.nodes
# ---------------------------------------------------------------------------
# Test 2: confidence_score is preserved for semantically_similar_to edges
# ---------------------------------------------------------------------------
def test_semantic_edge_confidence_score_preserved():
G = _make_graph_with_semantic_edge()
u, v, data = next(iter(G.edges(data=True)))
assert data.get("confidence_score") == pytest.approx(0.82)
assert data.get("confidence") == "INFERRED"
# ---------------------------------------------------------------------------
# Test 3: surprising_connections scores semantically_similar_to edges higher
# than references edges with the same community membership
# ---------------------------------------------------------------------------
def test_semantic_edge_scores_higher_than_references():
G = _make_two_edge_graph()
communities = {0: ["a", "b"], 1: ["c", "d"]}
node_community = {"a": 0, "b": 0, "c": 1, "d": 1}
score_sem, reasons_sem = _surprise_score(
G, "a", "b", G.edges["a", "b"], node_community,
"auth/validators.py", "api/checks.py"
)
score_ref, _ = _surprise_score(
G, "c", "d", G.edges["c", "d"], node_community,
"config/loader.py", "utils/reader.py"
)
assert score_sem > score_ref
def test_semantic_edge_reason_mentions_similarity():
G = _make_two_edge_graph()
communities = {0: ["a", "b"], 1: ["c", "d"]}
node_community = {"a": 0, "b": 0, "c": 1, "d": 1}
_, reasons = _surprise_score(
G, "a", "b", G.edges["a", "b"], node_community,
"auth/validators.py", "api/checks.py"
)
assert any("similar" in r for r in reasons)
# ---------------------------------------------------------------------------
# Test 4: report renders [semantically similar] tag for these edges
# ---------------------------------------------------------------------------
def _make_report_with_semantic_surprise():
G = _make_graph_with_semantic_edge()
communities = {0: ["a_validate_input", "b_check_input"]}
cohesion = {0: 0.5}
labels = {0: "Validators"}
gods = []
surprises = [
{
"source": "validate_input",
"target": "check_input",
"relation": "semantically_similar_to",
"confidence": "INFERRED",
"confidence_score": 0.82,
"source_files": ["auth/validators.py", "api/checks.py"],
"why": "semantically similar concepts with no structural link",
}
]
detection = {"total_files": 2, "total_words": 500, "needs_graph": True, "warning": None}
tokens = {"input": 100, "output": 50}
return generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
def test_report_renders_semantically_similar_tag():
report = _make_report_with_semantic_surprise()
assert "[semantically similar]" in report
def test_report_semantic_tag_on_correct_line():
report = _make_report_with_semantic_surprise()
for line in report.splitlines():
if "semantically_similar_to" in line:
assert "[semantically similar]" in line
break
else:
pytest.fail("No line with semantically_similar_to found in report")
def test_report_no_semantic_tag_for_other_relations():
"""Non-semantic edges must not get the [semantically similar] tag."""
G = nx.Graph()
for nid, label, src in [
("x", "Alpha", "repo1/a.py"),
("y", "Beta", "repo2/b.py"),
]:
G.add_node(nid, label=label, source_file=src, file_type="code")
G.add_edge("x", "y", relation="references", confidence="EXTRACTED",
confidence_score=1.0, source_file="repo1/a.py", weight=1.0)
communities = {0: ["x", "y"]}
cohesion = {0: 0.5}
labels = {0: "Misc"}
gods = []
surprises = [
{
"source": "Alpha",
"target": "Beta",
"relation": "references",
"confidence": "EXTRACTED",
"source_files": ["repo1/a.py", "repo2/b.py"],
"why": "cross-file connection",
}
]
detection = {"total_files": 2, "total_words": 200, "needs_graph": True, "warning": None}
tokens = {"input": 50, "output": 25}
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, "./project")
assert "[semantically similar]" not in report