codegraph-ir 0.1.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 (63) hide show
  1. cgir/__init__.py +6 -0
  2. cgir/analyses/__init__.py +7 -0
  3. cgir/analyses/call_graph.py +114 -0
  4. cgir/analyses/cfg.py +320 -0
  5. cgir/analyses/effects.py +95 -0
  6. cgir/analyses/entrypoints.py +55 -0
  7. cgir/analyses/param_flow.py +75 -0
  8. cgir/analyses/pdg.py +75 -0
  9. cgir/analyses/purity.py +37 -0
  10. cgir/analyses/reaching_defs.py +115 -0
  11. cgir/analyses/symbols.py +106 -0
  12. cgir/api/__init__.py +1 -0
  13. cgir/api/mcp_server.py +172 -0
  14. cgir/api/server.py +107 -0
  15. cgir/cli.py +688 -0
  16. cgir/config.py +22 -0
  17. cgir/export/__init__.py +5 -0
  18. cgir/export/graphml.py +52 -0
  19. cgir/export/html_viz.py +1087 -0
  20. cgir/export/json_export.py +37 -0
  21. cgir/export/mermaid.py +57 -0
  22. cgir/export/neo4j.py +11 -0
  23. cgir/hooks.py +189 -0
  24. cgir/ir/__init__.py +16 -0
  25. cgir/ir/component_spec.py +100 -0
  26. cgir/ir/edges.py +31 -0
  27. cgir/ir/graph.py +132 -0
  28. cgir/ir/nodes.py +38 -0
  29. cgir/languages/__init__.py +25 -0
  30. cgir/languages/base.py +221 -0
  31. cgir/languages/cache.py +73 -0
  32. cgir/languages/python.py +1145 -0
  33. cgir/languages/registry.py +24 -0
  34. cgir/languages/typescript.py +853 -0
  35. cgir/manifest.py +77 -0
  36. cgir/pipeline.py +54 -0
  37. cgir/py.typed +0 -0
  38. cgir/regenerate/__init__.py +6 -0
  39. cgir/regenerate/prompt_pack.py +16 -0
  40. cgir/regenerate/regenerator.py +108 -0
  41. cgir/report/__init__.py +5 -0
  42. cgir/report/diff.py +226 -0
  43. cgir/report/flow.py +84 -0
  44. cgir/report/impact.py +234 -0
  45. cgir/report/lint.py +84 -0
  46. cgir/report/pack.py +271 -0
  47. cgir/report/stats.py +121 -0
  48. cgir/slicing/__init__.py +5 -0
  49. cgir/slicing/slicer.py +174 -0
  50. cgir/sources/__init__.py +6 -0
  51. cgir/sources/base.py +14 -0
  52. cgir/sources/codeql_source.py +13 -0
  53. cgir/sources/joern_source.py +13 -0
  54. cgir/sources/tree_sitter_source.py +270 -0
  55. cgir/trace/__init__.py +5 -0
  56. cgir/trace/trace_map.py +83 -0
  57. cgir/verify.py +173 -0
  58. cgir/watch.py +171 -0
  59. codegraph_ir-0.1.0.dist-info/METADATA +143 -0
  60. codegraph_ir-0.1.0.dist-info/RECORD +63 -0
  61. codegraph_ir-0.1.0.dist-info/WHEEL +4 -0
  62. codegraph_ir-0.1.0.dist-info/entry_points.txt +2 -0
  63. codegraph_ir-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,83 @@
