codegraph-brain 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. cgis/__init__.py +10 -0
  2. cgis/__main__.py +16 -0
  3. cgis/api/.gitkeep +0 -0
  4. cgis/api/__init__.py +1 -0
  5. cgis/api/mcp_server.py +550 -0
  6. cgis/cli.py +1516 -0
  7. cgis/core/.gitkeep +0 -0
  8. cgis/core/models.py +140 -0
  9. cgis/extractors/.gitkeep +0 -0
  10. cgis/extractors/_python_ast.py +194 -0
  11. cgis/extractors/_python_classes.py +126 -0
  12. cgis/extractors/_python_functions.py +312 -0
  13. cgis/extractors/_python_imports.py +188 -0
  14. cgis/extractors/_python_types.py +84 -0
  15. cgis/extractors/base.py +42 -0
  16. cgis/extractors/python_extractor.py +235 -0
  17. cgis/extractors/typescript_extractor.py +310 -0
  18. cgis/guardian/__init__.py +1 -0
  19. cgis/guardian/bench.py +199 -0
  20. cgis/guardian/chunked.py +240 -0
  21. cgis/guardian/chunker.py +148 -0
  22. cgis/guardian/collector.py +309 -0
  23. cgis/guardian/core.py +120 -0
  24. cgis/guardian/diff_index.py +151 -0
  25. cgis/guardian/findings.py +70 -0
  26. cgis/guardian/github_poster.py +133 -0
  27. cgis/guardian/metrics.py +108 -0
  28. cgis/guardian/prompts.py +217 -0
  29. cgis/guardian/providers/__init__.py +1 -0
  30. cgis/guardian/providers/base.py +112 -0
  31. cgis/guardian/providers/gemini.py +83 -0
  32. cgis/guardian/providers/mistral.py +83 -0
  33. cgis/guardian/providers/ollama.py +99 -0
  34. cgis/guardian/recording.py +82 -0
  35. cgis/guardian/render.py +107 -0
  36. cgis/guardian/runner.py +295 -0
  37. cgis/guardian/skeptic.py +208 -0
  38. cgis/pipeline.py +252 -0
  39. cgis/py.typed +0 -0
  40. cgis/query/analysis/__init__.py +0 -0
  41. cgis/query/analysis/analyzer.py +241 -0
  42. cgis/query/analysis/anomaly.py +34 -0
  43. cgis/query/analysis/cohesion.py +277 -0
  44. cgis/query/analysis/health.py +128 -0
  45. cgis/query/analysis/suggest_service.py +221 -0
  46. cgis/query/context/__init__.py +0 -0
  47. cgis/query/context/audit.py +134 -0
  48. cgis/query/context/context_service.py +129 -0
  49. cgis/query/context/prompt.py +184 -0
  50. cgis/query/context/snippet.py +96 -0
  51. cgis/query/drift/__init__.py +0 -0
  52. cgis/query/drift/_scc.py +90 -0
  53. cgis/query/drift/drift.py +867 -0
  54. cgis/query/drift/drift_service.py +217 -0
  55. cgis/query/drift/fingerprint.py +255 -0
  56. cgis/query/drift/fractal.py +292 -0
  57. cgis/query/drift/ontology_init.py +470 -0
  58. cgis/query/drift/quotient.py +75 -0
  59. cgis/query/drift/triads.py +234 -0
  60. cgis/query/engine.py +170 -0
  61. cgis/query/fqn.py +65 -0
  62. cgis/query/render/__init__.py +0 -0
  63. cgis/query/render/graph_json.py +42 -0
  64. cgis/query/render/mermaid.py +235 -0
  65. cgis/query/render/metrics.py +346 -0
  66. cgis/resolver/.gitkeep +0 -0
  67. cgis/resolver/__init__.py +1 -0
  68. cgis/resolver/engine.py +145 -0
  69. cgis/resolver/indices.py +184 -0
  70. cgis/resolver/symbols.py +179 -0
  71. cgis/resolver/uplift.py +263 -0
  72. cgis/storage/.gitkeep +0 -0
  73. cgis/storage/sqlite_store.py +589 -0
  74. codegraph_brain-0.6.0.dist-info/METADATA +240 -0
  75. codegraph_brain-0.6.0.dist-info/RECORD +78 -0
  76. codegraph_brain-0.6.0.dist-info/WHEEL +4 -0
  77. codegraph_brain-0.6.0.dist-info/entry_points.txt +3 -0
  78. codegraph_brain-0.6.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,263 @@
