codegraph-ir 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/PKG-INFO +25 -3
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/README.md +22 -1
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/languages.md +44 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/status.md +4 -1
- codegraph_ir-0.4.0/docs/writing-an-adapter.md +309 -0
- codegraph_ir-0.4.0/examples/rust-adapter/NOTES.md +145 -0
- codegraph_ir-0.4.0/examples/rust-adapter/README.md +15 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/pyproject.toml +5 -2
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/__init__.py +1 -1
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/cli.py +15 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/languages/base.py +12 -1
- codegraph_ir-0.4.0/src/cgir/languages/registry.py +104 -0
- codegraph_ir-0.4.0/src/cgir/languages/rust.py +982 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_pins.py +18 -0
- codegraph_ir-0.4.0/tests/unit/test_plugin_registry.py +117 -0
- codegraph_ir-0.4.0/tests/unit/test_rust_adapter.py +634 -0
- codegraph_ir-0.3.0/src/cgir/languages/registry.py +0 -27
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/.github/workflows/ci.yml +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/.github/workflows/release.yml +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/.gitignore +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/.pre-commit-config.yaml +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/.pre-commit-hooks.yaml +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/.python-version +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/CLAUDE.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/Code-IR.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/LICENSE +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/RELEASING.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/action.yml +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/README.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/agents.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/architecture-rules.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/architecture.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/development.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/experiment-log.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/feature-research.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/gate-noise.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/github-action.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/lsp.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/plan-0.2-0.4.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/plan-0.3.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/roadmap.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/strategy.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/examples/agent-pr-contract-drift.md +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/schemas/component_spec.schema.json +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/__init__.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/call_graph.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/cfg.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/coverage_link.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/effects.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/entrypoints.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/param_flow.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/pdg.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/purity.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/reaching_defs.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/symbols.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/api/__init__.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/api/lsp_server.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/api/mcp_server.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/api/server.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/config.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/export/__init__.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/export/graphml.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/export/html_viz.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/export/json_export.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/export/mermaid.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/export/neo4j.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/hooks.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/ir/__init__.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/ir/component_spec.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/ir/edges.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/ir/graph.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/ir/nodes.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/languages/__init__.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/languages/cache.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/languages/go.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/languages/python.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/languages/typescript.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/manifest.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/pipeline.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/py.typed +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/regenerate/__init__.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/regenerate/prompt_pack.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/regenerate/regenerator.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/__init__.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/diff.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/flow.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/impact.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/lint.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/pack.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/pins.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/search.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/stats.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/slicing/__init__.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/slicing/slicer.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/sources/__init__.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/sources/base.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/sources/codeql_source.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/sources/joern_source.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/sources/tree_sitter_source.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/trace/__init__.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/trace/trace_map.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/verify.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/watch.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/__init__.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/conftest.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/fixtures/python_sample/orchestrator.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/fixtures/python_sample/pricing.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/fixtures/ts_sample/api/service.ts +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/fixtures/ts_sample/util.ts +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/integration/__init__.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/integration/test_api.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/integration/test_cli_scan.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/integration/test_hook.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/integration/test_init.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/__init__.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_action.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_call_graph.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_cfg.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_component_spec.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_confidence.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_coverage_link.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_diff.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_effects.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_entrypoints.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_go_adapter.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_graphml.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_html_viz.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_impact.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_impact_run.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_ir_graph.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_language_adapter.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_lint.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_lsp.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_manifest.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_mcp_tools.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_mermaid.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_pack.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_param_flow.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_pdg.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_purity.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_python_di.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_reaching_defs.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_regenerator.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_search.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_shapes.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_slicer.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_source_cache.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_stats.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_symbols.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_trace_map.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_tree_sitter_source.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_typescript_adapter.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_verify.py +0 -0
- {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_watch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codegraph-ir
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: The deterministic contract layer for AI-modified codebases — effects, purity, contracts, and blast radius, with zero LLM calls.
|
|
5
5
|
Project-URL: Homepage, https://github.com/asonkiya/llm-semantic-compilers
|
|
6
6
|
Project-URL: Repository, https://github.com/asonkiya/llm-semantic-compilers
|
|
@@ -43,8 +43,9 @@ Requires-Python: >=3.11
|
|
|
43
43
|
Requires-Dist: jsonschema>=4.21
|
|
44
44
|
Requires-Dist: networkx>=3.2
|
|
45
45
|
Requires-Dist: pydantic>=2.6
|
|
46
|
-
Requires-Dist: tree-sitter-go<0.
|
|
46
|
+
Requires-Dist: tree-sitter-go<0.24,>=0.23
|
|
47
47
|
Requires-Dist: tree-sitter-python<0.24,>=0.23
|
|
48
|
+
Requires-Dist: tree-sitter-rust<0.23.3,>=0.23
|
|
48
49
|
Requires-Dist: tree-sitter-typescript<0.24,>=0.23
|
|
49
50
|
Requires-Dist: tree-sitter<0.25,>=0.23
|
|
50
51
|
Requires-Dist: typer>=0.12
|
|
@@ -92,7 +93,7 @@ For library/agent use in a project: `uv pip install codegraph-ir`
|
|
|
92
93
|
## The local loop
|
|
93
94
|
|
|
94
95
|
```bash
|
|
95
|
-
cgir scan . # build the .cgir index (Python, TypeScript, Go)
|
|
96
|
+
cgir scan . # build the .cgir index (Python, TypeScript, Go, Rust)
|
|
96
97
|
cgir watch . # keep it live: re-scan + show contract drift on save
|
|
97
98
|
cgir pack app.service.charge --repo . # minimal context bundle for one component
|
|
98
99
|
cgir impact app.service.charge # blast radius: affected callers, entrypoints, tests
|
|
@@ -133,6 +134,27 @@ agent-PR case study.
|
|
|
133
134
|
Setup guide for agents (MCP config + CLAUDE.md snippet): [`docs/agents.md`](./docs/agents.md).
|
|
134
135
|
Or via the [pre-commit framework](https://pre-commit.com): hook id `cgir-contract-check`.
|
|
135
136
|
|
|
137
|
+
## How it compares (honestly)
|
|
138
|
+
|
|
139
|
+
| | **CGIR** | CodeGraph-style MCP graphs | Greptile / CodeRabbit | import-linter / ArchUnit | oasdiff / Pact |
|
|
140
|
+
|---|---|---|---|---|---|
|
|
141
|
+
| Effects & purity contracts per function | ✅ | ❌ | ❌ LLM judgment | ❌ | ❌ |
|
|
142
|
+
| Invariants declared in source (`# cgir: pure`) & enforced | ✅ | ❌ | ❌ | imports only | API boundary only |
|
|
143
|
+
| Deterministic (same input → same verdict, zero LLM) | ✅ | ✅ structure only | ❌ | ✅ | ✅ |
|
|
144
|
+
| Catches "service silently stopped calling the backend" | ✅ | ❌ | sometimes | ❌ | at spec boundaries |
|
|
145
|
+
| Blast radius + coverage-grounded test selection | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
146
|
+
| Agent context over MCP | ✅ contract packs | ✅ broader retrieval | ❌ | ❌ | ❌ |
|
|
147
|
+
| Finds logic bugs | ❌ | ❌ | ✅ | ❌ | ❌ |
|
|
148
|
+
| Languages | 4 builtin + [plugin API](./docs/languages.md) | 30+ | most | per-tool | spec-level |
|
|
149
|
+
| Cycle / layer rules | ✅ | ❌ | ❌ | ✅ mature | ❌ |
|
|
150
|
+
| Cost | free, local | mostly free | ~$24–30/user/mo | free | free |
|
|
151
|
+
|
|
152
|
+
Where CGIR loses, we say so: it does **not** find logic bugs (a subtly wrong
|
|
153
|
+
algorithm with unchanged effects passes the gate — pair it with tests or a
|
|
154
|
+
reviewer), supports 3 languages not 38, and its effect detection is static
|
|
155
|
+
analysis with [documented, measured limits](./docs/gate-noise.md) — every
|
|
156
|
+
tag carries a confidence tier so you know which claims are verified.
|
|
157
|
+
|
|
136
158
|
## Docs
|
|
137
159
|
|
|
138
160
|
- [`docs/strategy.md`](./docs/strategy.md) — positioning: the deterministic contract layer
|
|
@@ -23,7 +23,7 @@ For library/agent use in a project: `uv pip install codegraph-ir`
|
|
|
23
23
|
## The local loop
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
cgir scan . # build the .cgir index (Python, TypeScript, Go)
|
|
26
|
+
cgir scan . # build the .cgir index (Python, TypeScript, Go, Rust)
|
|
27
27
|
cgir watch . # keep it live: re-scan + show contract drift on save
|
|
28
28
|
cgir pack app.service.charge --repo . # minimal context bundle for one component
|
|
29
29
|
cgir impact app.service.charge # blast radius: affected callers, entrypoints, tests
|
|
@@ -64,6 +64,27 @@ agent-PR case study.
|
|
|
64
64
|
Setup guide for agents (MCP config + CLAUDE.md snippet): [`docs/agents.md`](./docs/agents.md).
|
|
65
65
|
Or via the [pre-commit framework](https://pre-commit.com): hook id `cgir-contract-check`.
|
|
66
66
|
|
|
67
|
+
## How it compares (honestly)
|
|
68
|
+
|
|
69
|
+
| | **CGIR** | CodeGraph-style MCP graphs | Greptile / CodeRabbit | import-linter / ArchUnit | oasdiff / Pact |
|
|
70
|
+
|---|---|---|---|---|---|
|
|
71
|
+
| Effects & purity contracts per function | ✅ | ❌ | ❌ LLM judgment | ❌ | ❌ |
|
|
72
|
+
| Invariants declared in source (`# cgir: pure`) & enforced | ✅ | ❌ | ❌ | imports only | API boundary only |
|
|
73
|
+
| Deterministic (same input → same verdict, zero LLM) | ✅ | ✅ structure only | ❌ | ✅ | ✅ |
|
|
74
|
+
| Catches "service silently stopped calling the backend" | ✅ | ❌ | sometimes | ❌ | at spec boundaries |
|
|
75
|
+
| Blast radius + coverage-grounded test selection | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
76
|
+
| Agent context over MCP | ✅ contract packs | ✅ broader retrieval | ❌ | ❌ | ❌ |
|
|
77
|
+
| Finds logic bugs | ❌ | ❌ | ✅ | ❌ | ❌ |
|
|
78
|
+
| Languages | 4 builtin + [plugin API](./docs/languages.md) | 30+ | most | per-tool | spec-level |
|
|
79
|
+
| Cycle / layer rules | ✅ | ❌ | ❌ | ✅ mature | ❌ |
|
|
80
|
+
| Cost | free, local | mostly free | ~$24–30/user/mo | free | free |
|
|
81
|
+
|
|
82
|
+
Where CGIR loses, we say so: it does **not** find logic bugs (a subtly wrong
|
|
83
|
+
algorithm with unchanged effects passes the gate — pair it with tests or a
|
|
84
|
+
reviewer), supports 3 languages not 38, and its effect detection is static
|
|
85
|
+
analysis with [documented, measured limits](./docs/gate-noise.md) — every
|
|
86
|
+
tag carries a confidence tier so you know which claims are verified.
|
|
87
|
+
|
|
67
88
|
## Docs
|
|
68
89
|
|
|
69
90
|
- [`docs/strategy.md`](./docs/strategy.md) — positioning: the deterministic contract layer
|
|
@@ -28,6 +28,7 @@ full test suite green.
|
|
|
28
28
|
| 3 | ingest structural dispatch + attr extraction (normalized `Declaration`s) | ✅ done |
|
|
29
29
|
| 4 | `TypeScriptAdapter` + per-file language dispatch | ✅ done |
|
|
30
30
|
| 5 | `GoAdapter` (package=directory merge, struct-field DI, panic≙raise) | ✅ done |
|
|
31
|
+
| 6 | `RustAdapter` (agent-written from the docs, reviewed & promoted; structs/impl, use-trees, match CFG) | ✅ done |
|
|
31
32
|
|
|
32
33
|
**Two languages ship (Python, TypeScript); a mixed repo scans both.**
|
|
33
34
|
Zero grammar node-type strings remain outside `cgir/languages/`. The CFG builder is pure topology over statement
|
|
@@ -66,3 +67,46 @@ closure, symbol-table resolution, slicing, and all seven product surfaces.
|
|
|
66
67
|
Register it in `cgir/languages/__init__.py:ADAPTERS`. The effect *taxonomy*
|
|
67
68
|
(`io`/`net`/`fs`/`db`/`nondeterm`/`raise`) is language-neutral and fixed in
|
|
68
69
|
`analyses/effects.py`; the adapter only decides which calls map to which tag.
|
|
70
|
+
|
|
71
|
+
**Full authoring guide: [`writing-an-adapter.md`](./writing-an-adapter.md)** — self-contained; written so an implementer (human or agent) needs no other source.
|
|
72
|
+
|
|
73
|
+
## Packaging a language plugin
|
|
74
|
+
|
|
75
|
+
Adapters are discovered via the ``cgir.languages`` entry-point group — no
|
|
76
|
+
fork needed. A minimal plugin package:
|
|
77
|
+
|
|
78
|
+
```toml
|
|
79
|
+
# pyproject.toml of cgir-rust
|
|
80
|
+
[project]
|
|
81
|
+
name = "cgir-rust"
|
|
82
|
+
dependencies = ["codegraph-ir", "tree-sitter-rust"]
|
|
83
|
+
|
|
84
|
+
[project.entry-points."cgir.languages"]
|
|
85
|
+
rust = "cgir_rust:RustAdapter"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# cgir_rust/__init__.py
|
|
90
|
+
from cgir.languages.base import ADAPTER_API_VERSION, LanguageAdapter
|
|
91
|
+
|
|
92
|
+
class RustAdapter(LanguageAdapter):
|
|
93
|
+
name = "rust"
|
|
94
|
+
file_extensions = (".rs",)
|
|
95
|
+
api_version = ADAPTER_API_VERSION
|
|
96
|
+
# implement: parse, locate_function, direct_effects, call_sites,
|
|
97
|
+
# function_body, block_statements, describe_statement,
|
|
98
|
+
# module_declarations — see GoAdapter for the most recent template.
|
|
99
|
+
# Optional (defaults provided): direct_effects_confidence,
|
|
100
|
+
# global_declared_names.
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`pip install cgir-rust` and `cgir languages` shows it. Safety rules:
|
|
104
|
+
builtins win extension conflicts; a plugin that fails to load, isn't a
|
|
105
|
+
`LanguageAdapter`, or reuses a language name is skipped with a warning —
|
|
106
|
+
a broken plugin never crashes cgir. An `api_version` mismatch warns but
|
|
107
|
+
loads (new adapter methods get base-class defaults, so older plugins
|
|
108
|
+
usually keep working).
|
|
109
|
+
|
|
110
|
+
Start from `src/cgir/languages/go.py` — the newest adapter and the best
|
|
111
|
+
template — and mirror `tests/unit/test_go_adapter.py` for the expected
|
|
112
|
+
pipeline-level test coverage.
|
|
@@ -79,6 +79,9 @@ cgir scan tests/fixtures/python_sample --out /tmp/cgir-out
|
|
|
79
79
|
| `forbid-cycle` (Tarjan SCCs) + `layers` rules in lint | done | `report/lint.py:_cycle_violations`, `_layer_violations` |
|
|
80
80
|
| Coverage-grounded test linkage (.coverage/coverage.json contexts ∪ static; pack test sources capped) | done | `analyses/coverage_link.py` |
|
|
81
81
|
| LSP diagnostics (`cgir lsp`, `cgir[lsp]` extra): pin violations as errors, gate drift as warnings, on save | done | `src/cgir/api/lsp_server.py`, `docs/lsp.md` |
|
|
82
|
+
| Language plugin registry (`cgir.languages` entry points, `cgir languages`, ADAPTER_API_VERSION; builtins win conflicts, broken plugins degrade to warnings) | done | `languages/registry.py:discover_adapters` |
|
|
83
|
+
| Adapter authoring guide, validated by a docs-only agent experiment (Rust adapter, 36/36, 9/9 bar; gaps folded back; PinIndex line_comment fix) | done | `docs/writing-an-adapter.md`, `examples/rust-adapter/` |
|
|
84
|
+
| Rust adapter (structs+impl methods w/ field DI, use-tree imports, match/loop CFG, effect tiers; agent-written, reviewed+promoted) | done | `src/cgir/languages/rust.py` |
|
|
82
85
|
| Entrypoint recognition (HTTP/CLI/task decorators) | done | `src/cgir/analyses/entrypoints.py` |
|
|
83
86
|
| Context packer (`cgir pack`, budget-aware, type closure + docstrings + raises) | done | `src/cgir/report/pack.py` |
|
|
84
87
|
| Docstring / raises / module-variable extraction | done | `tree_sitter_source._docstring_text`, `_raised_names`, `_add_module_variables` |
|
|
@@ -98,7 +101,7 @@ cgir scan tests/fixtures/python_sample --out /tmp/cgir-out
|
|
|
98
101
|
|
|
99
102
|
## Test coverage
|
|
100
103
|
|
|
101
|
-
`pytest -q` runs
|
|
104
|
+
`pytest -q` runs 541 tests, all green (the table below lists the load-bearing files; newer suites — `test_impact.py`, `test_watch.py`, `test_hook.py`, `test_python_di.py`, `test_typescript_adapter.py`, `test_diff.py` — are described by their docstrings):
|
|
102
105
|
|
|
103
106
|
| File | Covers |
|
|
104
107
|
|---|---|
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Writing a language adapter
|
|
2
|
+
|
|
3
|
+
This document is **self-contained**: everything needed to implement a new
|
|
4
|
+
language for CGIR without reading its source. An adapter is one class
|
|
5
|
+
implementing ~8 methods over a tree-sitter grammar; every downstream
|
|
6
|
+
feature — effects, purity, kind classification, pins, packs, blast radius,
|
|
7
|
+
the pre-commit gate, the LSP — works on your language for free.
|
|
8
|
+
|
|
9
|
+
## The mental model
|
|
10
|
+
|
|
11
|
+
CGIR's pipeline is language-neutral. Only *grammar extraction* is per
|
|
12
|
+
language, behind the `LanguageAdapter` ABC:
|
|
13
|
+
|
|
14
|
+
1. **Ingest** calls `module_declarations` to get normalized declarations
|
|
15
|
+
(functions, classes, imports, variables) and builds the graph.
|
|
16
|
+
2. **Call graph** calls `call_sites` per function and resolves callees via
|
|
17
|
+
language-neutral symbol tables.
|
|
18
|
+
3. **CFG** calls `function_body` / `block_statements` /
|
|
19
|
+
`describe_statement` per function; the *topology* (branch wiring, loop
|
|
20
|
+
back-edges) is built centrally — you only classify statements.
|
|
21
|
+
4. **Effects** calls `direct_effects_confidence` per function; transitive
|
|
22
|
+
propagation is central.
|
|
23
|
+
|
|
24
|
+
You never build graph nodes or edges. You translate grammar shapes into
|
|
25
|
+
the descriptor dataclasses below.
|
|
26
|
+
|
|
27
|
+
## Grammar version compatibility (check first)
|
|
28
|
+
|
|
29
|
+
tree-sitter grammar wheels ship a compiled language ABI; the `tree-sitter`
|
|
30
|
+
core only accepts a range. If `Language(...)` raises
|
|
31
|
+
``Incompatible Language version``, pin an older grammar wheel (e.g.
|
|
32
|
+
``tree-sitter-rust<0.24`` against ``tree-sitter 0.24``).
|
|
33
|
+
|
|
34
|
+
## Setup
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import tree_sitter_rust # any tree-sitter grammar package
|
|
38
|
+
from tree_sitter import Language, Parser
|
|
39
|
+
from tree_sitter import Node as TSNode
|
|
40
|
+
|
|
41
|
+
from cgir.languages.base import (
|
|
42
|
+
ADAPTER_API_VERSION, AssignDesc, BranchDesc, CallSite, CaseDesc,
|
|
43
|
+
ClassDecl, Declaration, FunctionDecl, ImportDecl, LanguageAdapter,
|
|
44
|
+
LoopDesc, MatchDesc, ParamDecl, PinIndex, ReturnDesc, SimpleDesc,
|
|
45
|
+
StatementDesc, TryDesc, VariableDecl, WithDesc,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
class RustAdapter(LanguageAdapter):
|
|
49
|
+
name = "rust" # unique language name
|
|
50
|
+
file_extensions = (".rs",) # extensions this adapter claims
|
|
51
|
+
api_version = ADAPTER_API_VERSION # currently 1
|
|
52
|
+
|
|
53
|
+
def __init__(self) -> None:
|
|
54
|
+
self._parser = Parser(Language(tree_sitter_rust.language()))
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Useful helper you'll write once: `node.text` returns the node's source
|
|
58
|
+
bytes (offset-safe); decode with `errors="replace"`.
|
|
59
|
+
|
|
60
|
+
**Comment node types:** grammars disagree (`comment` in python/ts/go;
|
|
61
|
+
`line_comment`/`block_comment` in rust/c/java). `PinIndex` handles all of
|
|
62
|
+
these (`cgir.languages.base.COMMENT_NODE_TYPES`) — but your
|
|
63
|
+
`block_statements` comment filter must use *your grammar's* comment types.
|
|
64
|
+
|
|
65
|
+
## The required methods, exactly
|
|
66
|
+
|
|
67
|
+
### `parse(self, source: bytes) -> TSNode`
|
|
68
|
+
Return `self._parser.parse(source).root_node`.
|
|
69
|
+
|
|
70
|
+
### `locate_function(self, root, name: str, start_row: int) -> TSNode | None`
|
|
71
|
+
Find the function/method definition node whose **name** matches and whose
|
|
72
|
+
**start row equals `start_row` (0-based)**. Analyses call this to re-find a
|
|
73
|
+
function from graph metadata; if it returns None for a function, no
|
|
74
|
+
analysis runs on it. Walk the whole tree (functions nest inside classes/
|
|
75
|
+
impl blocks).
|
|
76
|
+
|
|
77
|
+
### `module_declarations(self, root, source: bytes, module_name: str, rel_path: str) -> list[Declaration]`
|
|
78
|
+
Walk the module's top level and return normalized declarations.
|
|
79
|
+
`module_name` is the dotted module path derived from the file path
|
|
80
|
+
(`internal/store/keys.rs` → `internal.store.keys`); `rel_path` is the
|
|
81
|
+
repo-relative file path (for path-based import specifiers).
|
|
82
|
+
|
|
83
|
+
Declaration shapes (all carry `node: TSNode` — used for line spans):
|
|
84
|
+
|
|
85
|
+
- **`FunctionDecl(node, name, params, signature, returns, doc, raises,
|
|
86
|
+
decorators, free_names, pins)`**
|
|
87
|
+
- `params`: `list[ParamDecl(name, node)]` — **exclude** the implicit
|
|
88
|
+
receiver (`self`/`this`).
|
|
89
|
+
- `signature`: human-readable, e.g. `"add(a: i64, b: i64) -> i64"`.
|
|
90
|
+
- `returns`: return-type text or None.
|
|
91
|
+
- `doc`: docstring/doc-comment text, `""` if none.
|
|
92
|
+
- `raises`: exception-ish names; for panic-style languages return
|
|
93
|
+
`["panic"]` when the body contains a panic, else `[]`. For
|
|
94
|
+
Result/error-value languages (Rust `?`, Go `error` returns), error
|
|
95
|
+
*values* are not raises — only aborts (panic) count.
|
|
96
|
+
- `decorators`: attribute/annotation strings (may be `[]`).
|
|
97
|
+
- `free_names`: identifiers the body references that aren't locally
|
|
98
|
+
bound (feeds context packing; `[]` is acceptable v1).
|
|
99
|
+
- `pins`: see **Pins** below.
|
|
100
|
+
- **`ClassDecl(node, name, methods, fields)`** — for any named type that
|
|
101
|
+
owns methods or fields (structs, impl targets, interfaces).
|
|
102
|
+
- `methods`: list of FunctionDecl (their params exclude the receiver).
|
|
103
|
+
- `fields`: `dict[field_name, type_name]` — **powers DI resolution**: a
|
|
104
|
+
call `self.client.fetch()` resolves through the declared type of
|
|
105
|
+
`client`. For plain data shapes (structs without methods) fields also
|
|
106
|
+
feed shape-drift detection; use the base type name for DI-relevant
|
|
107
|
+
fields (strip pointers/references), full type text is fine for
|
|
108
|
+
data-only shapes.
|
|
109
|
+
- **`ImportDecl(node, target, alias)`** — `target` is the imported module
|
|
110
|
+
as a **dotted path** (`std::collections::HashMap` →
|
|
111
|
+
`std.collections.HashMap`); `alias` is the local name it binds (the last
|
|
112
|
+
segment when unaliased). Resolution against in-repo modules happens
|
|
113
|
+
centrally by exact/unique-suffix match on dotted module names.
|
|
114
|
+
- **`VariableDecl(node, name)`** — module-level constants/statics.
|
|
115
|
+
|
|
116
|
+
### `call_sites(self, func_node, source) -> list[CallSite]`
|
|
117
|
+
`CallSite = tuple[str, list[str], int]` — (dotted callee text, argument
|
|
118
|
+
identifier names, 0-based line). Rules:
|
|
119
|
+
- A plain call `f(x)` → `("f", ["x"], line)`.
|
|
120
|
+
- A path/method call → dotted text: `store::save(k)` → `"store.save"`;
|
|
121
|
+
`client.fetch(id)` → `"client.fetch"`.
|
|
122
|
+
- **Receiver normalization (required for DI):** inside a method, calls
|
|
123
|
+
through the method's own receiver must be emitted with the literal
|
|
124
|
+
receiver replaced by `self`: in `fn sync(&self)` a call
|
|
125
|
+
`self.client.fetch(id)` → `"self.client.fetch"`. (In Go, where receivers
|
|
126
|
+
are arbitrarily named, `s.client.Fetch` must become
|
|
127
|
+
`"self.client.Fetch"`.) The central resolver keys on the `self.`/`this.`
|
|
128
|
+
prefix + the owning class's `fields` map.
|
|
129
|
+
- Skip panic/raise intrinsics (they're effects, not calls).
|
|
130
|
+
|
|
131
|
+
### `direct_effects(self, func_node, source, aliases) -> set[str]`
|
|
132
|
+
Effect tags directly present in the body. Tags: `io`, `net`, `fs`, `db`,
|
|
133
|
+
`nondeterm`, `raise`. `aliases` maps local import names → absolute dotted
|
|
134
|
+
targets (`r` → `requests`), built centrally from your ImportDecls —
|
|
135
|
+
normalize dotted callees through it before matching your tables.
|
|
136
|
+
Implement as `return set(self.direct_effects_confidence(...))`.
|
|
137
|
+
|
|
138
|
+
### `direct_effects_confidence(self, func_node, source, aliases) -> dict[str, str]`
|
|
139
|
+
The real classifier: `{tag: "high" | "lexical"}`.
|
|
140
|
+
- `"high"`: exact or prefix matches against curated tables of known APIs
|
|
141
|
+
(`reqwest.`, `std.fs.`, `println!`) — ideally alias-normalized.
|
|
142
|
+
- `"lexical"`: guesses from bare method suffixes or receiver names
|
|
143
|
+
(`anything.now()`, `db.query(...)` gated only by the receiver being
|
|
144
|
+
named `db`). These are *reported but don't fail builds by default* — be
|
|
145
|
+
honest about which of your rules are guesses.
|
|
146
|
+
If a tag matches both ways, high wins. Raise/panic detection is `"high"`.
|
|
147
|
+
|
|
148
|
+
### `function_body(self, func_node) -> TSNode | None`
|
|
149
|
+
The body block node (usually `child_by_field_name("body")`).
|
|
150
|
+
|
|
151
|
+
### `block_statements(self, block) -> list[TSNode]`
|
|
152
|
+
The block's statement nodes, comments filtered
|
|
153
|
+
(`[c for c in block.named_children if c.type != "comment"]`).
|
|
154
|
+
|
|
155
|
+
### `describe_statement(self, node, source) -> StatementDesc`
|
|
156
|
+
Classify one statement into exactly one descriptor. The CFG builder wires
|
|
157
|
+
topology from these; **you never recurse into sub-blocks yourself** — you
|
|
158
|
+
hand back the sub-block *nodes* and the builder calls you again for their
|
|
159
|
+
statements.
|
|
160
|
+
|
|
161
|
+
- **`AssignDesc(writes, mutates, reads)`** — assignments/let-bindings.
|
|
162
|
+
`writes`: plain names bound; `mutates`: base names of attribute/index
|
|
163
|
+
targets (`self.x = v` → mutates `["self"]`, `xs[0] = v` → `["xs"]`);
|
|
164
|
+
`reads`: identifiers read on the RHS.
|
|
165
|
+
- **`BranchDesc(reads, consequence, else_block, next_branch)`** — an
|
|
166
|
+
if-arm. `reads`: condition identifiers. `consequence`: then-block node.
|
|
167
|
+
For `else if`, set `next_branch` to the nested if node (it will be
|
|
168
|
+
described again); for a plain `else`, set `else_block` to its block.
|
|
169
|
+
- **`LoopDesc(reads, writes, body)`** — all loop forms. `writes`: loop
|
|
170
|
+
variables (`for x in xs` → `["x"]`).
|
|
171
|
+
- **`ReturnDesc(reads, mutates)`** — returns; `reads` from the returned
|
|
172
|
+
expression.
|
|
173
|
+
- **`MatchDesc(cases=[CaseDesc(node, reads, consequence)])`** — match/
|
|
174
|
+
switch. `reads`: the scrutinee identifiers (repeat per case is fine);
|
|
175
|
+
`consequence`: the case's body node (the case node itself is acceptable
|
|
176
|
+
when the grammar nests statements directly under it).
|
|
177
|
+
- **`TryDesc(body, handlers=[HandlerDesc(node, writes, block)], else_block,
|
|
178
|
+
finally_block)`** — try/catch shapes; skip if the language has none.
|
|
179
|
+
- **`WithDesc(writes, reads, body)`** — resource-acquisition headers;
|
|
180
|
+
skip if none.
|
|
181
|
+
- **`SimpleDesc(reads, mutates)`** — everything else (expression
|
|
182
|
+
statements). Put mutator-method receivers in `mutates` if you track
|
|
183
|
+
them; `reads` from the expression.
|
|
184
|
+
|
|
185
|
+
Purity note: a function is classified `pure_function` only if it has no
|
|
186
|
+
impure effects **and** no caller-observable mutation — `mutates` on a
|
|
187
|
+
parameter or module global is what flips it to `state_transformer`.
|
|
188
|
+
|
|
189
|
+
### Optional: `global_declared_names(self, func_node, source) -> set[str]`
|
|
190
|
+
Names the function declares as outer-scope (Python `global`/`nonlocal`).
|
|
191
|
+
Assignments to them count as mutations. Default: empty set — correct for
|
|
192
|
+
most languages.
|
|
193
|
+
|
|
194
|
+
## Pins (required wiring in `module_declarations`)
|
|
195
|
+
|
|
196
|
+
Pins are `cgir:` comment pragmas (`// cgir: pure`). `PinIndex` does the
|
|
197
|
+
extraction; you wire it:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
def module_declarations(self, root, source, module_name, rel_path):
|
|
201
|
+
pin_index = PinIndex(root, source) # works on any grammar with "comment" nodes
|
|
202
|
+
...
|
|
203
|
+
# per definition — pass the OUTERMOST node (so a pin above an
|
|
204
|
+
# attribute/export/pub keyword is found):
|
|
205
|
+
fn = FunctionDecl(..., pins=pin_index.for_definition(outermost_node))
|
|
206
|
+
...
|
|
207
|
+
# module-level pins: a header comment block applies to every function
|
|
208
|
+
# in the file — UNLESS it sits directly above a pinnable definition
|
|
209
|
+
# (then it's that definition's pin). Pass the first decl's row only
|
|
210
|
+
# when that first decl is a function/class-like node; imports and
|
|
211
|
+
# plain statements must pass None-equivalent:
|
|
212
|
+
first = next((c for c in root.named_children if c.type != "comment"), None)
|
|
213
|
+
pinnable = {"function_item", "struct_item", "impl_item"} # your grammar's types
|
|
214
|
+
module_pins = pin_index.module_pins(
|
|
215
|
+
first.start_point[0] if first is not None and first.type in pinnable else None
|
|
216
|
+
)
|
|
217
|
+
if module_pins:
|
|
218
|
+
for decl in decls:
|
|
219
|
+
if isinstance(decl, FunctionDecl):
|
|
220
|
+
decl.pins = sorted(set(decl.pins) | set(module_pins))
|
|
221
|
+
elif isinstance(decl, ClassDecl):
|
|
222
|
+
for m in decl.methods:
|
|
223
|
+
m.pins = sorted(set(m.pins) | set(module_pins))
|
|
224
|
+
return decls
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Registration
|
|
228
|
+
|
|
229
|
+
**In-tree:** add your adapter to the tuple in
|
|
230
|
+
`src/cgir/languages/registry.py` (`_BUILTINS`).
|
|
231
|
+
|
|
232
|
+
**As a plugin package** (no fork):
|
|
233
|
+
|
|
234
|
+
```toml
|
|
235
|
+
[project.entry-points."cgir.languages"]
|
|
236
|
+
rust = "cgir_rust:RustAdapter"
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Builtins win extension conflicts; broken plugins degrade to warnings.
|
|
240
|
+
`cgir languages` shows what loaded.
|
|
241
|
+
|
|
242
|
+
## The test bar
|
|
243
|
+
|
|
244
|
+
Mirror this pipeline-level suite (this is the acceptance standard; adapt
|
|
245
|
+
the code snippets to your language):
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from cgir.analyses.call_graph import build_call_graph
|
|
249
|
+
from cgir.analyses.cfg import build as build_cfg
|
|
250
|
+
from cgir.analyses.effects import classify
|
|
251
|
+
from cgir.analyses.purity import score
|
|
252
|
+
from cgir.analyses.symbols import build_symbol_tables
|
|
253
|
+
from cgir.ir.component_spec import ComponentKind
|
|
254
|
+
from cgir.ir.edges import EdgeKind
|
|
255
|
+
from cgir.ir.nodes import NodeKind
|
|
256
|
+
from cgir.slicing import slice_components
|
|
257
|
+
from cgir.sources import TreeSitterSource
|
|
258
|
+
|
|
259
|
+
def _scan(tmp_path):
|
|
260
|
+
graph = TreeSitterSource().ingest(tmp_path)
|
|
261
|
+
tables = build_symbol_tables(graph)
|
|
262
|
+
build_call_graph(graph, tables, tmp_path)
|
|
263
|
+
build_cfg(graph, tmp_path)
|
|
264
|
+
effects = classify(graph, tmp_path)
|
|
265
|
+
purity = score(graph, effects)
|
|
266
|
+
return {s.id: s for s in slice_components(graph, effects=effects, purity_scores=purity)}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Cover at least:
|
|
270
|
+
1. functions and methods ingested with correct ids
|
|
271
|
+
(`<module>.<fn>` / `<module>.<Type>.<method>`). Note the graph split:
|
|
272
|
+
free functions are `NodeKind.Function` with `func:` node ids; methods
|
|
273
|
+
are `NodeKind.Method` with `method:` ids — queries must use the right
|
|
274
|
+
prefix.
|
|
275
|
+
2. params + signature extracted; `spec.language == "<your name>"`
|
|
276
|
+
3. effect detection per tag you implement, incl. a pure function staying
|
|
277
|
+
`ComponentKind.pure_function` with `purity == 1.0`
|
|
278
|
+
4. struct/class `fields` extracted
|
|
279
|
+
5. receiver-field call resolves:
|
|
280
|
+
`graph.out_edges("method:<mod>.<Type>.<m>", EdgeKind.CALLS)` contains
|
|
281
|
+
the target method (this proves your `self.` normalization)
|
|
282
|
+
6. caller of an effectful callee gets `"calls_effectful"` in effects
|
|
283
|
+
7. CFG: a function with if + loop has `NodeKind.Branch` and
|
|
284
|
+
`NodeKind.Loop` children
|
|
285
|
+
8. a `// cgir: pure` pin lands in `spec.pins`
|
|
286
|
+
9. cross-file call resolution through your ImportDecls
|
|
287
|
+
|
|
288
|
+
**Unregistered-adapter warning (applies to every pass, not just
|
|
289
|
+
ingest):** each analysis that reads source (`build_call_graph`,
|
|
290
|
+
`build_cfg`, `classify`) resolves the adapter per file extension over
|
|
291
|
+
*registered* adapters, and **silently produces empty results** for files
|
|
292
|
+
nobody claims. During development either register your adapter (in-tree
|
|
293
|
+
tuple or installed entry point) or pass it explicitly everywhere:
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
adapter = RustAdapter()
|
|
297
|
+
graph = TreeSitterSource(adapter=adapter).ingest(tmp_path)
|
|
298
|
+
tables = build_symbol_tables(graph)
|
|
299
|
+
build_call_graph(graph, tables, tmp_path, adapter=adapter)
|
|
300
|
+
build_cfg(graph, tmp_path, adapter=adapter)
|
|
301
|
+
effects = classify(graph, tmp_path, adapter=adapter)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Honesty requirements
|
|
305
|
+
|
|
306
|
+
Document your known limits in the module docstring (which dynamic features
|
|
307
|
+
you can't see; which effect rules are lexical guesses). CGIR's credibility
|
|
308
|
+
is that its claims are checkable — an adapter that silently over-claims
|
|
309
|
+
purity is worse than none.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Rust Adapter — Implementation Notes
|
|
2
|
+
|
|
3
|
+
## Doc gaps and ambiguities found during implementation
|
|
4
|
+
|
|
5
|
+
### 1. PinIndex comment type mismatch
|
|
6
|
+
**Gap:** The spec says "Wire pins using PinIndex per the doc's pattern," and PinIndex is documented as
|
|
7
|
+
"grammar-agnostic: both supported grammars use `comment` nodes." However, the Rust tree-sitter
|
|
8
|
+
grammar uses `line_comment` and `block_comment` node types, NOT `comment`. The base PinIndex
|
|
9
|
+
would silently miss all Rust `cgir:` pragmas.
|
|
10
|
+
|
|
11
|
+
**Resolution:** Implemented `RustPinIndex` — a complete reimplementation of PinIndex that extends
|
|
12
|
+
the type check to `{"comment", "line_comment", "block_comment"}`. This is a drop-in replacement
|
|
13
|
+
and the adapter uses it everywhere instead of PinIndex.
|
|
14
|
+
|
|
15
|
+
### 2. Pipeline adapter threading
|
|
16
|
+
**Gap:** The spec/doc does not mention that `classify`, `build_call_graph`, and `build_cfg` each
|
|
17
|
+
accept an optional `adapter` parameter. Without passing the RustAdapter, these functions call
|
|
18
|
+
`adapter_for_extension(".rs")` which returns `None` (since RustAdapter is a plugin not in the
|
|
19
|
+
builtin registry), silently giving empty results for effects, call sites, and CFG nodes.
|
|
20
|
+
|
|
21
|
+
**Resolution:** All pipeline calls must pass `adapter=adapter`. This is a required pattern for any
|
|
22
|
+
language plugin that is not registered in `cgir.languages.registry`. The test suite documents this
|
|
23
|
+
by always threading the adapter through.
|
|
24
|
+
|
|
25
|
+
### 3. `impl_item` type field name
|
|
26
|
+
**Gap:** The spec says "check if `impl_item.child_by_field_name('type')` works or if you need to
|
|
27
|
+
iterate children." `child_by_field_name("type")` works correctly — confirmed via grammar probing.
|
|
28
|
+
The field is indeed named `"type"` in the tree-sitter-rust 0.23.2 grammar.
|
|
29
|
+
|
|
30
|
+
### 4. `for_expression` field names
|
|
31
|
+
**Gap:** Spec was partially uncertain about `for`-loop field names. Confirmed via probing:
|
|
32
|
+
- `pattern` = loop variable (identifier)
|
|
33
|
+
- `value` = iterable expression
|
|
34
|
+
- `body` = the block
|
|
35
|
+
|
|
36
|
+
### 5. `if_expression` alternative field
|
|
37
|
+
**Gap:** Spec mentioned field might be `"alternative"`. Confirmed it IS `"alternative"` and
|
|
38
|
+
its child is `else_clause`. The `else_clause` contains either a `block` (for plain `else`) or
|
|
39
|
+
an `if_expression` (for `else if`).
|
|
40
|
+
|
|
41
|
+
### 6. `match_expression` fields
|
|
42
|
+
**Gap:** The spec mentioned field names might vary. Confirmed:
|
|
43
|
+
- `value` = the scrutinee expression
|
|
44
|
+
- `body` = match_block containing match_arm nodes
|
|
45
|
+
- Each `match_arm` has: `pattern` (match_pattern), `value` (the RHS expression/block)
|
|
46
|
+
|
|
47
|
+
### 7. Doc comment node naming
|
|
48
|
+
**Gap:** Spec says look for `line_comment` nodes with `outer_doc_comment_marker` child. This is
|
|
49
|
+
correct. The doc text is in a `doc_comment` named child within `line_comment`. However, doc
|
|
50
|
+
comment collection needs sibling-based walking (look backwards from function index in parent's
|
|
51
|
+
children list), not the tree-sitter grammar's parent/child relationship, since line_comment nodes
|
|
52
|
+
are siblings of function_item, not children.
|
|
53
|
+
|
|
54
|
+
### 8. `NodeKind.Function` vs `NodeKind.Method`
|
|
55
|
+
**Gap:** Not a Rust adapter issue per se, but test authors need to know: Rust impl methods are
|
|
56
|
+
ingested as `NodeKind.Method` (id prefix `method:`), while top-level functions are
|
|
57
|
+
`NodeKind.Function` (id prefix `func:`). `graph.nodes(NodeKind.Function)` returns only free
|
|
58
|
+
functions, not methods. Tests querying for methods must use `NodeKind.Method` or combine both.
|
|
59
|
+
|
|
60
|
+
### 9. `scoped_use_list` child structure
|
|
61
|
+
**Gap:** When parsing `use std::io::{Read, Write}`, the `scoped_use_list` node contains:
|
|
62
|
+
- An `identifier` or `scoped_identifier` child (the path prefix, e.g. `std::io`)
|
|
63
|
+
- A `use_list` named child containing `identifier` and `use_as_clause` items
|
|
64
|
+
|
|
65
|
+
The path prefix node is NOT always a `scoped_identifier` — for single-segment prefixes it's
|
|
66
|
+
`identifier`. The adapter handles both.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Known limits
|
|
71
|
+
|
|
72
|
+
### Effect detection limits
|
|
73
|
+
|
|
74
|
+
1. **Macro expansion:** Macros beyond `println!`, `panic!`, etc. that expand to effectful calls
|
|
75
|
+
are not detected. CGIR's design flags dynamic dispatch/macros as precision limits.
|
|
76
|
+
|
|
77
|
+
2. **Trait object calls:** `dyn Trait` method calls cannot be resolved to concrete implementations
|
|
78
|
+
without type inference. These appear as call sites but won't resolve to specific methods.
|
|
79
|
+
|
|
80
|
+
3. **`async`/`.await`:** Async functions are parsed identically to sync ones. The `await` keyword
|
|
81
|
+
appears as a child node in expressions but is not specially handled. Effects from async
|
|
82
|
+
functions work correctly since the body is walked the same way.
|
|
83
|
+
|
|
84
|
+
4. **`?` operator (question mark / error propagation):** `expr?` desugars to an early return +
|
|
85
|
+
`From::from` conversion. The `?` operator is not currently detected as a potential `raise`
|
|
86
|
+
source (it's not a panic). This is intentional — `?` is value-based error handling, not an
|
|
87
|
+
exception.
|
|
88
|
+
|
|
89
|
+
5. **`std::process::exit`:** Not in the effect tables. Could be added as a `raise`-like effect.
|
|
90
|
+
|
|
91
|
+
6. **`reqwest::blocking::` vs `reqwest::`:** Both map to `net` via the `reqwest.` prefix after
|
|
92
|
+
`::` → `.` normalization.
|
|
93
|
+
|
|
94
|
+
### Call site resolution limits
|
|
95
|
+
|
|
96
|
+
7. **Chained method calls:** `c.get(url).send()` — the adapter resolves the outermost call
|
|
97
|
+
(`send`) with a receiver that is itself a call expression. In this case `_field_expr_dotted`
|
|
98
|
+
recursively resolves the chain, yielding the callee name. This is best-effort; very deeply
|
|
99
|
+
chained expressions may produce incomplete dotted paths.
|
|
100
|
+
|
|
101
|
+
8. **Closure captures:** Calls inside closure bodies (`|| { foo() }`) are walked and collected,
|
|
102
|
+
but they are attributed to the containing function, not to a separate closure component.
|
|
103
|
+
|
|
104
|
+
9. **`use crate::...` imports:** The `crate` prefix is left as-is (`crate.store.MyStore`). The
|
|
105
|
+
CGIR central resolver must handle `crate` → current package name mapping. Without that mapping,
|
|
106
|
+
cross-crate-relative imports won't resolve to their target functions.
|
|
107
|
+
|
|
108
|
+
10. **Conditional compilation (`#[cfg(...)]`):** Attribute filtering is not applied; all items
|
|
109
|
+
are ingested regardless of `#[cfg]` attributes.
|
|
110
|
+
|
|
111
|
+
### CFG limits
|
|
112
|
+
|
|
113
|
+
11. **Implicit returns:** Rust blocks often return the last expression without `return`. These
|
|
114
|
+
appear as bare expressions in the block's named_children (e.g., `identifier`, `binary_expression`)
|
|
115
|
+
rather than `return_expression`. The adapter processes them as `SimpleDesc` nodes rather than
|
|
116
|
+
`ReturnDesc`. CFG topology still terminates correctly since the block ends.
|
|
117
|
+
|
|
118
|
+
12. **`loop` + `break value`:** A `loop { ... break value; }` expression can return a value via
|
|
119
|
+
`break`. The `break` is treated as a statement without connecting its value to a `ReturnDesc`.
|
|
120
|
+
|
|
121
|
+
13. **Pattern matching in `if let` / `while let`:** `if let Some(x) = expr` is parsed as
|
|
122
|
+
`if_expression` with a `let_condition` in the condition field rather than a plain expression.
|
|
123
|
+
The adapter extracts reads from the condition node generically, which collects identifiers
|
|
124
|
+
from the pattern as reads rather than writes. This is a minor inaccuracy in the CFG.
|
|
125
|
+
|
|
126
|
+
### Structural limits
|
|
127
|
+
|
|
128
|
+
14. **`enum` items:** `enum` declarations are not ingested as classes or any other typed node.
|
|
129
|
+
Enum variants are not extracted. This is a known omission — enums would need a new kind
|
|
130
|
+
(or `ClassDecl`) with each variant as a pseudo-field.
|
|
131
|
+
|
|
132
|
+
15. **`trait` items:** Trait declarations are not ingested. Trait method signatures are invisible
|
|
133
|
+
to the pipeline. Only concrete `impl` blocks are processed.
|
|
134
|
+
|
|
135
|
+
16. **Nested `impl` blocks / `impl Trait for Type`:** The adapter uses `child_by_field_name("type")`
|
|
136
|
+
to get the implementing type. For `impl Display for MyStruct`, the field is `name` (the trait
|
|
137
|
+
name) rather than `type` (the implementing type). The adapter handles the `"type"` field path
|
|
138
|
+
and falls back to iterating named_children for a `type_identifier`. Trait impls where the
|
|
139
|
+
implementing type is complex (generic bounds) may not resolve the struct name correctly.
|
|
140
|
+
|
|
141
|
+
17. **Same-crate cross-file calls without `use`:** Unlike Go (same-package = no import needed),
|
|
142
|
+
Rust requires explicit `use` or full path qualification for cross-module calls. A bare call
|
|
143
|
+
`helper(x)` in `b.rs` to `helper` defined in `a.rs` (same directory) will NOT resolve unless
|
|
144
|
+
`b.rs` has `use crate::a::helper`. The `use crate::a::helper` import correctly creates an
|
|
145
|
+
`ImportDecl` with `target="crate.a.helper"` and `alias="helper"`, which feeds the call graph.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Rust adapter — the docs-only experiment
|
|
2
|
+
|
|
3
|
+
Rust support was written by an **independent agent (Claude Sonnet) given
|
|
4
|
+
only [`docs/writing-an-adapter.md`](../../docs/writing-an-adapter.md)** —
|
|
5
|
+
no access to CGIR's source — as a usability test of the plugin docs. It hit
|
|
6
|
+
all 9 points of the doc's test bar (36/36 tests) in one sitting; the doc
|
|
7
|
+
gaps it surfaced were folded back into the guide, and its biggest find —
|
|
8
|
+
`PinIndex` missing `line_comment` grammars — became a core fix.
|
|
9
|
+
|
|
10
|
+
The adapter was then reviewed and **promoted to a builtin**:
|
|
11
|
+
`src/cgir/languages/rust.py` (tests: `tests/unit/test_rust_adapter.py`).
|
|
12
|
+
`NOTES.md` here preserves the agent's original gap report and limits list.
|
|
13
|
+
|
|
14
|
+
The takeaway for plugin authors: the guide is sufficient — an implementer
|
|
15
|
+
who has never seen the codebase produced a promotable adapter from it.
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
# Distribution name (the import package and CLI command are both `cgir`;
|
|
7
7
|
# the PyPI project name `cgir` was already taken, so we ship as codegraph-ir).
|
|
8
8
|
name = "codegraph-ir"
|
|
9
|
-
version = "0.
|
|
9
|
+
version = "0.4.0"
|
|
10
10
|
description = "The deterministic contract layer for AI-modified codebases — effects, purity, contracts, and blast radius, with zero LLM calls."
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
license = { file = "LICENSE" }
|
|
@@ -43,7 +43,10 @@ dependencies = [
|
|
|
43
43
|
"tree-sitter>=0.23,<0.25",
|
|
44
44
|
"tree-sitter-python>=0.23,<0.24",
|
|
45
45
|
"tree-sitter-typescript>=0.23,<0.24",
|
|
46
|
-
"tree-sitter-go>=0.23,<0.
|
|
46
|
+
"tree-sitter-go>=0.23,<0.24", # 0.24+ ships language ABI 15
|
|
47
|
+
# <0.23.3: rust grammar bumped language ABI to 15 in a PATCH release;
|
|
48
|
+
# tree-sitter 0.24 core supports <=14 (0.23.2 is the last ABI-14 wheel)
|
|
49
|
+
"tree-sitter-rust>=0.23,<0.23.3",
|
|
47
50
|
"jsonschema>=4.21",
|
|
48
51
|
]
|
|
49
52
|
|