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.
- cgir/__init__.py +6 -0
- cgir/analyses/__init__.py +7 -0
- cgir/analyses/call_graph.py +114 -0
- cgir/analyses/cfg.py +320 -0
- cgir/analyses/effects.py +95 -0
- cgir/analyses/entrypoints.py +55 -0
- cgir/analyses/param_flow.py +75 -0
- cgir/analyses/pdg.py +75 -0
- cgir/analyses/purity.py +37 -0
- cgir/analyses/reaching_defs.py +115 -0
- cgir/analyses/symbols.py +106 -0
- cgir/api/__init__.py +1 -0
- cgir/api/mcp_server.py +172 -0
- cgir/api/server.py +107 -0
- cgir/cli.py +688 -0
- cgir/config.py +22 -0
- cgir/export/__init__.py +5 -0
- cgir/export/graphml.py +52 -0
- cgir/export/html_viz.py +1087 -0
- cgir/export/json_export.py +37 -0
- cgir/export/mermaid.py +57 -0
- cgir/export/neo4j.py +11 -0
- cgir/hooks.py +189 -0
- cgir/ir/__init__.py +16 -0
- cgir/ir/component_spec.py +100 -0
- cgir/ir/edges.py +31 -0
- cgir/ir/graph.py +132 -0
- cgir/ir/nodes.py +38 -0
- cgir/languages/__init__.py +25 -0
- cgir/languages/base.py +221 -0
- cgir/languages/cache.py +73 -0
- cgir/languages/python.py +1145 -0
- cgir/languages/registry.py +24 -0
- cgir/languages/typescript.py +853 -0
- cgir/manifest.py +77 -0
- cgir/pipeline.py +54 -0
- cgir/py.typed +0 -0
- cgir/regenerate/__init__.py +6 -0
- cgir/regenerate/prompt_pack.py +16 -0
- cgir/regenerate/regenerator.py +108 -0
- cgir/report/__init__.py +5 -0
- cgir/report/diff.py +226 -0
- cgir/report/flow.py +84 -0
- cgir/report/impact.py +234 -0
- cgir/report/lint.py +84 -0
- cgir/report/pack.py +271 -0
- cgir/report/stats.py +121 -0
- cgir/slicing/__init__.py +5 -0
- cgir/slicing/slicer.py +174 -0
- cgir/sources/__init__.py +6 -0
- cgir/sources/base.py +14 -0
- cgir/sources/codeql_source.py +13 -0
- cgir/sources/joern_source.py +13 -0
- cgir/sources/tree_sitter_source.py +270 -0
- cgir/trace/__init__.py +5 -0
- cgir/trace/trace_map.py +83 -0
- cgir/verify.py +173 -0
- cgir/watch.py +171 -0
- codegraph_ir-0.1.0.dist-info/METADATA +143 -0
- codegraph_ir-0.1.0.dist-info/RECORD +63 -0
- codegraph_ir-0.1.0.dist-info/WHEEL +4 -0
- codegraph_ir-0.1.0.dist-info/entry_points.txt +2 -0
- codegraph_ir-0.1.0.dist-info/licenses/LICENSE +21 -0
cgir/trace/trace_map.py
ADDED
|
@@ -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,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.
|