1
+ """Semantic Uplift Engine — enriches the code graph with ontology classes and domain tags."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fnmatch
6
+ import re
7
+ from collections import deque
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ import yaml
12
+
13
+ from cgis.core.models import (
14
+ VIRTUAL_FILE_PATH,
15
+ Edge,
16
+ EdgeType,
17
+ Node,
18
+ NodeNamespace,
19
+ NodeType,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from cgis.storage.sqlite_store import SQLiteStore
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Ontology class mapping table (Phase 1)
27
+ # ---------------------------------------------------------------------------
28
+
29
+ _ONTOLOGY_CLASS: dict[NodeType, str] = {
30
+ NodeType.FILE: "File",
31
+ NodeType.MODULE: "Module",
32
+ NodeType.CLASS: "Class",
33
+ NodeType.FUNCTION: "Function",
34
+ NodeType.METHOD: "Method",
35
+ NodeType.VARIABLE: "Variable",
36
+ NodeType.IMPORT: "Import",
37
+ NodeType.EXPORT: "Export",
38
+ NodeType.API_ENDPOINT: "APIEndpoint",
39
+ NodeType.ROUTE_HANDLER: "RouteHandler",
40
+ NodeType.DB_TABLE: "DBTable",
41
+ NodeType.DB_QUERY: "DBQuery",
42
+ NodeType.EVENT: "Event",
43
+ NodeType.DOMAIN_CONCEPT: "DomainConcept",
44
+ }
45
+
46
+ _STRUCTURAL_EDGE_TYPES = frozenset({EdgeType.CONTAINS, EdgeType.DECLARES})
47
+
48
+
49
+ def _load_domains_config(config_path: str) -> dict[str, Any]:
50
+ """Read and parse a domains.yaml file, returning the `domains` mapping."""
51
+ try:
52
+ with Path(config_path).open(encoding="utf-8") as fh:
53
+ data = yaml.safe_load(fh)
54
+ except OSError as e:
55
+ msg = f"Cannot read domains config {config_path!r}: {e}"
56
+ raise ValueError(msg) from e
57
+ except yaml.YAMLError as e:
58
+ msg = f"Invalid YAML in domains config {config_path!r}: {e}"
59
+ raise ValueError(msg) from e
60
+ if not isinstance(data, dict):
61
+ return {}
62
+ domains: dict[str, Any] = data.get("domains") or {}
63
+ return domains
64
+
65
+
66
+ class SemanticUpliftEngine:
67
+ """
68
+ Post-processing pass that enriches the graph with semantic metadata.
69
+
70
+ Run after ResolverEngine has produced fully resolved edges. Operates
71
+ directly on the SQLiteStore — reads, updates, and saves in-place.
72
+
73
+ Phases:
74
+ 1. Ontology class mapping — NodeType → ontology_class string
75
+ 2. Heuristic domain tagging — file_path / fqn pattern matching from domains.yaml
76
+ 3. Structural propagation — domains flow FILE → CLASS → METHOD via CONTAINS/DECLARES
77
+ 4. Domain dependency inference — emit DOMAIN_DEPENDS_ON edges from cross-domain CALLS
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ store: SQLiteStore,
83
+ domains_config_path: str | None = None,
84
+ ) -> None:
85
+ """Bind to a store and optionally load domain heuristics from a YAML config."""
86
+ self._store = store
87
+ self._domains: dict[str, Any] = {}
88
+ if domains_config_path:
89
+ self._domains = _load_domains_config(domains_config_path)
90
+
91
+ def execute_uplift(self) -> None:
92
+ """Run all phases against the store in a single pass."""
93
+ self._store.delete_semantic_uplift()
94
+ nodes = self._store.get_all_nodes()
95
+ edges = self._store.get_all_edges()
96
+
97
+ original_nodes: dict[str, Node] = {n.id: n for n in nodes}
98
+ nodes_map = dict(original_nodes)
99
+
100
+ nodes_map = _phase1_map_ontology_classes(nodes_map)
101
+
102
+ # Always reset domain tags — ensures deterministic state whether or not a
103
+ # domains config is provided; prevents stale tags from driving phase 4.
104
+ # Only copy nodes that actually carry domains to avoid unnecessary allocations.
105
+ nodes_map = {
106
+ nid: n.model_copy(update={"domains": []}) if n.domains else n
107
+ for nid, n in nodes_map.items()
108
+ }
109
+
110
+ if self._domains:
111
+ nodes_map = _phase2_apply_heuristic_tagging(nodes_map, self._domains)
112
+ nodes_map = _phase3_propagate_domains(nodes_map, edges)
113
+
114
+ domain_nodes, domain_edges = _phase4_infer_domain_dependencies(nodes_map, edges)
115
+
116
+ changed_nodes = [n for node_id, n in nodes_map.items() if n != original_nodes[node_id]]
117
+ if changed_nodes:
118
+ self._store.upsert_nodes(changed_nodes)
119
+ if domain_nodes:
120
+ self._store.upsert_nodes(domain_nodes)
121
+ if domain_edges:
122
+ self._store.upsert_edges(domain_edges)
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # Phase implementations (module-level for testability)
127
+ # ---------------------------------------------------------------------------
128
+
129
+
130
+ def _phase1_map_ontology_classes(nodes_map: dict[str, Node]) -> dict[str, Node]:
131
+ """Assign ontology_class to every node based on its NodeType."""
132
+ result: dict[str, Node] = {}
133
+ for node_id, node in nodes_map.items():
134
+ ontology_class = _ONTOLOGY_CLASS.get(node.type)
135
+ if ontology_class is not None and node.ontology_class != ontology_class:
136
+ result[node_id] = node.model_copy(update={"ontology_class": ontology_class})
137
+ else:
138
+ result[node_id] = node
139
+ return result
140
+
141
+
142
+ def _compile_patterns(patterns: list[str]) -> list[re.Pattern[str]]:
143
+ """Pre-compile fnmatch glob patterns into regex for efficient repeated matching."""
144
+ return [re.compile(fnmatch.translate(p)) for p in patterns]
145
+
146
+
147
+ def _phase2_apply_heuristic_tagging(
148
+ nodes_map: dict[str, Node],
149
+ domains: dict[str, Any],
150
+ ) -> dict[str, Node]:
151
+ """Tag nodes with domain labels using file_path and fqn patterns from domains config."""
152
+ result = dict(nodes_map)
153
+ for domain_name, raw_cfg in domains.items():
154
+ domain_cfg: dict[str, Any] = raw_cfg or {}
155
+ heuristics: dict[str, Any] = domain_cfg.get("heuristics") or {}
156
+ fp_patterns = _compile_patterns(heuristics.get("file_path_patterns") or [])
157
+ fqn_patterns = _compile_patterns(heuristics.get("fqn_patterns") or [])
158
+
159
+ for node_id, node in nodes_map.items():
160
+ if node.namespace != NodeNamespace.INTERNAL:
161
+ continue
162
+ matched = any(p.match(node.file_path) for p in fp_patterns) or any(
163
+ p.match(node.id) for p in fqn_patterns
164
+ )
165
+ if matched and domain_name not in result[node_id].domains:
166
+ result[node_id] = result[node_id].model_copy(
167
+ update={"domains": sorted({*result[node_id].domains, domain_name})}
168
+ )
169
+ return result
170
+
171
+
172
+ def _build_children_map(edges: list[Edge]) -> dict[str, list[str]]:
173
+ """Build a parent → [child, ...] mapping from CONTAINS/DECLARES structural edges."""
174
+ children: dict[str, list[str]] = {}
175
+ for edge in edges:
176
+ if edge.type in _STRUCTURAL_EDGE_TYPES:
177
+ children.setdefault(edge.source, []).append(edge.target)
178
+ return children
179
+
180
+
181
+ def _phase3_propagate_domains(
182
+ nodes_map: dict[str, Node],
183
+ edges: list[Edge],
184
+ ) -> dict[str, Node]:
185
+ """Propagate domains downward through CONTAINS/DECLARES structural edges (O(V+E) BFS)."""
186
+ children = _build_children_map(edges)
187
+ result = dict(nodes_map)
188
+ queue: deque[str] = deque(node_id for node_id, node in result.items() if node.domains)
189
+ in_queue: set[str] = set(queue)
190
+
191
+ while queue:
192
+ node_id = queue.popleft()
193
+ in_queue.discard(node_id)
194
+ node = result[node_id]
195
+ for child_id in children.get(node_id, []):
196
+ child = result.get(child_id)
197
+ if child is None:
198
+ continue
199
+ merged = sorted(set(child.domains) | set(node.domains))
200
+ if merged != child.domains:
201
+ result[child_id] = child.model_copy(update={"domains": merged})
202
+ if child_id not in in_queue:
203
+ queue.append(child_id)
204
+ in_queue.add(child_id)
205
+ return result
206
+
207
+
208
+ def _collect_cross_domain_pairs(
209
+ src_domains: list[str],
210
+ tgt_domains: list[str],
211
+ dep_pairs: set[tuple[str, str]],
212
+ ) -> None:
213
+ """Add all cross-domain (src, tgt) pairs where src_domain != tgt_domain."""
214
+ for src_domain in src_domains:
215
+ for tgt_domain in tgt_domains:
216
+ if src_domain != tgt_domain:
217
+ dep_pairs.add((src_domain, tgt_domain))
218
+
219
+
220
+ def _phase4_infer_domain_dependencies(
221
+ nodes_map: dict[str, Node],
222
+ edges: list[Edge],
223
+ ) -> tuple[list[Node], list[Edge]]:
224
+ """Emit DOMAIN_DEPENDS_ON edges for every cross-domain CALLS relationship."""
225
+ dep_pairs: set[tuple[str, str]] = set()
226
+ for edge in edges:
227
+ if edge.type != EdgeType.CALLS:
228
+ continue
229
+ src = nodes_map.get(edge.source)
230
+ tgt = nodes_map.get(edge.target)
231
+ if src is None or tgt is None:
232
+ continue
233
+ _collect_cross_domain_pairs(src.domains, tgt.domains, dep_pairs)
234
+
235
+ domain_ids: set[str] = set()
236
+ new_edges: list[Edge] = []
237
+ for src_domain, tgt_domain in dep_pairs:
238
+ domain_ids.add(src_domain)
239
+ domain_ids.add(tgt_domain)
240
+ new_edges.append(
241
+ Edge(
242
+ id=f"domain:{src_domain}->domain:{tgt_domain}",
243
+ type=EdgeType.DOMAIN_DEPENDS_ON,
244
+ source=f"domain:{src_domain}",
245
+ target=f"domain:{tgt_domain}",
246
+ confidence=0.8,
247
+ )
248
+ )
249
+
250
+ domain_nodes = [
251
+ Node(
252
+ id=f"domain:{name}",
253
+ type=NodeType.DOMAIN_CONCEPT,
254
+ name=name,
255
+ file_path=VIRTUAL_FILE_PATH,
256
+ start_line=0,
257
+ end_line=0,
258
+ namespace=NodeNamespace.INTERNAL,
259
+ ontology_class="DomainConcept",
260
+ )
261
+ for name in domain_ids
262
+ ]
263
+ return domain_nodes, new_edges
cgis/storage/.gitkeep ADDED
File without changes