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
cgis/core/.gitkeep ADDED
File without changes
cgis/core/models.py ADDED
@@ -0,0 +1,140 @@
1
+ """Basic atoms for ontology."""
2
+
3
+ from enum import StrEnum
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+
9
+ class NodeNamespace(StrEnum):
10
+ """Classifies whether a node belongs to the ingested repo, stdlib, or third-party."""
11
+
12
+ INTERNAL = "INTERNAL"
13
+ STDLIB = "STDLIB"
14
+ EXTERNAL = "EXTERNAL"
15
+ UNKNOWN = "UNKNOWN"
16
+
17
+
18
+ VIRTUAL_FILE_PATH = "EXTERNAL"
19
+
20
+ # Edge-target encoding conventions for raw (pre-resolution) targets.
21
+ # These mirror the raw_call: / raw_dep: prefixes used by extractors and the
22
+ # resolver, but apply to self-method dispatch and class-inheritance edges.
23
+ SELF_PREFIX = "self."
24
+ RAW_CLASS_PREFIX = "raw_class:"
25
+
26
+
27
+ class NodeType(StrEnum):
28
+ """All possible node types in the Code Graph."""
29
+
30
+ # Structural (L0/L1)
31
+ FILE = "FILE"
32
+ MODULE = "MODULE"
33
+ CLASS = "CLASS"
34
+ FUNCTION = "FUNCTION"
35
+ METHOD = "METHOD"
36
+ VARIABLE = "VARIABLE"
37
+ IMPORT = "IMPORT"
38
+ EXPORT = "EXPORT"
39
+
40
+ # Runtime/Behavioral (L1/L2)
41
+ API_ENDPOINT = "API_ENDPOINT"
42
+ ROUTE_HANDLER = "ROUTE_HANDLER"
43
+ DB_TABLE = "DB_TABLE"
44
+ DB_QUERY = "DB_QUERY"
45
+ EVENT = "EVENT"
46
+
47
+ # Semantic (L3/Ontology)
48
+ DOMAIN_CONCEPT = "DOMAIN_CONCEPT"
49
+
50
+
51
+ class EdgeType(StrEnum):
52
+ """All possible relationship types between nodes."""
53
+
54
+ # Structural
55
+ CONTAINS = "CONTAINS"
56
+ DECLARES = "DECLARES"
57
+ BELONGS_TO = "BELONGS_TO"
58
+ IMPORTS = "IMPORTS"
59
+ IMPORTS_SYMBOL = "IMPORTS_SYMBOL"
60
+ EXPORTS = "EXPORTS"
61
+ EXTENDS = "EXTENDS"
62
+
63
+ # Behavioral/Execution
64
+ CALLS = "CALLS"
65
+ INSTANTIATES = "INSTANTIATES"
66
+ REFERENCES = "REFERENCES"
67
+ USES = "USES"
68
+ ROUTES_TO = "ROUTES_TO"
69
+ TRIGGERS = "TRIGGERS"
70
+ EMITS = "EMITS"
71
+ CONSUMES = "CONSUMES"
72
+ DEPENDS_ON = "DEPENDS_ON"
73
+
74
+ # Semantic (The OWL-Lite Layer)
75
+ HANDLES = "HANDLES"
76
+ TRANSFORMS = "TRANSFORMS"
77
+ VALIDATES = "VALIDATES"
78
+ AUTHORIZES = "AUTHORIZES"
79
+ PERSISTS = "PERSISTS"
80
+ DOMAIN_DEPENDS_ON = "DOMAIN_DEPENDS_ON"
81
+
82
+
83
+ class Node(BaseModel):
84
+ """
85
+ Represents a single semantic entity in the code graph.
86
+ The 'id' must be a Fully Qualified Name (FQN).
87
+ """
88
+
89
+ model_config = ConfigDict(frozen=True) # Nodes should be immutable once created
90
+
91
+ id: str = Field(..., description="Fully Qualified Name (e.g., 'pkg.mod.Class.method')")
92
+ type: NodeType
93
+ name: str
94
+
95
+ # Location metadata
96
+ file_path: str
97
+ start_line: int
98
+ end_line: int
99
+
100
+ # Language context
101
+ language: str = Field(default="python")
102
+
103
+ # Semantic enrichment (L2/L3)
104
+ ontology_class: str | None = Field(
105
+ default=None, description="Mapping to the formal ontology class"
106
+ )
107
+ domains: list[str] = Field(default_factory=list)
108
+
109
+ # Classification
110
+ namespace: NodeNamespace = Field(default=NodeNamespace.INTERNAL)
111
+
112
+ # Reliability
113
+ confidence_score: float = Field(default=1.0, ge=0.0, le=1.0)
114
+ metadata: dict[str, Any] = Field(default_factory=dict)
115
+
116
+
117
+ class Edge(BaseModel):
118
+ """
119
+ Represents a directed relationship between two nodes.
120
+ """
121
+
122
+ model_config = ConfigDict(frozen=True)
123
+
124
+ id: str = Field(..., description="Unique edge identifier")
125
+ source: str = Field(..., description="Source Node FQN")
126
+ target: str = Field(..., description="Target Node FQN")
127
+ type: EdgeType
128
+
129
+ # Strength and certainty
130
+ weight: float = Field(
131
+ default=1.0,
132
+ ge=0.0,
133
+ description="Edge strength: 1.0 for plain edges, aggregated count (>1) on quotient edges",
134
+ )
135
+ confidence: float = Field(default=1.0, ge=0.0, le=1.0)
136
+
137
+ # Contextual info (e.g., the code snippet or line number of the call)
138
+ context: str | None = None
139
+ file_path: str | None = None
140
+ line_number: int | None = None
File without changes
@@ -0,0 +1,194 @@
1
+ """Pure AST naming and path helpers shared by the Python extractor handlers.
2
+
3
+ This is a leaf module: it imports only tree-sitter types and holds no state.
4
+ Every function is pure (its output depends solely on its arguments), which lets
5
+ the extractor's handler collaborators depend on it without import cycles.
6
+ """
7
+
8
+ from tree_sitter import Node as BaseNode
9
+
10
+ PYTHON_LANG = "python"
11
+
12
+
13
+ def resolve_relative_module(
14
+ module_fqn: str, leading_dots: int, relative_path: str, is_package: bool = False
15
+ ) -> str:
16
+ """Resolve a relative import to an absolute FQN.
17
+
18
+ Args:
19
+ module_fqn: FQN of the current file, e.g. "src.cgis.extractors.python_extractor"
20
+ leading_dots: number of leading dots in the import statement
21
+ relative_path: module path after the dots, e.g. "core" for "from ..core import x"
22
+ is_package: True when the importing file is a package ``__init__.py``. Its
23
+ FQN already had the ``/__init__`` suffix stripped, so it IS the
24
+ package — one leading dot refers to the package itself and must trim
25
+ one fewer segment than a regular module would.
26
+
27
+ Returns "src.cgis.core" for leading_dots=2, relative_path="core" in the example above.
28
+ """
29
+ segments = module_fqn.split(".")
30
+ effective_dots = leading_dots - 1 if is_package and leading_dots > 0 else leading_dots
31
+ trim = min(effective_dots, len(segments))
32
+ if trim == 0:
33
+ # leading_dots == 0: not a relative import — return the module unchanged.
34
+ # (Guards against `segments[:-0]` == `[]`, which would drop the whole FQN.)
35
+ base = module_fqn
36
+ elif trim == len(segments):
37
+ base = ""
38
+ else:
39
+ base = ".".join(segments[:-trim])
40
+ if relative_path:
41
+ return f"{base}.{relative_path}" if base else relative_path
42
+ return base
43
+
44
+
45
+ def file_path_to_module_fqn(file_path: str, source_root: str | None = None) -> str:
46
+ """Convert a file path to a dot-separated module namespace.
47
+
48
+ Examples:
49
+ src/cgis/pipeline.py -> src.cgis.pipeline
50
+ src/cgis/__init__.py -> src.cgis
51
+ /abs/path/mod.py -> abs.path.mod
52
+ C:\\path\\to\\mod.py -> path.to.mod
53
+
54
+ With source_root="src":
55
+ src/cgis/pipeline.py -> cgis.pipeline
56
+ """
57
+ clean = file_path
58
+ # Strip Windows drive letter (e.g. "C:") before normalising slashes
59
+ if len(clean) >= 2 and clean[1] == ":" and clean[0].isalpha():
60
+ clean = clean[2:]
61
+ clean = clean.replace("\\", "/").lstrip("/")
62
+ if source_root:
63
+ # removeprefix("./") so CI-style roots ("cgis ingest ./src") strip too
64
+ sr = source_root.replace("\\", "/").removeprefix("./").strip("/") + "/"
65
+ if clean.startswith(sr):
66
+ clean = clean[len(sr) :]
67
+ if clean.endswith(".py"):
68
+ clean = clean[:-3]
69
+ if clean.endswith("/__init__"):
70
+ clean = clean[:-9]
71
+ return clean.replace("/", ".")
72
+
73
+
74
+ def extract_node_name(node: BaseNode | None, code_bytes: bytes) -> str:
75
+ """Extract node name from name node using byte slicing."""
76
+ if node:
77
+ start, end = node.start_byte, node.end_byte
78
+ return code_bytes[start:end].decode("utf8", errors="replace")
79
+ return "unknown"
80
+
81
+
82
+ def get_identifier(node: BaseNode, code_bytes: bytes) -> str:
83
+ """Extract name from AST node using byte slicing."""
84
+ if node.type == "identifier":
85
+ start, end = node.start_byte, node.end_byte
86
+ return code_bytes[start:end].decode("utf8", errors="replace")
87
+ if node.type == "parenthesized_expression":
88
+ for child in node.children:
89
+ if child.type not in ("(", ")", "comment"):
90
+ return get_identifier(child, code_bytes)
91
+ if node.type in ("attribute", "call", "subscript"):
92
+ return extract_nested_name(node, code_bytes)
93
+ return "unknown"
94
+
95
+
96
+ def extract_object_attr_name(node: BaseNode, code_bytes: bytes) -> str:
97
+ """Extract identifier from object/attribute nodes."""
98
+ obj_node = node.child_by_field_name("object")
99
+ attr_node = node.child_by_field_name("attribute")
100
+ if obj_node and attr_node:
101
+ obj_id = get_identifier(obj_node, code_bytes)
102
+ attr_id = get_identifier(attr_node, code_bytes)
103
+ defined = obj_id != "unknown" and attr_id != "unknown"
104
+ return f"{obj_id}.{attr_id}" if defined else "unknown"
105
+ if attr_node:
106
+ return get_identifier(attr_node, code_bytes)
107
+ return "unknown"
108
+
109
+
110
+ def extract_nested_name(node: BaseNode, code_bytes: bytes) -> str:
111
+ """Extract nested identifier from attribute/call/subscript nodes."""
112
+ if node.type == "attribute":
113
+ return extract_object_attr_name(node, code_bytes)
114
+ if node.type == "call":
115
+ func_node = node.child_by_field_name("function")
116
+ if func_node:
117
+ return get_identifier(func_node, code_bytes)
118
+ elif node.type == "subscript":
119
+ value_node = node.child_by_field_name("value")
120
+ if value_node:
121
+ return get_identifier(value_node, code_bytes)
122
+ return "unknown"
123
+
124
+
125
+ def get_fqn_prefix(node: BaseNode, code_bytes: bytes) -> str | None:
126
+ """Traverse up to find class and function names, returning them joined by dots."""
127
+ parts = []
128
+ curr = node.parent
129
+ extract_types = ("class_definition", "function_definition", "async_function_definition")
130
+ while curr:
131
+ if curr.type in extract_types:
132
+ name_node = curr.child_by_field_name("name")
133
+ parts.append(extract_node_name(name_node, code_bytes))
134
+ curr = curr.parent
135
+ return ".".join(reversed(parts)) if parts else None
136
+
137
+
138
+ def get_id(node: BaseNode, code_bytes: bytes, file_path: str, source_root: str | None) -> str:
139
+ """Generate a fully qualified function/method ID including class/function context."""
140
+ name = node.child_by_field_name("name")
141
+ node_name = extract_node_name(name, code_bytes)
142
+ prefix = get_fqn_prefix(node, code_bytes)
143
+ module = file_path_to_module_fqn(file_path, source_root)
144
+ if not module:
145
+ return f"{prefix}.{node_name}" if prefix else node_name
146
+ return f"{module}.{prefix}.{node_name}" if prefix else f"{module}.{node_name}"
147
+
148
+
149
+ def is_method(node: BaseNode) -> bool:
150
+ """Check if a function node is a method (defined inside a class)."""
151
+ curr = node.parent
152
+ while curr:
153
+ if curr.type == "class_definition":
154
+ return True
155
+ if curr.type in ("function_definition", "async_function_definition"):
156
+ return False
157
+ curr = curr.parent
158
+ return False
159
+
160
+
161
+ def is_module_level_assignment(
162
+ node: BaseNode, code_bytes: bytes, current_func_node: object | None
163
+ ) -> bool:
164
+ """True for assignments at true module level (not function, not class body)."""
165
+ return (
166
+ node.type == "assignment"
167
+ and current_func_node is None
168
+ and get_fqn_prefix(node, code_bytes) is None
169
+ )
170
+
171
+
172
+ def get_decorator_name(decorator_node: BaseNode, code_bytes: bytes) -> str | None:
173
+ """Return the decorator's callable name from a single decorator node, or None.
174
+
175
+ Skips the leading '@' token and any comments; passes the first real expression
176
+ node to get_identifier, which handles identifier / attribute / call / PEP-614
177
+ parenthesized expressions.
178
+ """
179
+ for inner in decorator_node.children:
180
+ if inner.type not in ("@", "comment"):
181
+ name = get_identifier(inner, code_bytes)
182
+ return name if name != "unknown" else None
183
+ return None
184
+
185
+
186
+ def extract_decorator_names(node: BaseNode, code_bytes: bytes) -> list[str]:
187
+ """Extract decorator names from a decorated_definition node."""
188
+ names: list[str] = []
189
+ for child in node.children:
190
+ if child.type == "decorator":
191
+ name = get_decorator_name(child, code_bytes)
192
+ if name:
193
+ names.append(name)
194
+ return names
@@ -0,0 +1,126 @@
1
+ """Class-definition handling for the Python extractor.
2
+
3
+ Emits CLASS nodes plus their CONTAINS/EXTENDS structural edges and the
4
+ abstract-class / metaclass metadata.
5
+ """
6
+
7
+ from collections.abc import Callable
8
+ from typing import Any
9
+
10
+ from tree_sitter import Node as BaseNode
11
+
12
+ from cgis.core.models import Edge, EdgeType, Node, NodeType
13
+ from cgis.extractors._python_ast import (
14
+ PYTHON_LANG,
15
+ extract_node_name,
16
+ get_id,
17
+ get_identifier,
18
+ )
19
+
20
+ _ABC_NAMES: frozenset[str] = frozenset({"ABC", "ABCMeta"})
21
+
22
+
23
+ class ClassHandler:
24
+ """Extracts CLASS nodes and their inheritance metadata from class AST nodes."""
25
+
26
+ def __init__(self, pick_source_root: Callable[[str], str | None]) -> None:
27
+ """Store the per-file source-root picker used for FQN construction."""
28
+ self._pick_source_root = pick_source_root
29
+
30
+ def process_class_node(
31
+ self,
32
+ node: BaseNode,
33
+ code_bytes: bytes,
34
+ file_path: str,
35
+ nodes: list[Node],
36
+ edges: list[Edge],
37
+ module_fqn: str,
38
+ decorators: list[str] | None = None,
39
+ ) -> None:
40
+ """Process class definition node."""
41
+ child = node.child_by_field_name("name")
42
+ node_id = get_id(node, code_bytes, file_path, self._pick_source_root(file_path))
43
+ node_name = extract_node_name(child, code_bytes)
44
+
45
+ superclass_names = self._collect_superclasses(node, node_id, file_path, code_bytes, edges)
46
+
47
+ metadata: dict[str, Any] = {}
48
+ if decorators:
49
+ metadata["decorators"] = decorators
50
+ if self._is_abstract_class(superclass_names, decorators):
51
+ metadata["is_abstract"] = True
52
+
53
+ nodes.append(
54
+ Node(
55
+ id=node_id,
56
+ type=NodeType.CLASS,
57
+ name=node_name,
58
+ file_path=file_path,
59
+ start_line=node.start_point.row + 1,
60
+ end_line=node.end_point.row + 1,
61
+ language=PYTHON_LANG,
62
+ metadata=metadata,
63
+ )
64
+ )
65
+
66
+ parts = node_id.rsplit(".", maxsplit=1)
67
+ parent_fqn = parts[0] if len(parts) > 1 else module_fqn
68
+ edges.append(
69
+ Edge(
70
+ id=f"{parent_fqn}:structural:{node_id}",
71
+ type=EdgeType.CONTAINS,
72
+ source=parent_fqn,
73
+ target=node_id,
74
+ confidence=1.0,
75
+ file_path=file_path,
76
+ )
77
+ )
78
+
79
+ def _collect_superclasses(
80
+ self,
81
+ node: BaseNode,
82
+ node_id: str,
83
+ file_path: str,
84
+ code_bytes: bytes,
85
+ edges: list[Edge],
86
+ ) -> list[str]:
87
+ """Collect superclass names and emit EXTENDS edges for a class definition node."""
88
+ names: list[str] = []
89
+ superclasses_node = node.child_by_field_name("superclasses")
90
+ if not superclasses_node:
91
+ return names
92
+ for sc in superclasses_node.children:
93
+ if sc.type in ("identifier", "attribute", "subscript"):
94
+ sc_name = get_identifier(sc, code_bytes)
95
+ if sc_name != "unknown":
96
+ names.append(sc_name)
97
+ edges.append(
98
+ Edge(
99
+ id=f"{node_id}:extends:{sc_name}",
100
+ type=EdgeType.EXTENDS,
101
+ source=node_id,
102
+ target=f"raw_class:{sc_name}",
103
+ confidence=1.0,
104
+ file_path=file_path,
105
+ )
106
+ )
107
+ elif sc.type == "keyword_argument":
108
+ meta_name = self._extract_metaclass_name(sc, code_bytes)
109
+ if meta_name:
110
+ names.append(meta_name)
111
+ return names
112
+
113
+ @staticmethod
114
+ def _is_abstract_class(superclass_names: list[str], decorators: list[str] | None) -> bool:
115
+ """Return True if class should be marked abstract (ABC/ABCMeta in bases or decorators)."""
116
+ candidates = (*superclass_names, *(decorators or []))
117
+ return any(n in _ABC_NAMES or n.rpartition(".")[-1] in _ABC_NAMES for n in candidates)
118
+
119
+ def _extract_metaclass_name(self, node: BaseNode, code_bytes: bytes) -> str | None:
120
+ """Return the metaclass name from a keyword_argument node like `metaclass=ABCMeta`."""
121
+ name_node = node.child_by_field_name("name")
122
+ value_node = node.child_by_field_name("value")
123
+ if name_node and value_node and get_identifier(name_node, code_bytes) == "metaclass":
124
+ meta_name = get_identifier(value_node, code_bytes)
125
+ return meta_name if meta_name != "unknown" else None
126
+ return None