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,37 @@
1
+ """Write the repo graph + ComponentSpec index to disk."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from cgir.ir.component_spec import ComponentSpec
9
+ from cgir.ir.graph import RepoGraph
10
+ from cgir.manifest import write_manifest
11
+
12
+
13
+ def read_specs(index_dir: Path) -> list[ComponentSpec]:
14
+ """Load every ComponentSpec from an existing index directory."""
15
+ components_dir = index_dir / "components"
16
+ return [
17
+ ComponentSpec.from_dict(json.loads(p.read_text()))
18
+ for p in sorted(components_dir.glob("*.json"))
19
+ ]
20
+
21
+
22
+ def write_index(out_dir: Path, graph: RepoGraph, specs: list[ComponentSpec]) -> None:
23
+ out_dir.mkdir(parents=True, exist_ok=True)
24
+ (out_dir / "repo_graph.json").write_text(
25
+ json.dumps(graph.to_jsonable(), indent=2, sort_keys=True)
26
+ )
27
+
28
+ components_dir = out_dir / "components"
29
+ components_dir.mkdir(parents=True, exist_ok=True)
30
+ for spec in specs:
31
+ spec.validate()
32
+ (components_dir / f"{spec.id}.json").write_text(spec.to_json())
33
+
34
+ index = [{"id": spec.id, "kind": spec.kind.value, "trace": spec.trace} for spec in specs]
35
+ (out_dir / "components_index.json").write_text(json.dumps(index, indent=2, sort_keys=True))
36
+
37
+ write_manifest(out_dir, component_count=len(specs))
cgir/export/mermaid.py ADDED
@@ -0,0 +1,57 @@
1
+ """Mermaid call-graph rendering from ComponentSpecs.
2
+
3
+ Produces ``flowchart LR`` text suitable for embedding in Markdown (GitHub,
4
+ Obsidian, mermaid.live). One node per component, one ``subgraph`` per
5
+ source file, one edge per resolved intra-repo call; nodes are colored by
6
+ :class:`~cgir.ir.component_spec.ComponentKind`.
7
+
8
+ Calls to components outside the spec set (stdlib, third-party) are skipped
9
+ rather than rendered as dangling nodes — the diagram shows *this* repo.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import re
15
+
16
+ from cgir.ir.component_spec import ComponentSpec
17
+
18
+ _KIND_STYLES: dict[str, str] = {
19
+ "pure_function": "fill:#c6f6d5,stroke:#2f855a,color:#1a202c",
20
+ "orchestrator": "fill:#bee3f8,stroke:#2b6cb0,color:#1a202c",
21
+ "state_transformer": "fill:#feebc8,stroke:#c05621,color:#1a202c",
22
+ "effect_adapter": "fill:#fed7d7,stroke:#c53030,color:#1a202c",
23
+ "unknown": "fill:#e2e8f0,stroke:#4a5568,color:#1a202c",
24
+ }
25
+
26
+
27
+ def render_call_graph(specs: list[ComponentSpec]) -> str:
28
+ """Render the component call graph as Mermaid flowchart text."""
29
+ lines = ["flowchart LR"]
30
+ known_ids = {s.id for s in specs}
31
+
32
+ by_file: dict[str, list[ComponentSpec]] = {}
33
+ for spec in specs:
34
+ file = spec.trace[0].rsplit(":", 1)[0] if spec.trace else "(unknown)"
35
+ by_file.setdefault(file, []).append(spec)
36
+
37
+ for file, group in sorted(by_file.items()):
38
+ lines.append(f' subgraph {_mermaid_id(file)}["{file}"]')
39
+ for spec in group:
40
+ lines.append(f' {_mermaid_id(spec.id)}["{spec.id}"]')
41
+ lines.append(" end")
42
+
43
+ for spec in specs:
44
+ for callee in spec.calls:
45
+ if callee in known_ids:
46
+ lines.append(f" {_mermaid_id(spec.id)} --> {_mermaid_id(callee)}")
47
+
48
+ for kind in sorted({s.kind.value for s in specs}):
49
+ lines.append(f" classDef {kind} {_KIND_STYLES[kind]}")
50
+ for spec in specs:
51
+ lines.append(f" class {_mermaid_id(spec.id)} {spec.kind.value}")
52
+
53
+ return "\n".join(lines) + "\n"
54
+
55
+
56
+ def _mermaid_id(raw: str) -> str:
57
+ return re.sub(r"[^0-9A-Za-z_]", "_", raw)
cgir/export/neo4j.py ADDED
@@ -0,0 +1,11 @@
1
+ """Neo4j export — milestone: P2-neo4j."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from cgir.ir.graph import RepoGraph
8
+
9
+
10
+ def write(out_dir: Path, graph: RepoGraph) -> None:
11
+ raise NotImplementedError("milestone: P2-neo4j")
cgir/hooks.py ADDED
@@ -0,0 +1,189 @@
1
+ """The git pre-commit seatbelt.
2
+
3
+ A local, deterministic gate on your own — and your agent's — commits.
4
+ On commit, ``run_check`` scans the last committed tree (``HEAD``) against
5
+ the *staged* tree (``git write-tree``), diffs the contracts, and reports:
6
+
7
+ * **violations** — the ``--fail-on`` drift rules that trip (defaults to the
8
+ evidence-based low-noise set from ``docs/gate-noise.md``);
9
+ * **tests to run** — the union of :func:`compute_typed_impact` over every
10
+ changed component, narrowed by *its* contract delta, so a body-only
11
+ refactor asks for nothing downstream.
12
+
13
+ Design choices that matter for a hook:
14
+
15
+ * **Scans the staged tree, not the working tree** — it checks exactly what
16
+ will be committed (``git write-tree`` materialises the index as a tree).
17
+ * **Fail-open on internal error** — a bug in CGIR must never block your
18
+ commit; only a real contract violation does.
19
+ * **Skips non-source commits** — no scan when nothing with a supported
20
+ extension is staged.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import subprocess
26
+ import tempfile
27
+ from dataclasses import dataclass, field
28
+ from pathlib import Path
29
+
30
+ from cgir.ir.component_spec import ComponentSpec
31
+ from cgir.languages import ADAPTERS
32
+ from cgir.pipeline import scan_repo
33
+ from cgir.report.diff import compute_diff, render_diff, violations
34
+ from cgir.report.impact import compute_typed_impact
35
+
36
+ # The low-noise default from the real-history noise measurement: fail only
37
+ # when something starts — or silently stops — reaching the outside world.
38
+ DEFAULT_FAIL_ON: tuple[str, ...] = (
39
+ "effect-gain:net",
40
+ "effect-gain:fs",
41
+ "effect-gain:db",
42
+ "effect-loss:net",
43
+ "effect-loss:fs",
44
+ "effect-loss:db",
45
+ )
46
+
47
+ _SUPPORTED_EXTS: frozenset[str] = frozenset(
48
+ ext for adapter in ADAPTERS.values() for ext in adapter.file_extensions
49
+ )
50
+
51
+ _HOOK_TEMPLATE = """\
52
+ #!/bin/sh
53
+ # CGIR contract seatbelt — installed by `cgir hook install`. Delete to remove.
54
+ if ! command -v cgir >/dev/null 2>&1; then
55
+ echo "cgir not on PATH; skipping contract check" >&2
56
+ exit 0
57
+ fi
58
+ exec cgir hook run {flags}
59
+ """
60
+
61
+
62
+ @dataclass
63
+ class HookResult:
64
+ checked: bool
65
+ violations: list[str] = field(default_factory=list)
66
+ changed: list[str] = field(default_factory=list)
67
+ tests: list[str] = field(default_factory=list)
68
+ diff_text: str = ""
69
+ error: str | None = None
70
+
71
+
72
+ def _git(repo: Path, *args: str) -> str:
73
+ return subprocess.check_output(["git", "-C", str(repo), *args], text=True).strip()
74
+
75
+
76
+ def _staged_supported(repo: Path) -> bool:
77
+ names = _git(repo, "diff", "--cached", "--name-only").splitlines()
78
+ return any(Path(n).suffix in _SUPPORTED_EXTS for n in names)
79
+
80
+
81
+ def _specs_of_tree(repo: Path, tree: str) -> list[ComponentSpec]:
82
+ """Scan the content of a git tree-ish out-of-tree."""
83
+ with tempfile.TemporaryDirectory() as td:
84
+ src = Path(td) / "src"
85
+ src.mkdir()
86
+ archive = subprocess.check_output(["git", "-C", str(repo), "archive", tree])
87
+ subprocess.run(["tar", "-x", "-C", str(src)], input=archive, check=True)
88
+ with tempfile.TemporaryDirectory() as od:
89
+ return scan_repo(src, out=Path(od)).specs
90
+
91
+
92
+ def run_check(repo: Path, fail_on: list[str] | None = None) -> HookResult:
93
+ """Contract-check the staged tree against HEAD. Fail-open on any error."""
94
+ rules = list(fail_on) if fail_on is not None else list(DEFAULT_FAIL_ON)
95
+ try:
96
+ if not _staged_supported(repo):
97
+ return HookResult(checked=False)
98
+ staged_tree = _git(repo, "write-tree")
99
+ try:
100
+ head_tree = _git(repo, "rev-parse", "HEAD^{tree}")
101
+ except subprocess.CalledProcessError:
102
+ head_tree = None # unborn branch — the initial commit
103
+ if head_tree == staged_tree:
104
+ return HookResult(checked=False)
105
+
106
+ base_specs = _specs_of_tree(repo, head_tree) if head_tree else []
107
+ head_specs = _specs_of_tree(repo, staged_tree)
108
+ except Exception as exc: # never block a commit because of our own bug
109
+ return HookResult(checked=False, error=f"{type(exc).__name__}: {exc}")
110
+
111
+ diff = compute_diff(base_specs, head_specs)
112
+ found = violations(diff, rules)
113
+ changed = [c["id"] for c in diff["changed"]]
114
+
115
+ tests: set[str] = set()
116
+ for change in diff["changed"]:
117
+ typed = compute_typed_impact(head_specs, change["id"], list(change["fields"].keys()))
118
+ tests |= set(typed["tests"])
119
+
120
+ return HookResult(
121
+ checked=True,
122
+ violations=found,
123
+ changed=changed,
124
+ tests=sorted(tests),
125
+ diff_text=render_diff(diff),
126
+ )
127
+
128
+
129
+ def render_hook(result: HookResult) -> str:
130
+ if result.error:
131
+ return f"cgir: skipped contract check ({result.error})\n"
132
+ if not result.checked:
133
+ return ""
134
+ lines = ["cgir contract seatbelt"]
135
+ if result.changed:
136
+ lines.append(f" contract changes in {len(result.changed)} component(s)")
137
+ if result.tests:
138
+ lines.append(f" tests to run ({len(result.tests)}):")
139
+ lines.extend(f" • {t}" for t in result.tests)
140
+ if result.violations:
141
+ lines.append("")
142
+ lines.append(f" ❌ blocked — {len(result.violations)} drift violation(s):")
143
+ lines.extend(f" ! {v}" for v in result.violations)
144
+ lines.append(" (bypass once with `git commit --no-verify`)")
145
+ else:
146
+ lines.append(" ✅ no blocking contract drift")
147
+ return "\n".join(lines) + "\n"
148
+
149
+
150
+ def _hooks_dir(repo: Path) -> Path:
151
+ return Path(_git(repo, "rev-parse", "--git-path", "hooks"))
152
+
153
+
154
+ def install(repo: Path, fail_on: list[str] | None = None, force: bool = False) -> Path:
155
+ """Write ``.git/hooks/pre-commit`` invoking ``cgir hook run``."""
156
+ rules = list(fail_on) if fail_on is not None else list(DEFAULT_FAIL_ON)
157
+ hooks = _hooks_dir(repo)
158
+ if not hooks.is_absolute():
159
+ hooks = repo / hooks
160
+ hooks.mkdir(parents=True, exist_ok=True)
161
+ path = hooks / "pre-commit"
162
+ if path.exists() and not force:
163
+ raise FileExistsError(f"{path} exists; pass force=True to overwrite")
164
+ flags = " ".join(f"--fail-on {r}" for r in rules)
165
+ path.write_text(_HOOK_TEMPLATE.format(flags=flags))
166
+ path.chmod(0o755)
167
+ return path
168
+
169
+
170
+ def uninstall(repo: Path) -> bool:
171
+ """Remove the hook if it is ours. Returns whether anything was removed."""
172
+ hooks = _hooks_dir(repo)
173
+ if not hooks.is_absolute():
174
+ hooks = repo / hooks
175
+ path = hooks / "pre-commit"
176
+ if path.exists() and "cgir hook run" in path.read_text():
177
+ path.unlink()
178
+ return True
179
+ return False
180
+
181
+
182
+ __all__ = [
183
+ "DEFAULT_FAIL_ON",
184
+ "HookResult",
185
+ "install",
186
+ "render_hook",
187
+ "run_check",
188
+ "uninstall",
189
+ ]
cgir/ir/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """Internal IR: nodes, edges, RepoGraph, and ComponentSpec."""
2
+
3
+ from cgir.ir.component_spec import ComponentKind, ComponentSpec
4
+ from cgir.ir.edges import Edge, EdgeKind
5
+ from cgir.ir.graph import RepoGraph
6
+ from cgir.ir.nodes import Node, NodeKind
7
+
8
+ __all__ = [
9
+ "ComponentKind",
10
+ "ComponentSpec",
11
+ "Edge",
12
+ "EdgeKind",
13
+ "Node",
14
+ "NodeKind",
15
+ "RepoGraph",
16
+ ]
@@ -0,0 +1,100 @@
1
+ """ComponentSpec — the agent-facing contract (Code-IR.md §Data model)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict, dataclass, field
7
+ from enum import StrEnum
8
+ from functools import lru_cache
9
+ from typing import Any
10
+
11
+ from jsonschema import Draft202012Validator
12
+
13
+ COMPONENT_SPEC_SCHEMA: dict[str, Any] = {
14
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
15
+ "$id": "https://cgir.dev/schemas/component_spec.schema.json",
16
+ "title": "ComponentSpec",
17
+ "type": "object",
18
+ "required": ["id", "kind", "inputs", "outputs", "effects", "calls", "trace"],
19
+ "properties": {
20
+ "id": {"type": "string"},
21
+ "kind": {
22
+ "enum": [
23
+ "pure_function",
24
+ "state_transformer",
25
+ "effect_adapter",
26
+ "orchestrator",
27
+ "unknown",
28
+ ]
29
+ },
30
+ "language": {"type": "string"},
31
+ "signature": {"type": "string"},
32
+ "entrypoint": {"type": "string"},
33
+ "doc": {"type": "string"},
34
+ "raises": {"type": "array", "items": {"type": "string"}},
35
+ "covered_by": {"type": "array", "items": {"type": "string"}},
36
+ "inputs": {"type": "array", "items": {"type": "string"}},
37
+ "outputs": {"type": "array", "items": {"type": "string"}},
38
+ "effects": {"type": "array", "items": {"type": "string"}},
39
+ "calls": {"type": "array", "items": {"type": "string"}},
40
+ "constructs": {"type": "array", "items": {"type": "string"}},
41
+ "reads": {"type": "array", "items": {"type": "string"}},
42
+ "writes": {"type": "array", "items": {"type": "string"}},
43
+ "purity": {"type": "number", "minimum": 0, "maximum": 1},
44
+ "algorithm": {"type": "array", "items": {"type": "string"}},
45
+ "trace": {"type": "array", "items": {"type": "string"}},
46
+ },
47
+ "additionalProperties": False,
48
+ }
49
+
50
+
51
+ class ComponentKind(StrEnum):
52
+ pure_function = "pure_function"
53
+ state_transformer = "state_transformer"
54
+ effect_adapter = "effect_adapter"
55
+ orchestrator = "orchestrator"
56
+ unknown = "unknown"
57
+
58
+
59
+ @dataclass(slots=True)
60
+ class ComponentSpec:
61
+ id: str
62
+ kind: ComponentKind
63
+ inputs: list[str] = field(default_factory=list)
64
+ outputs: list[str] = field(default_factory=list)
65
+ effects: list[str] = field(default_factory=list)
66
+ calls: list[str] = field(default_factory=list)
67
+ constructs: list[str] = field(default_factory=list)
68
+ trace: list[str] = field(default_factory=list)
69
+ language: str | None = None
70
+ signature: str | None = None
71
+ entrypoint: str | None = None
72
+ doc: str | None = None
73
+ raises: list[str] = field(default_factory=list)
74
+ covered_by: list[str] = field(default_factory=list)
75
+ reads: list[str] = field(default_factory=list)
76
+ writes: list[str] = field(default_factory=list)
77
+ purity: float | None = None
78
+ algorithm: list[str] = field(default_factory=list)
79
+
80
+ def to_dict(self) -> dict[str, Any]:
81
+ data = asdict(self)
82
+ data["kind"] = self.kind.value
83
+ return {k: v for k, v in data.items() if v is not None}
84
+
85
+ def to_json(self, indent: int | None = 2) -> str:
86
+ return json.dumps(self.to_dict(), indent=indent, sort_keys=True)
87
+
88
+ @classmethod
89
+ def from_dict(cls, data: dict[str, Any]) -> ComponentSpec:
90
+ payload = dict(data)
91
+ payload["kind"] = ComponentKind(payload["kind"])
92
+ return cls(**payload)
93
+
94
+ def validate(self) -> None:
95
+ _validator().validate(self.to_dict())
96
+
97
+
98
+ @lru_cache(maxsize=1)
99
+ def _validator() -> Draft202012Validator:
100
+ return Draft202012Validator(COMPONENT_SPEC_SCHEMA)
cgir/ir/edges.py ADDED
@@ -0,0 +1,31 @@
1
+ """Edge vocabulary for the CGIR graph (Code-IR.md §Data model)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import StrEnum
7
+ from typing import Any
8
+
9
+
10
+ class EdgeKind(StrEnum):
11
+ CONTAINS = "CONTAINS"
12
+ IMPORTS = "IMPORTS"
13
+ CALLS = "CALLS"
14
+ READS = "READS"
15
+ WRITES = "WRITES"
16
+ MUTATES = "MUTATES"
17
+ RETURNS = "RETURNS"
18
+ THROWS = "THROWS"
19
+ FLOWS_TO = "FLOWS_TO"
20
+ CONTROLS = "CONTROLS"
21
+ DEPENDS_ON = "DEPENDS_ON"
22
+ TRACE_OF = "TRACE_OF"
23
+ REGENERATED_AS = "REGENERATED_AS"
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class Edge:
28
+ src: str
29
+ dst: str
30
+ kind: EdgeKind
31
+ attrs: dict[str, Any] = field(default_factory=dict)
cgir/ir/graph.py ADDED
@@ -0,0 +1,132 @@
1
+ """RepoGraph: a thin wrapper over networkx.MultiDiGraph with CGIR semantics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from typing import Any
7
+
8
+ import networkx as nx
9
+
10
+ from cgir.ir.edges import Edge, EdgeKind
11
+ from cgir.ir.nodes import Node, NodeKind
12
+
13
+
14
+ class RepoGraph:
15
+ def __init__(self) -> None:
16
+ self._g: nx.MultiDiGraph = nx.MultiDiGraph()
17
+
18
+ def add_node(self, node: Node) -> None:
19
+ self._g.add_node(
20
+ node.id,
21
+ kind=node.kind,
22
+ name=node.name,
23
+ path=node.path,
24
+ start_line=node.start_line,
25
+ end_line=node.end_line,
26
+ attrs=dict(node.attrs),
27
+ )
28
+
29
+ def add_edge(self, edge: Edge) -> None:
30
+ self._g.add_edge(
31
+ edge.src, edge.dst, key=edge.kind.value, kind=edge.kind, attrs=dict(edge.attrs)
32
+ )
33
+
34
+ def has_node(self, node_id: str) -> bool:
35
+ return node_id in self._g
36
+
37
+ def get_node(self, node_id: str) -> Node:
38
+ data = self._g.nodes[node_id]
39
+ return Node(
40
+ id=node_id,
41
+ kind=data["kind"],
42
+ name=data["name"],
43
+ path=data.get("path"),
44
+ start_line=data.get("start_line"),
45
+ end_line=data.get("end_line"),
46
+ attrs=dict(data.get("attrs") or {}),
47
+ )
48
+
49
+ def nodes(self, kind: NodeKind | None = None) -> Iterator[Node]:
50
+ for node_id, data in self._g.nodes(data=True):
51
+ if kind is None or data["kind"] == kind:
52
+ yield Node(
53
+ id=node_id,
54
+ kind=data["kind"],
55
+ name=data["name"],
56
+ path=data.get("path"),
57
+ start_line=data.get("start_line"),
58
+ end_line=data.get("end_line"),
59
+ attrs=dict(data.get("attrs") or {}),
60
+ )
61
+
62
+ def out_edges(self, node_id: str, kind: EdgeKind | None = None) -> Iterator[Edge]:
63
+ for src, dst, data in self._g.out_edges(node_id, data=True):
64
+ if kind is None or data["kind"] == kind:
65
+ yield Edge(src=src, dst=dst, kind=data["kind"], attrs=dict(data.get("attrs") or {}))
66
+
67
+ def in_edges(self, node_id: str, kind: EdgeKind | None = None) -> Iterator[Edge]:
68
+ for src, dst, data in self._g.in_edges(node_id, data=True):
69
+ if kind is None or data["kind"] == kind:
70
+ yield Edge(src=src, dst=dst, kind=data["kind"], attrs=dict(data.get("attrs") or {}))
71
+
72
+ def children(self, node_id: str, kind: NodeKind | None = None) -> Iterator[Node]:
73
+ for edge in self.out_edges(node_id, EdgeKind.CONTAINS):
74
+ child = self.get_node(edge.dst)
75
+ if kind is None or child.kind == kind:
76
+ yield child
77
+
78
+ def __len__(self) -> int:
79
+ return int(self._g.number_of_nodes())
80
+
81
+ @classmethod
82
+ def from_jsonable(cls, data: dict[str, Any]) -> RepoGraph:
83
+ """Inverse of :meth:`to_jsonable` — rebuild a graph from its JSON dump."""
84
+ graph = cls()
85
+ for n in data.get("nodes", []):
86
+ graph.add_node(
87
+ Node(
88
+ id=n["id"],
89
+ kind=NodeKind(n["kind"]),
90
+ name=n["name"],
91
+ path=n.get("path"),
92
+ start_line=n.get("start_line"),
93
+ end_line=n.get("end_line"),
94
+ attrs=dict(n.get("attrs") or {}),
95
+ )
96
+ )
97
+ for e in data.get("edges", []):
98
+ graph.add_edge(
99
+ Edge(
100
+ src=e["src"],
101
+ dst=e["dst"],
102
+ kind=EdgeKind(e["kind"]),
103
+ attrs=dict(e.get("attrs") or {}),
104
+ )
105
+ )
106
+ return graph
107
+
108
+ def to_jsonable(self) -> dict[str, Any]:
109
+ nodes = []
110
+ for node in self.nodes():
111
+ nodes.append(
112
+ {
113
+ "id": node.id,
114
+ "kind": node.kind.value,
115
+ "name": node.name,
116
+ "path": node.path,
117
+ "start_line": node.start_line,
118
+ "end_line": node.end_line,
119
+ "attrs": node.attrs,
120
+ }
121
+ )
122
+ edges = []
123
+ for src, dst, data in self._g.edges(data=True):
124
+ edges.append(
125
+ {
126
+ "src": src,
127
+ "dst": dst,
128
+ "kind": data["kind"].value,
129
+ "attrs": data.get("attrs") or {},
130
+ }
131
+ )
132
+ return {"nodes": nodes, "edges": edges}
cgir/ir/nodes.py ADDED
@@ -0,0 +1,38 @@
1
+ """Node vocabulary for the CGIR graph (Code-IR.md §Data model)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import StrEnum
7
+ from typing import Any
8
+
9
+
10
+ class NodeKind(StrEnum):
11
+ Repository = "Repository"
12
+ File = "File"
13
+ Module = "Module"
14
+ Class = "Class"
15
+ Function = "Function"
16
+ Method = "Method"
17
+ Parameter = "Parameter"
18
+ Variable = "Variable"
19
+ Assignment = "Assignment"
20
+ Expr = "Expr"
21
+ Statement = "Statement"
22
+ Branch = "Branch"
23
+ Loop = "Loop"
24
+ Return = "Return"
25
+ Import = "Import"
26
+ Effect = "Effect"
27
+ Test = "Test"
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class Node:
32
+ id: str
33
+ kind: NodeKind
34
+ name: str
35
+ path: str | None = None
36
+ start_line: int | None = None
37
+ end_line: int | None = None
38
+ attrs: dict[str, Any] = field(default_factory=dict)
@@ -0,0 +1,25 @@
1
+ """Language adapters — the per-language seam over a shared analysis pipeline.
2
+
3
+ Add a language by implementing :class:`LanguageAdapter`; register it in
4
+ ``registry.ADAPTERS`` and everything downstream (stats, viz, flow, diff,
5
+ pack, lint, verify) works on it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from cgir.languages.base import CallSite, LanguageAdapter
11
+ from cgir.languages.cache import SourceCache
12
+ from cgir.languages.python import PythonAdapter
13
+ from cgir.languages.registry import ADAPTERS, DEFAULT_ADAPTER, adapter_for_extension
14
+ from cgir.languages.typescript import TypeScriptAdapter
15
+
16
+ __all__ = [
17
+ "ADAPTERS",
18
+ "DEFAULT_ADAPTER",
19
+ "CallSite",
20
+ "LanguageAdapter",
21
+ "PythonAdapter",
22
+ "SourceCache",
23
+ "TypeScriptAdapter",
24
+ "adapter_for_extension",
25
+ ]