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,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
@@ -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