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.
- cgis/__init__.py +10 -0
- cgis/__main__.py +16 -0
- cgis/api/.gitkeep +0 -0
- cgis/api/__init__.py +1 -0
- cgis/api/mcp_server.py +550 -0
- cgis/cli.py +1516 -0
- cgis/core/.gitkeep +0 -0
- cgis/core/models.py +140 -0
- cgis/extractors/.gitkeep +0 -0
- cgis/extractors/_python_ast.py +194 -0
- cgis/extractors/_python_classes.py +126 -0
- cgis/extractors/_python_functions.py +312 -0
- cgis/extractors/_python_imports.py +188 -0
- cgis/extractors/_python_types.py +84 -0
- cgis/extractors/base.py +42 -0
- cgis/extractors/python_extractor.py +235 -0
- cgis/extractors/typescript_extractor.py +310 -0
- cgis/guardian/__init__.py +1 -0
- cgis/guardian/bench.py +199 -0
- cgis/guardian/chunked.py +240 -0
- cgis/guardian/chunker.py +148 -0
- cgis/guardian/collector.py +309 -0
- cgis/guardian/core.py +120 -0
- cgis/guardian/diff_index.py +151 -0
- cgis/guardian/findings.py +70 -0
- cgis/guardian/github_poster.py +133 -0
- cgis/guardian/metrics.py +108 -0
- cgis/guardian/prompts.py +217 -0
- cgis/guardian/providers/__init__.py +1 -0
- cgis/guardian/providers/base.py +112 -0
- cgis/guardian/providers/gemini.py +83 -0
- cgis/guardian/providers/mistral.py +83 -0
- cgis/guardian/providers/ollama.py +99 -0
- cgis/guardian/recording.py +82 -0
- cgis/guardian/render.py +107 -0
- cgis/guardian/runner.py +295 -0
- cgis/guardian/skeptic.py +208 -0
- cgis/pipeline.py +252 -0
- cgis/py.typed +0 -0
- cgis/query/analysis/__init__.py +0 -0
- cgis/query/analysis/analyzer.py +241 -0
- cgis/query/analysis/anomaly.py +34 -0
- cgis/query/analysis/cohesion.py +277 -0
- cgis/query/analysis/health.py +128 -0
- cgis/query/analysis/suggest_service.py +221 -0
- cgis/query/context/__init__.py +0 -0
- cgis/query/context/audit.py +134 -0
- cgis/query/context/context_service.py +129 -0
- cgis/query/context/prompt.py +184 -0
- cgis/query/context/snippet.py +96 -0
- cgis/query/drift/__init__.py +0 -0
- cgis/query/drift/_scc.py +90 -0
- cgis/query/drift/drift.py +867 -0
- cgis/query/drift/drift_service.py +217 -0
- cgis/query/drift/fingerprint.py +255 -0
- cgis/query/drift/fractal.py +292 -0
- cgis/query/drift/ontology_init.py +470 -0
- cgis/query/drift/quotient.py +75 -0
- cgis/query/drift/triads.py +234 -0
- cgis/query/engine.py +170 -0
- cgis/query/fqn.py +65 -0
- cgis/query/render/__init__.py +0 -0
- cgis/query/render/graph_json.py +42 -0
- cgis/query/render/mermaid.py +235 -0
- cgis/query/render/metrics.py +346 -0
- cgis/resolver/.gitkeep +0 -0
- cgis/resolver/__init__.py +1 -0
- cgis/resolver/engine.py +145 -0
- cgis/resolver/indices.py +184 -0
- cgis/resolver/symbols.py +179 -0
- cgis/resolver/uplift.py +263 -0
- cgis/storage/.gitkeep +0 -0
- cgis/storage/sqlite_store.py +589 -0
- codegraph_brain-0.6.0.dist-info/METADATA +240 -0
- codegraph_brain-0.6.0.dist-info/RECORD +78 -0
- codegraph_brain-0.6.0.dist-info/WHEEL +4 -0
- codegraph_brain-0.6.0.dist-info/entry_points.txt +3 -0
- codegraph_brain-0.6.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Extract the exact source lines of a focal node for the prompt compiler (#19).
|
|
2
|
+
|
|
3
|
+
Feeding an LLM the focal node's real code is the single biggest accuracy lever
|
|
4
|
+
in the GraphRAG context package, yet we never want to load whole files. This
|
|
5
|
+
leaf reads only ``start_line..end_line`` via :mod:`linecache` and degrades to an
|
|
6
|
+
empty string on any I/O problem so context generation never crashes.
|
|
7
|
+
|
|
8
|
+
It also owns the mapping from a node's *stored* ``file_path`` to a real path on
|
|
9
|
+
disk (``resolve_source_path``), since where the file actually lives is a
|
|
10
|
+
filesystem question, not a graph one.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import linecache
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _collapsed_join(root: Path, parts: tuple[str, ...]) -> Path | None:
|
|
18
|
+
"""Join ``root`` with ``parts`` when the two overlap at the boundary.
|
|
19
|
+
|
|
20
|
+
``root=/repo/src`` and ``parts=("src", "pkg", "m.py")`` describe the same
|
|
21
|
+
file from two directions; the longest matching overlap is dropped once so
|
|
22
|
+
the result is ``/repo/src/pkg/m.py`` rather than ``/repo/src/src/pkg/m.py``.
|
|
23
|
+
Returns ``None`` when the two do not overlap at all.
|
|
24
|
+
"""
|
|
25
|
+
root_parts = root.parts
|
|
26
|
+
for size in range(min(len(root_parts), len(parts)), 0, -1):
|
|
27
|
+
if root_parts[-size:] == parts[:size]:
|
|
28
|
+
return root.joinpath(*parts[size:])
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _is_existing_file(path: Path) -> bool:
|
|
33
|
+
"""True when ``path`` is an existing regular file; any OS error means "not a candidate".
|
|
34
|
+
|
|
35
|
+
``Path.is_file()`` swallows only ENOENT/ENOTDIR/EBADF/ELOOP — a candidate the
|
|
36
|
+
OS refuses to stat (EACCES on an unreadable parent, ENAMETOOLONG on a bogus
|
|
37
|
+
root) raises. This module promises degradation, never a crash, so an
|
|
38
|
+
un-stat-able candidate is simply skipped.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
return path.is_file()
|
|
42
|
+
except OSError:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def resolve_source_path(file_path: str, source_root: str = "") -> str:
|
|
47
|
+
"""Locate a node's stored ``file_path`` on disk, first existing candidate wins (#228).
|
|
48
|
+
|
|
49
|
+
Stored paths are relative to whatever directory was ingested, so the same
|
|
50
|
+
file reads as ``pkg/m.py`` (``cgis ingest ./src``) or ``src/pkg/m.py``
|
|
51
|
+
(``cgis ingest .``). Blindly prepending ``source_root`` breaks the second
|
|
52
|
+
layout — ``src/src/pkg/m.py`` — and the miss is silent, because a missing
|
|
53
|
+
snippet degrades to "(source unavailable)". Candidates are therefore tried
|
|
54
|
+
in order: the explicit ``source_root`` join, the stored path as-is
|
|
55
|
+
(CWD-relative), then the join with a duplicated boundary segment collapsed.
|
|
56
|
+
|
|
57
|
+
Backslash separators from a Windows ingest are normalised first. With no
|
|
58
|
+
``source_root`` the stored path is returned untouched; when nothing exists
|
|
59
|
+
on disk the ``source_root`` join is returned, so the caller's explicit
|
|
60
|
+
intent is what surfaces in any downstream diagnostics.
|
|
61
|
+
"""
|
|
62
|
+
relative = file_path.replace("\\", "/")
|
|
63
|
+
if not source_root:
|
|
64
|
+
return relative
|
|
65
|
+
root = Path(source_root)
|
|
66
|
+
joined = root / relative
|
|
67
|
+
candidates = [joined, Path(relative)]
|
|
68
|
+
collapsed = _collapsed_join(root, Path(relative).parts)
|
|
69
|
+
if collapsed is not None:
|
|
70
|
+
candidates.append(collapsed)
|
|
71
|
+
for candidate in candidates:
|
|
72
|
+
if _is_existing_file(candidate):
|
|
73
|
+
return str(candidate)
|
|
74
|
+
return str(joined)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def extract_snippet(file_path: str, start_line: int, end_line: int) -> str:
|
|
78
|
+
"""Return source lines ``start_line..end_line`` (inclusive, 1-based).
|
|
79
|
+
|
|
80
|
+
``start_line`` below 1 is clamped to the first line; an ``end_line`` past
|
|
81
|
+
EOF truncates to whatever lines exist. A missing or unreadable file yields
|
|
82
|
+
an empty string rather than raising — a snippet is best-effort enrichment,
|
|
83
|
+
not a hard dependency of the context package.
|
|
84
|
+
"""
|
|
85
|
+
# Drop any stale cache entry so freshly written/edited files read correctly.
|
|
86
|
+
linecache.checkcache(file_path)
|
|
87
|
+
start = max(1, start_line)
|
|
88
|
+
lines: list[str] = []
|
|
89
|
+
for i in range(start, end_line + 1):
|
|
90
|
+
line = linecache.getline(file_path, i)
|
|
91
|
+
if not line:
|
|
92
|
+
# Empty string means EOF (a blank source line is "\n"); stop early so a
|
|
93
|
+
# corrupt/huge end_line can't spin millions of empty reads.
|
|
94
|
+
break
|
|
95
|
+
lines.append(line)
|
|
96
|
+
return "".join(lines)
|
|
File without changes
|
cgis/query/drift/_scc.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Shared Tarjan SCC and adjacency utilities for the query package."""
|
|
2
|
+
|
|
3
|
+
from cgis.core.models import Edge, EdgeType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_adjacency(edges: list[Edge], allowed_types: frozenset[EdgeType]) -> dict[str, list[str]]:
|
|
7
|
+
"""Build a directed adjacency list from edges matching allowed_types."""
|
|
8
|
+
adj: dict[str, list[str]] = {}
|
|
9
|
+
for edge in edges:
|
|
10
|
+
if edge.type not in allowed_types:
|
|
11
|
+
continue
|
|
12
|
+
if edge.target.startswith(("raw_call:", "raw_import:")):
|
|
13
|
+
continue
|
|
14
|
+
adj.setdefault(edge.source, []).append(edge.target)
|
|
15
|
+
return adj
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _pop_scc(v: str, stack: list[str], on_stack: dict[str, bool]) -> list[str]:
|
|
19
|
+
"""Pop nodes from stack until v is reached, forming one SCC."""
|
|
20
|
+
scc: list[str] = []
|
|
21
|
+
while True:
|
|
22
|
+
w = stack.pop()
|
|
23
|
+
on_stack[w] = False
|
|
24
|
+
scc.append(w)
|
|
25
|
+
if w == v:
|
|
26
|
+
break
|
|
27
|
+
return scc
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _tarjan_step(
|
|
31
|
+
v: str,
|
|
32
|
+
i: int,
|
|
33
|
+
adj: dict[str, list[str]],
|
|
34
|
+
index: dict[str, int],
|
|
35
|
+
lowlink: dict[str, int],
|
|
36
|
+
on_stack: dict[str, bool],
|
|
37
|
+
stack: list[str],
|
|
38
|
+
work: list[tuple[str, int]],
|
|
39
|
+
sccs: list[list[str]],
|
|
40
|
+
counter: list[int],
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Process one step of the iterative Tarjan walk: advance or backtrack."""
|
|
43
|
+
neighbors = adj.get(v, [])
|
|
44
|
+
if i < len(neighbors):
|
|
45
|
+
work[-1] = (v, i + 1)
|
|
46
|
+
w = neighbors[i]
|
|
47
|
+
if w not in index:
|
|
48
|
+
index[w] = counter[0]
|
|
49
|
+
lowlink[w] = counter[0]
|
|
50
|
+
counter[0] += 1
|
|
51
|
+
stack.append(w)
|
|
52
|
+
on_stack[w] = True
|
|
53
|
+
work.append((w, 0))
|
|
54
|
+
elif on_stack.get(w, False):
|
|
55
|
+
lowlink[v] = min(lowlink[v], index[w])
|
|
56
|
+
else:
|
|
57
|
+
work.pop()
|
|
58
|
+
if work:
|
|
59
|
+
lowlink[work[-1][0]] = min(lowlink[work[-1][0]], lowlink[v])
|
|
60
|
+
if lowlink[v] == index[v]:
|
|
61
|
+
sccs.append(_pop_scc(v, stack, on_stack))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def tarjan_scc(adj: dict[str, list[str]]) -> list[list[str]]:
|
|
65
|
+
"""Iterative Tarjan's SCC algorithm.
|
|
66
|
+
|
|
67
|
+
Returns all strongly connected components. O(V+E), no recursion limit.
|
|
68
|
+
Any returned component with len > 1 is a cycle.
|
|
69
|
+
"""
|
|
70
|
+
index: dict[str, int] = {}
|
|
71
|
+
lowlink: dict[str, int] = {}
|
|
72
|
+
on_stack: dict[str, bool] = {}
|
|
73
|
+
stack: list[str] = []
|
|
74
|
+
sccs: list[list[str]] = []
|
|
75
|
+
counter = [0]
|
|
76
|
+
|
|
77
|
+
for start in sorted(adj):
|
|
78
|
+
if start in index:
|
|
79
|
+
continue
|
|
80
|
+
index[start] = counter[0]
|
|
81
|
+
lowlink[start] = counter[0]
|
|
82
|
+
counter[0] += 1
|
|
83
|
+
stack.append(start)
|
|
84
|
+
on_stack[start] = True
|
|
85
|
+
work: list[tuple[str, int]] = [(start, 0)]
|
|
86
|
+
while work:
|
|
87
|
+
v, i = work[-1]
|
|
88
|
+
_tarjan_step(v, i, adj, index, lowlink, on_stack, stack, work, sccs, counter)
|
|
89
|
+
|
|
90
|
+
return sccs
|