v2: semantic similarity edges, scored higher in surprising connections
This commit is contained in:
@@ -166,6 +166,11 @@ def _surprise_score(
|
|||||||
score += 1
|
score += 1
|
||||||
reasons.append("bridges separate communities")
|
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
|
# 5. Peripheral→hub: a low-degree node connecting to a high-degree one
|
||||||
deg_u = G.degree(u)
|
deg_u = G.degree(u)
|
||||||
deg_v = G.degree(v)
|
deg_v = G.degree(v)
|
||||||
|
|||||||
+2
-1
@@ -66,8 +66,9 @@ def generate(
|
|||||||
conf_tag = f"INFERRED {cscore:.2f}"
|
conf_tag = f"INFERRED {cscore:.2f}"
|
||||||
else:
|
else:
|
||||||
conf_tag = conf
|
conf_tag = conf
|
||||||
|
sem_tag = " [semantically similar]" if relation == "semantically_similar_to" else ""
|
||||||
lines += [
|
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 ""),
|
f" {files[0]} → {files[1]}" + (f" _{note}_" if note else ""),
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
|
|||||||
+7
-1
@@ -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,
|
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.
|
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,
|
If a file has YAML frontmatter (--- ... ---), copy source_url, captured_at, author,
|
||||||
contributor onto every node from that file.
|
contributor onto every node from that file.
|
||||||
|
|
||||||
@@ -218,7 +224,7 @@ confidence_score rules:
|
|||||||
- AMBIGUOUS edges: score 0.1-0.3
|
- AMBIGUOUS edges: score 0.1-0.3
|
||||||
|
|
||||||
Output exactly this JSON (no other text):
|
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**
|
**Step B3 - Collect, cache, and merge**
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "graphifyy"
|
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"
|
description = "Claude Code skill - turn any folder of code, docs, papers, images, or tweets into a queryable knowledge graph"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { text = "MIT" }
|
license = { text = "MIT" }
|
||||||
|
|||||||
@@ -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,
|
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.
|
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,
|
If a file has YAML frontmatter (--- ... ---), copy source_url, captured_at, author,
|
||||||
contributor onto every node from that file.
|
contributor onto every node from that file.
|
||||||
|
|
||||||
@@ -218,7 +224,7 @@ confidence_score rules:
|
|||||||
- AMBIGUOUS edges: score 0.1-0.3
|
- AMBIGUOUS edges: score 0.1-0.3
|
||||||
|
|
||||||
Output exactly this JSON (no other text):
|
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**
|
**Step B3 - Collect, cache, and merge**
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user