1
+ """Map ``path:line`` source locations to ComponentSpec ids.
2
+
3
+ Used by ``cgir trace`` to answer "which component owns this line?". For now
4
+ we index by start/end-line ranges per Function/Method. The future PDG pass
5
+ will refine this to statement-level traces.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ from cgir.ir.graph import RepoGraph
15
+ from cgir.ir.nodes import NodeKind
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class _Span:
20
+ path: str
21
+ start: int
22
+ end: int
23
+ component_id: str
24
+
25
+
26
+ class TraceMap:
27
+ def __init__(self, spans: list[_Span]) -> None:
28
+ self._by_path: dict[str, list[_Span]] = {}
29
+ for span in spans:
30
+ self._by_path.setdefault(span.path, []).append(span)
31
+
32
+ def lookup(self, path: str, line: int) -> str | None:
33
+ for span in self._by_path.get(path, []):
34
+ if span.start <= line <= span.end:
35
+ return span.component_id
36
+ return None
37
+
38
+ def to_jsonable(self) -> list[dict[str, str | int]]:
39
+ return [
40
+ {"path": s.path, "start": s.start, "end": s.end, "component_id": s.component_id}
41
+ for spans in self._by_path.values()
42
+ for s in spans
43
+ ]
44
+
45
+ @classmethod
46
+ def from_jsonable(cls, data: list[dict[str, str | int]]) -> TraceMap:
47
+ spans = [
48
+ _Span(
49
+ path=str(d["path"]),
50
+ start=int(d["start"]),
51
+ end=int(d["end"]),
52
+ component_id=str(d["component_id"]),
53
+ )
54
+ for d in data
55
+ ]
56
+ return cls(spans)
57
+
58
+ def write(self, path: Path) -> None:
59
+ path.write_text(json.dumps(self.to_jsonable(), indent=2, sort_keys=True))
60
+
61
+ @classmethod
62
+ def read(cls, path: Path) -> TraceMap:
63
+ return cls.from_jsonable(json.loads(path.read_text()))
64
+
65
+
66
+ def build_trace_map(graph: RepoGraph) -> TraceMap:
67
+ spans: list[_Span] = []
68
+ for node in graph.nodes():
69
+ if node.kind not in {NodeKind.Function, NodeKind.Method}:
70
+ continue
71
+ if node.path is None or node.start_line is None or node.end_line is None:
72
+ continue
73
+ qual = node.attrs.get("qualname") if node.attrs else None
74
+ component_id = str(qual) if isinstance(qual, str) else node.name
75
+ spans.append(
76
+ _Span(
77
+ path=node.path,
78
+ start=node.start_line,
79
+ end=node.end_line,
80
+ component_id=component_id,
81
+ )
82
+ )
83
+ return TraceMap(spans)
cgir/verify.py ADDED
@@ -0,0 +1,173 @@
1
+ """cgir verify — the trust loop for an LLM-written component.
2
+
3
+ Splice a candidate implementation into a *copy* of the repo at the
4
+ component's span, rescan, and contract-diff the result against the indexed
5
+ spec. Answers the question no compiler or test suite answers directly:
6
+ **did this rewrite change what the component is** — its effects, purity,
7
+ kind, call surface, entrypoint? Optionally also runs the component's
8
+ linked tests (behavior on top of contract).
9
+
10
+ Deterministic and offline. This is what lets an agent (or a CI gate) trust
11
+ a generated change before it lands.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ import shutil
18
+ import subprocess
19
+ import sys
20
+ import tempfile
21
+ from dataclasses import asdict, dataclass, field
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ from cgir.export.json_export import read_specs
26
+ from cgir.ir.graph import RepoGraph
27
+ from cgir.ir.nodes import Node, NodeKind
28
+ from cgir.pipeline import scan_repo
29
+ from cgir.report.diff import compute_diff, violations
30
+
31
+ _IGNORE = shutil.ignore_patterns(
32
+ ".git", ".venv", "venv", "node_modules", "__pycache__", ".cgir", "outputs", "site"
33
+ )
34
+
35
+
36
+ @dataclass(slots=True)
37
+ class VerifyResult:
38
+ component_id: str
39
+ contract_ok: bool
40
+ violations: list[str] = field(default_factory=list)
41
+ drift: dict[str, Any] = field(default_factory=dict)
42
+ tests_ran: list[str] = field(default_factory=list)
43
+ tests_ok: bool | None = None
44
+ detail: str = ""
45
+
46
+ def to_dict(self) -> dict[str, Any]:
47
+ return asdict(self)
48
+
49
+
50
+ def verify(
51
+ index_dir: Path,
52
+ component_id: str,
53
+ candidate: str,
54
+ repo: Path,
55
+ fail_on: list[str] | None = None,
56
+ run_tests: bool = False,
57
+ ) -> VerifyResult:
58
+ old_specs = read_specs(index_dir)
59
+ if component_id not in {s.id for s in old_specs}:
60
+ raise KeyError(component_id)
61
+ node = _find_node(index_dir, component_id)
62
+ if node is None or node.path is None or node.start_line is None or node.end_line is None:
63
+ raise KeyError(f"{component_id}: no source span in index")
64
+
65
+ shadow = Path(tempfile.mkdtemp(prefix="cgir-verify-"))
66
+ try:
67
+ repo = repo.resolve()
68
+ shutil.copytree(repo, shadow / "repo", ignore=_IGNORE, dirs_exist_ok=True)
69
+ target_file = shadow / "repo" / node.path
70
+ _splice(target_file, node.start_line, node.end_line, candidate)
71
+
72
+ new_index = shadow / "idx"
73
+ scan_repo(shadow / "repo", new_index)
74
+ new_specs = read_specs(new_index)
75
+
76
+ diff = compute_diff(old_specs, new_specs)
77
+ drift: dict[str, Any] = next(
78
+ (c["fields"] for c in diff["changed"] if c["id"] == component_id), {}
79
+ )
80
+ target_diff = {
81
+ "changed": [c for c in diff["changed"] if c["id"] == component_id],
82
+ "entrypoints": {
83
+ key: [e for e in items if e.get("id") == component_id]
84
+ for key, items in diff["entrypoints"].items()
85
+ },
86
+ }
87
+ viol = violations(target_diff, fail_on or [])
88
+
89
+ tests_ran: list[str] = []
90
+ tests_ok: bool | None = None
91
+ detail = ""
92
+ if run_tests:
93
+ tests_ran = _linked_tests(shadow / "repo", component_id)
94
+ if tests_ran:
95
+ tests_ok, detail = _run_tests(shadow / "repo", tests_ran)
96
+
97
+ return VerifyResult(
98
+ component_id=component_id,
99
+ contract_ok=not drift,
100
+ violations=viol,
101
+ drift=drift,
102
+ tests_ran=tests_ran,
103
+ tests_ok=tests_ok,
104
+ detail=detail,
105
+ )
106
+ finally:
107
+ shutil.rmtree(shadow, ignore_errors=True)
108
+
109
+
110
+ def _find_node(index_dir: Path, component_id: str) -> Node | None:
111
+ graph_path = index_dir / "repo_graph.json"
112
+ if not graph_path.exists():
113
+ return None
114
+ import json
115
+
116
+ graph = RepoGraph.from_jsonable(json.loads(graph_path.read_text()))
117
+ for node in graph.nodes():
118
+ if node.kind in {NodeKind.Function, NodeKind.Method} and (
119
+ node.attrs.get("qualname") == component_id
120
+ ):
121
+ return node
122
+ return None
123
+
124
+
125
+ def _splice(target_file: Path, start_line: int, end_line: int, candidate: str) -> None:
126
+ lines = target_file.read_text().splitlines()
127
+ original_def = lines[start_line - 1] if start_line - 1 < len(lines) else ""
128
+ new_lines = _reindent(candidate, original_def).rstrip("\n").splitlines()
129
+ spliced = lines[: start_line - 1] + new_lines + lines[end_line:]
130
+ target_file.write_text("\n".join(spliced) + "\n")
131
+
132
+
133
+ def _reindent(candidate: str, expected_def: str) -> str:
134
+ body = candidate.rstrip("\n").splitlines()
135
+ if not body:
136
+ return candidate
137
+ got = _leading_ws(body[0])
138
+ want = _leading_ws(expected_def)
139
+ if got == want:
140
+ return candidate
141
+ out = [want + line[len(got) :] if line.startswith(got) else line for line in body]
142
+ return "\n".join(out)
143
+
144
+
145
+ def _leading_ws(line: str) -> str:
146
+ match = re.match(r"\s*", line)
147
+ return match.group(0) if match else ""
148
+
149
+
150
+ def _linked_tests(repo: Path, component_id: str) -> list[str]:
151
+ """Test files that name the component (grep fallback until Sprint 25 linkage)."""
152
+ name = component_id.split(".")[-1]
153
+ pattern = re.compile(rf"\b{re.escape(name)}\b")
154
+ hits: list[str] = []
155
+ for test_dir in (repo / "tests", repo):
156
+ if not test_dir.is_dir():
157
+ continue
158
+ for path in sorted(test_dir.glob("test_*.py")):
159
+ if pattern.search(path.read_text(errors="replace")):
160
+ hits.append(str(path.relative_to(repo)))
161
+ return list(dict.fromkeys(hits))
162
+
163
+
164
+ def _run_tests(repo: Path, test_files: list[str]) -> tuple[bool, str]:
165
+ proc = subprocess.run(
166
+ [sys.executable, "-m", "pytest", "-q", "-x", *test_files],
167
+ cwd=repo,
168
+ capture_output=True,
169
+ text=True,
170
+ timeout=300,
171
+ )
172
+ tail = "\n".join((proc.stdout + proc.stderr).splitlines()[-5:])
173
+ return proc.returncode == 0, tail
cgir/watch.py ADDED
@@ -0,0 +1,171 @@
1
+ """Watch mode — keep the ``.cgir`` index live as you (or your agent) edit.
2
+
3
+ The whole local loop (``pack`` → edit → ``impact`` → ``hook``) reads a
4
+ ``.cgir`` index; a stale index feeds stale context and wrong verdicts.
5
+ ``cgir watch`` polls the repo, and on a real content change re-scans and
6
+ prints the **contract drift** against the previous index — so "you just
7
+ made this pure function hit the network" shows up a second after you save,
8
+ not in CI.
9
+
10
+ Change detection is by content hash (``source_hashes``), so editor noise
11
+ and non-source edits are free no-ops. Re-scan cost is amortised by the
12
+ process-wide parse cache in :mod:`cgir.languages.cache` (unchanged files
13
+ are never re-parsed). Per-component incremental *analysis* — recomputing
14
+ only the changed blast radius — is the next step; the machinery for it is
15
+ :func:`cgir.report.impact.compute_typed_impact`.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import json
22
+ import time
23
+ from dataclasses import dataclass, field
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ from cgir.export.json_export import read_specs
28
+ from cgir.languages import ADAPTERS
29
+ from cgir.pipeline import scan_repo
30
+ from cgir.report.diff import compute_diff
31
+ from cgir.sources.tree_sitter_source import DEFAULT_IGNORE_DIRS
32
+
33
+ _SUPPORTED_EXTS: frozenset[str] = frozenset(
34
+ ext for adapter in ADAPTERS.values() for ext in adapter.file_extensions
35
+ )
36
+ _MANIFEST_NAME = "file_hashes.json"
37
+
38
+
39
+ def _skip(rel: Path, extra_ignore: frozenset[str]) -> bool:
40
+ return any(
41
+ part.startswith(".") or part in DEFAULT_IGNORE_DIRS or part in extra_ignore
42
+ for part in rel.parts
43
+ )
44
+
45
+
46
+ def source_hashes(repo: Path, extra_ignore: frozenset[str] = frozenset()) -> dict[str, str]:
47
+ """Map repo-relative path -> sha256 for every ingestible source file."""
48
+ repo = repo.resolve()
49
+ out: dict[str, str] = {}
50
+ for ext in _SUPPORTED_EXTS:
51
+ for path in repo.rglob(f"*{ext}"):
52
+ rel = path.relative_to(repo)
53
+ if _skip(rel, extra_ignore):
54
+ continue
55
+ try:
56
+ out[str(rel)] = hashlib.sha256(path.read_bytes()).hexdigest()
57
+ except OSError:
58
+ continue
59
+ return out
60
+
61
+
62
+ def diff_hashes(old: dict[str, str], new: dict[str, str]) -> tuple[list[str], list[str], list[str]]:
63
+ """(changed, added, deleted) relative paths."""
64
+ changed = sorted(k for k in old.keys() & new.keys() if old[k] != new[k])
65
+ added = sorted(new.keys() - old.keys())
66
+ deleted = sorted(old.keys() - new.keys())
67
+ return changed, added, deleted
68
+
69
+
70
+ def read_manifest(index_dir: Path) -> dict[str, str]:
71
+ path = index_dir / _MANIFEST_NAME
72
+ if not path.exists():
73
+ return {}
74
+ try:
75
+ data = json.loads(path.read_text())
76
+ except (OSError, json.JSONDecodeError):
77
+ return {}
78
+ return data if isinstance(data, dict) else {}
79
+
80
+
81
+ def write_manifest(index_dir: Path, hashes: dict[str, str]) -> None:
82
+ index_dir.mkdir(parents=True, exist_ok=True)
83
+ (index_dir / _MANIFEST_NAME).write_text(json.dumps(hashes, indent=2, sort_keys=True))
84
+
85
+
86
+ @dataclass
87
+ class Tick:
88
+ changed: list[str] = field(default_factory=list)
89
+ added: list[str] = field(default_factory=list)
90
+ deleted: list[str] = field(default_factory=list)
91
+ reindexed: bool = False
92
+ components: int = 0
93
+ drift: dict[str, Any] | None = None
94
+ elapsed_ms: float = 0.0
95
+
96
+
97
+ def tick(repo: Path, index_dir: Path, prev_hashes: dict[str, str]) -> tuple[Tick, dict[str, str]]:
98
+ """One iteration: detect content changes and, if any, re-scan + diff."""
99
+ now = source_hashes(repo)
100
+ changed, added, deleted = diff_hashes(prev_hashes, now)
101
+ if not (changed or added or deleted):
102
+ return Tick(), now
103
+
104
+ old_specs = read_specs(index_dir) if (index_dir / "components").exists() else []
105
+ start = time.perf_counter()
106
+ result = scan_repo(repo, out=index_dir)
107
+ elapsed = (time.perf_counter() - start) * 1000
108
+ write_manifest(index_dir, now)
109
+ drift = compute_diff(old_specs, result.specs)
110
+ return (
111
+ Tick(
112
+ changed=changed,
113
+ added=added,
114
+ deleted=deleted,
115
+ reindexed=True,
116
+ components=len(result.specs),
117
+ drift=drift,
118
+ elapsed_ms=elapsed,
119
+ ),
120
+ now,
121
+ )
122
+
123
+
124
+ def render_tick(t: Tick) -> str:
125
+ n = len(t.changed) + len(t.added) + len(t.deleted)
126
+ head = f"↻ {n} file(s) → {t.components} components in {t.elapsed_ms:.0f}ms"
127
+ lines = [head]
128
+ drift = t.drift or {}
129
+ changed = drift.get("changed", [])
130
+ if changed:
131
+ lines.append(f" contract drift ({len(changed)}):")
132
+ for c in changed:
133
+ fields = ", ".join(f"{name} {v['old']}→{v['new']}" for name, v in c["fields"].items())
134
+ lines.append(f" ~ {c['id']}: {fields}")
135
+ else:
136
+ lines.append(" no contract drift")
137
+ return "\n".join(lines) + "\n"
138
+
139
+
140
+ def run_watch(
141
+ repo: Path,
142
+ index_dir: Path,
143
+ interval: float = 0.5,
144
+ once: bool = False,
145
+ emit: Any = print,
146
+ ) -> None:
147
+ """Poll ``repo`` and keep ``index_dir`` fresh, printing drift on change."""
148
+ prev = read_manifest(index_dir) or source_hashes(repo)
149
+ # Ensure an index exists so the first real edit can diff against it.
150
+ if not (index_dir / "components").exists():
151
+ scan_repo(repo, out=index_dir)
152
+ write_manifest(index_dir, prev)
153
+ while True:
154
+ result, prev = tick(repo, index_dir, prev)
155
+ if result.reindexed:
156
+ emit(render_tick(result).rstrip("\n"))
157
+ if once:
158
+ return
159
+ time.sleep(interval)
160
+
161
+
162
+ __all__ = [
163
+ "Tick",
164
+ "diff_hashes",
165
+ "read_manifest",
166
+ "render_tick",
167
+ "run_watch",
168
+ "source_hashes",
169
+ "tick",
170
+ "write_manifest",
171
+ ]
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: codegraph-ir
3
+ Version: 0.1.0
4
+ Summary: The deterministic contract layer for AI-modified codebases — effects, purity, contracts, and blast radius, with zero LLM calls.
5
+ Project-URL: Homepage, https://github.com/asonkiya/llm-semantic-compilers
6
+ Project-URL: Repository, https://github.com/asonkiya/llm-semantic-compilers
7
+ Project-URL: Issues, https://github.com/asonkiya/llm-semantic-compilers/issues
8
+ Author: Aryaman Sonkiya
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Aryaman Sonkiya
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: ai-agents,code-graph,code-review,contracts,effects,llm,mcp,purity,static-analysis,tree-sitter
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Programming Language :: Python :: 3.13
39
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
40
+ Classifier: Topic :: Software Development :: Quality Assurance
41
+ Classifier: Typing :: Typed
42
+ Requires-Python: >=3.11
43
+ Requires-Dist: jsonschema>=4.21
44
+ Requires-Dist: networkx>=3.2
45
+ Requires-Dist: pydantic>=2.6
46
+ Requires-Dist: tree-sitter-python<0.24,>=0.23
47
+ Requires-Dist: tree-sitter-typescript<0.24,>=0.23
48
+ Requires-Dist: tree-sitter<0.25,>=0.23
49
+ Requires-Dist: typer>=0.12
50
+ Provides-Extra: api
51
+ Requires-Dist: fastapi>=0.110; extra == 'api'
52
+ Requires-Dist: uvicorn>=0.27; extra == 'api'
53
+ Provides-Extra: dev
54
+ Requires-Dist: httpx>=0.27; extra == 'dev'
55
+ Requires-Dist: mypy>=1.10; extra == 'dev'
56
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
57
+ Requires-Dist: pytest>=8.0; extra == 'dev'
58
+ Requires-Dist: pyyaml>=6.0; extra == 'dev'
59
+ Requires-Dist: ruff>=0.5; extra == 'dev'
60
+ Requires-Dist: types-jsonschema; extra == 'dev'
61
+ Provides-Extra: llm
62
+ Requires-Dist: anthropic>=0.40; extra == 'llm'
63
+ Provides-Extra: mcp
64
+ Requires-Dist: mcp>=1.2; extra == 'mcp'
65
+ Description-Content-Type: text/markdown
66
+
67
+ # CodeGraph IR (CGIR)
68
+
69
+ **The deterministic contract layer for AI-modified codebases.** Agents write
70
+ more of the code than you can review. CGIR reads a repo and — with **zero LLM
71
+ calls** — tells you what each component *is* (effects, purity, contract,
72
+ entrypoints, call surface) and whether a change *altered* it. Think **ruff,
73
+ but for architecture instead of style**: fast, static, hallucination-free.
74
+
75
+ Distributed as **`codegraph-ir`**; the command and import package are both
76
+ `cgir`.
77
+
78
+ ## Install
79
+
80
+ ```bash
81
+ uv tool install codegraph-ir # isolated CLI (recommended); or: pipx install codegraph-ir
82
+ cgir --version
83
+ ```
84
+
85
+ For library/agent use in a project: `uv pip install codegraph-ir`
86
+ (extras: `[mcp]` for the agent server, `[api]` for the HTTP surface,
87
+ `[llm]` for regeneration).
88
+
89
+ ## The local loop
90
+
91
+ ```bash
92
+ cgir scan . # build the .cgir index (Python + TypeScript)
93
+ cgir watch . # keep it live: re-scan + show contract drift on save
94
+ cgir pack app.service.charge --repo . # minimal context bundle for one component
95
+ cgir impact app.service.charge # blast radius: affected callers, entrypoints, tests
96
+ cgir impact app.service.charge --candidate new.py --repo . # radius narrowed by the real delta
97
+ cgir verify app.service.charge --candidate new.py --repo . # contract-check an edit
98
+ cgir hook install # pre-commit seatbelt: block contract-breaking commits
99
+ ```
100
+
101
+ `pack` → edit → `impact` → `verify` → `hook`, with `watch` keeping the index
102
+ fresh underneath — an always-on membrane you and your agent both consult.
103
+
104
+ ## Gate CI on contract drift
105
+
106
+ The [GitHub Action](./docs/github-action.md) scans a PR's base and head and
107
+ fails the build on drift — a pure function that starts hitting the network, a
108
+ service that *stops* persisting, a new `POST /admin` route — deterministically,
109
+ with no per-seat LLM cost:
110
+
111
+ ```yaml
112
+ - uses: asonkiya/llm-semantic-compilers@v0
113
+ with:
114
+ fail-on: "effect-gain:net effect-gain:fs effect-gain:db effect-loss:net effect-loss:fs effect-loss:db"
115
+ ```
116
+
117
+ The default rule set is [evidence-based](./docs/gate-noise.md): replaying real
118
+ commit history, the I/O effect rules fire on ~0–10% of commits, each a genuine
119
+ change in a component's I/O surface.
120
+
121
+ ## Agents as first-class users
122
+
123
+ `cgir mcp --index .cgir` serves the index over MCP. Instead of grepping, an
124
+ agent calls `search` / `pack` to load minimal context, `impact` to see what a
125
+ change touches, and `verify` / `impact_of_change` to contract-check its own
126
+ edit before proposing it. See [`examples/`](./examples) for a worked
127
+ agent-PR case study.
128
+
129
+ ## Docs
130
+
131
+ - [`docs/strategy.md`](./docs/strategy.md) — positioning: the deterministic contract layer
132
+ - [`docs/status.md`](./docs/status.md) — what runs today, test coverage, milestones
133
+ - [`docs/gate-noise.md`](./docs/gate-noise.md) — false-alarm measurement on real history
134
+ - [`docs/github-action.md`](./docs/github-action.md) — CI contract-diff gate
135
+ - [`docs/experiment-log.md`](./docs/experiment-log.md) — rewrite-readiness / contract-preservation benchmarks
136
+ - [`docs/architecture.md`](./docs/architecture.md) — layered pipeline, data model, extension seams
137
+ - [`docs/languages.md`](./docs/languages.md) — adding a language (the `LanguageAdapter` seam)
138
+ - [`Code-IR.md`](./Code-IR.md) — full product specification
139
+ - [`RELEASING.md`](./RELEASING.md) — how to cut a release
140
+
141
+ ## License
142
+
143
+ MIT — see [`LICENSE`](./LICENSE).
@@ -0,0 +1,63 @@
1
+ cgir/__init__.py,sha256=xDGBKZVfqb2CRu_mkwEOStkQAN_BzPBLNksGAMVzaPI,153
2
+ cgir/cli.py,sha256=omPCEQ_GnhuCOUtpSk83aIdA1-JinoB5hG96ZQQ5MlU,24945
3
+ cgir/config.py,sha256=-PV5vhm_cKdRu5eYZ88GLW1e8FazgczbZ1CWal9rHSk,630
4
+ cgir/hooks.py,sha256=CZmEz6zxpPuSLcA5yAwJBiETCBZnpL4HV_9nMLgaofc,6663
5
+ cgir/manifest.py,sha256=60xXoN4638AmjaVlOYTpCXJ5Ln0C-Aabpw388cUnjb0,2628
6
+ cgir/pipeline.py,sha256=J9ukXzacB4goKTRktQYz2Qf-3WiEYPGIziHPNpzLOYU,2022
7
+ cgir/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ cgir/verify.py,sha256=9n2nV70BAdV8EF_vlx_nPKrJdmMXh9n7XuwCbjTfibc,5896
9
+ cgir/watch.py,sha256=OkaXCP45Px0qwnsVLlRWakg4qjEv7x-gWhEFZWdh0fU,5730
10
+ cgir/analyses/__init__.py,sha256=zXyy1sTQ1_vr6ngVp-wHXQ_tvfPMMoqo6Y8VDUzaPJs,298
11
+ cgir/analyses/call_graph.py,sha256=lT_hiaAnNomtkzWcxiXubHLqbhVKzVIwi7LTpgaXNyI,4432
12
+ cgir/analyses/cfg.py,sha256=22qXPTIxR_gfRe7P0SNivfs6haXdPb_vbaiSN-WkkMw,12866
13
+ cgir/analyses/effects.py,sha256=0wjkoK4z3vTEAU1YQg7Du54lo7qbL8M4gDe7GGiMmBg,4051
14
+ cgir/analyses/entrypoints.py,sha256=ZDAbfXpmu1fBwWj7moBEgo9UeEH6NzQJUwAR_I51YkI,2203
15
+ cgir/analyses/param_flow.py,sha256=CGLmylORfT_GG_xk8eNx9RvH8TGVSQ3scSGJlf25a5k,2623
16
+ cgir/analyses/pdg.py,sha256=-iDFxlZPHIqdhZL0gTKiqGyn3KhaS1ZzxPKLuOb8c4Y,2817
17
+ cgir/analyses/purity.py,sha256=jOWYnQAwjlgwHvpJYIS0yrmZv4ZUSppauYRj15CIOIg,1344
18
+ cgir/analyses/reaching_defs.py,sha256=qT_1ZJdY-h5C0Bpn8KblzFbniYTIEgdOmQCgNcPnsDo,4058
19
+ cgir/analyses/symbols.py,sha256=EhGo1M32s_DqBu2EI3VeDWTrmxrJjelraMiCL04s-90,3894
20
+ cgir/api/__init__.py,sha256=-POnqxxzBdUq0bKqENEM_I7o40tfVV59XXPOtn68zXM,75
21
+ cgir/api/mcp_server.py,sha256=ourDxB0hjT1XxLzGJxUctqTrf-huiRP9yJdksUFZ14o,6736
22
+ cgir/api/server.py,sha256=aJitZ4oU5RKpok4MUsrJXdNjg1HVnWap937wS-59GOs,3807
23
+ cgir/export/__init__.py,sha256=hq1RtvEHohA7r9WEYHjpNbswt7aEFl7q2K30LFpyEaw,149
24
+ cgir/export/graphml.py,sha256=vFaWgciM9rebPL6DiHVSEs_NRKSGp277iM6bJ7W8Bp8,1643
25
+ cgir/export/html_viz.py,sha256=AaVC1l8in9iQuJ80NBDUEgYUHlmo8B9lT2q89XwoJxw,41348
26
+ cgir/export/json_export.py,sha256=xmliikhH4jDLUAYEJ5xMIYhKdSSbM8scOlNmLkeHKAY,1284
27
+ cgir/export/mermaid.py,sha256=iBqa8fAc4zDWJ_ir7Jf1mi4b9MJiRcaYvUKsAlU1Hic,2100
28
+ cgir/export/neo4j.py,sha256=FGxJdYULOGlLKSJ3-IUcMDV5UPmJWYC0Uj7FwHvvsL8,250
29
+ cgir/ir/__init__.py,sha256=bGnVcBlM2eWi8S19OJwv9zlBsmtrIMP8Fxvo0Yf2GQw,376
30
+ cgir/ir/component_spec.py,sha256=EBcxB2W_HSf0Ya0i6BmEIke1vw_csQpxNfrF5-fuLMs,3620
31
+ cgir/ir/edges.py,sha256=pDIFgpg4UVx9lcTi8Y7aoAxGYT5yEU6aEqywq-KNa4I,700
32
+ cgir/ir/graph.py,sha256=T18-WS_wi_nDfjd7P7_xjZylkUPrR0JTtpwpJqXBnKs,4592
33
+ cgir/ir/nodes.py,sha256=P5dZ4GqQZwsxANg5e_jYc5uN3R6hkUYqjP7O6n-XDsY,858
34
+ cgir/languages/__init__.py,sha256=4CQavFpRbmfdw9MjIh8nKOv0o3md8E8YC-ECiuUGXdY,779
35
+ cgir/languages/base.py,sha256=ZhZU41q722k-3pmAreHwVqYBQKG9Obun6LCdBKydX1I,7167
36
+ cgir/languages/cache.py,sha256=_wcNLb5OmVBSuIXEIxEvVOT6GA5FzhXxWKOsC1sGCm0,2878
37
+ cgir/languages/python.py,sha256=c7FuEQPUu2k7MM8OsUihnAc5KXkhPD3Yr9C0UZEgnto,41945
38
+ cgir/languages/registry.py,sha256=Fc6cGaG3p-PIy4wi46ZJh4EYN-KqpWkQ7i5StD0mits,836
39
+ cgir/languages/typescript.py,sha256=r4bvnrsYfKV_KKeGbYKtroXwOaT6DejntoyVbC71vSk,32383
40
+ cgir/regenerate/__init__.py,sha256=tES3u6yn28zcxOaKUGli5iqCc0j5W501SS-zxvsy_a8,227
41
+ cgir/regenerate/prompt_pack.py,sha256=wJs9x5Jn7ap09HHF4ULJ2yHiZ56YYhLaQ6uXZEjkDHc,584
42
+ cgir/regenerate/regenerator.py,sha256=DBpufeJItrhBkxt4v45Ltnp8BnV1gAUshxwKa-U8fzA,3517
43
+ cgir/report/__init__.py,sha256=HWO721YukDxO11NBs6kZch7tQLsP8ot1fQrYIhXGEFg,130
44
+ cgir/report/diff.py,sha256=VLvz9IyaWwfOIwOO4_B8czT0IwEPwMWIiVKTEv1ybxM,9337
45
+ cgir/report/flow.py,sha256=kbuYgBdBr36xYNkO24r6EqGFPayJYfcvkzmY0rDEN8Y,2733
46
+ cgir/report/impact.py,sha256=1MfiFWoT5MJcOIbllzJgw_WIpBmoEU61QjDf6Xd8IAA,8497
47
+ cgir/report/lint.py,sha256=ISRGKlRSX-wxbmVLgoihXPC9Wo28FvGyUUiHmcg0IHg,3198
48
+ cgir/report/pack.py,sha256=PHyzoilV5Di3XAhVwTQJVYxF3nodZOLZ65XwCzoBq6w,9212
49
+ cgir/report/stats.py,sha256=o4EQXLREo2xJhNmq-N_2V5ee4ft8S4pkhM44quMnx_I,4587
50
+ cgir/slicing/__init__.py,sha256=KbU8OUrOgXDBCgtwksaX4ueTMIbZVUOGpgp6c64kHzk,138
51
+ cgir/slicing/slicer.py,sha256=M1gTr-CXC1G2DX6MgHw2FLqj535pQJ9cv1Ksv4Rzbhk,6672
52
+ cgir/sources/__init__.py,sha256=EqasptkWdUz0CIoVgBnNOrqHjKc8iGcJcO1T2ji3Vg0,223
53
+ cgir/sources/base.py,sha256=Pp6M3PvQDQhxiC8lJmuFpJT65Sf6l1uy2WAsbMIP9-4,383
54
+ cgir/sources/codeql_source.py,sha256=6nkRedr4G_pD5njUBfJKSywJUVRlACnUo_OmghAx1XU,347
55
+ cgir/sources/joern_source.py,sha256=TeI5Uu7E3vDLLnCDVuHu1U_k1RW7lpdKAwZGzb5OuEc,343
56
+ cgir/sources/tree_sitter_source.py,sha256=oJZk_wR_4ed6sOB4Esg2mgBVC9pFZrxqbGbzHi0Hi_o,9529
57
+ cgir/trace/__init__.py,sha256=vMwb47f70-zpsupyVCC_cq6lEIe2k2Kn2W8Z87-30Hg,153
58
+ cgir/trace/trace_map.py,sha256=x5k46_5CZYPe7PxXmFSKZrbhlg1rXK0_OBHsawE5xWM,2536
59
+ codegraph_ir-0.1.0.dist-info/METADATA,sha256=PblJQXRc1W4E91YRU1xEsV8yzse93tcoyHeZUz2gXVo,6662
60
+ codegraph_ir-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
61
+ codegraph_ir-0.1.0.dist-info/entry_points.txt,sha256=-y9moAY3BkNj-4tRQW8JM_xLjioEIsgQFKRECvVZpj0,38
62
+ codegraph_ir-0.1.0.dist-info/licenses/LICENSE,sha256=ZKZDuzSRXDx9ssBTaEKsx97f7j7w5L3LIvns0fVbMLo,1072
63
+ codegraph_ir-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cgir = cgir.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aryaman Sonkiya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.