Navegador

Blame History Raw 106 lines
1
"""
2
FrameworkEnricher base class.
3
4
Post-processing step that runs after AST ingestion. Examines existing graph
5
nodes and promotes generic Function/Class nodes to semantic framework types
6
by adding labels/properties.
7
"""
8
9
from abc import ABC, abstractmethod
10
11
from navegador.graph.store import GraphStore
12
13
14
class EnrichmentResult:
15
"""Result of an enrichment pass."""
16
17
def __init__(self):
18
self.promoted: int = 0 # nodes that got framework-specific labels
19
self.edges_added: int = 0 # new semantic edges added
20
self.patterns_found: dict[str, int] = {} # pattern_name -> count
21
22
23
class FrameworkEnricher(ABC):
24
"""Base class for framework-specific graph enrichment."""
25
26
def __init__(self, store: GraphStore):
27
self.store = store
28
29
@property
30
@abstractmethod
31
def framework_name(self) -> str:
32
"""Name of the framework (e.g. 'django', 'fastapi')."""
33
34
@property
35
@abstractmethod
36
def detection_patterns(self) -> list[str]:
37
"""Import module names that indicate this framework is in use.
38
39
Detection queries Import nodes for exact module matches, so use
40
the actual package name (e.g. 'django', 'fastapi', 'express').
41
"""
42
43
@property
44
def detection_files(self) -> list[str]:
45
"""Filenames whose presence confirms the framework.
46
47
Override this to add file-based detection (e.g. 'manage.py' for
48
Django, 'Gemfile' for Rails). File names are matched exactly
49
against File node names — not as substrings.
50
"""
51
return []
52
53
@abstractmethod
54
def enrich(self) -> EnrichmentResult:
55
"""Run enrichment on the current graph."""
56
57
def detect(self) -> bool:
58
"""Check if the framework is present by looking for real imports and marker files."""
59
# Check Import nodes for actual framework imports
60
for pattern in self.detection_patterns:
61
result = self.store.query(
62
"MATCH (n:Import) WHERE n.name = $name OR n.module = $name RETURN count(n) AS c",
63
{"name": pattern},
64
)
65
rows = result.result_set or []
66
if rows and rows[0][0] > 0:
67
return True
68
69
# Check for marker files by exact filename match
70
for filename in self.detection_files:
71
result = self.store.query(
72
"MATCH (f:File) WHERE f.name = $name RETURN count(f) AS c",
73
{"name": filename},
74
)
75
rows = result.result_set or []
76
if rows and rows[0][0] > 0:
77
return True
78
79
return False
80
81
def _promote_node(
82
self, name: str, file_path: str, semantic_type: str, props: dict = None
83
) -> None:
84
"""Add a semantic_type property to an existing node."""
85
extra = ""
86
if props:
87
extra = ", " + ", ".join(f"n.{k} = ${k}" for k in props)
88
self.store.query(
89
f"MATCH (n) WHERE n.name = $name AND n.file_path = $file_path "
90
f"SET n.semantic_type = $semantic_type{extra}",
91
{"name": name, "file_path": file_path, "semantic_type": semantic_type, **(props or {})},
92
)
93
94
def _add_semantic_edge(
95
self, from_name: str, edge_type: str, to_name: str, props: dict = None
96
) -> None:
97
"""Create a semantic edge between two nodes by name."""
98
extra = ""
99
if props:
100
extra = " SET " + ", ".join(f"r.{k} = ${k}" for k in props)
101
self.store.query(
102
f"MATCH (a), (b) WHERE a.name = $from_name AND b.name = $to_name "
103
f"MERGE (a)-[r:{edge_type}]->(b){extra}",
104
{"from_name": from_name, "to_name": to_name, **(props or {})},
105
)
106

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button