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.
Files changed (154) hide show
  1. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/PKG-INFO +25 -3
  2. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/README.md +22 -1
  3. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/languages.md +44 -0
  4. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/status.md +4 -1
  5. codegraph_ir-0.4.0/docs/writing-an-adapter.md +309 -0
  6. codegraph_ir-0.4.0/examples/rust-adapter/NOTES.md +145 -0
  7. codegraph_ir-0.4.0/examples/rust-adapter/README.md +15 -0
  8. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/pyproject.toml +5 -2
  9. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/__init__.py +1 -1
  10. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/cli.py +15 -0
  11. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/languages/base.py +12 -1
  12. codegraph_ir-0.4.0/src/cgir/languages/registry.py +104 -0
  13. codegraph_ir-0.4.0/src/cgir/languages/rust.py +982 -0
  14. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_pins.py +18 -0
  15. codegraph_ir-0.4.0/tests/unit/test_plugin_registry.py +117 -0
  16. codegraph_ir-0.4.0/tests/unit/test_rust_adapter.py +634 -0
  17. codegraph_ir-0.3.0/src/cgir/languages/registry.py +0 -27
  18. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/.github/workflows/ci.yml +0 -0
  19. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/.github/workflows/release.yml +0 -0
  20. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/.gitignore +0 -0
  21. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/.pre-commit-config.yaml +0 -0
  22. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/.pre-commit-hooks.yaml +0 -0
  23. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/.python-version +0 -0
  24. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/CLAUDE.md +0 -0
  25. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/Code-IR.md +0 -0
  26. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/LICENSE +0 -0
  27. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/RELEASING.md +0 -0
  28. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/action.yml +0 -0
  29. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/README.md +0 -0
  30. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/agents.md +0 -0
  31. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/architecture-rules.md +0 -0
  32. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/architecture.md +0 -0
  33. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/development.md +0 -0
  34. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/experiment-log.md +0 -0
  35. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/feature-research.md +0 -0
  36. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/gate-noise.md +0 -0
  37. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/github-action.md +0 -0
  38. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/lsp.md +0 -0
  39. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/plan-0.2-0.4.md +0 -0
  40. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/plan-0.3.md +0 -0
  41. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/roadmap.md +0 -0
  42. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/docs/strategy.md +0 -0
  43. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/examples/agent-pr-contract-drift.md +0 -0
  44. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/schemas/component_spec.schema.json +0 -0
  45. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/__init__.py +0 -0
  46. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/call_graph.py +0 -0
  47. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/cfg.py +0 -0
  48. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/coverage_link.py +0 -0
  49. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/effects.py +0 -0
  50. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/entrypoints.py +0 -0
  51. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/param_flow.py +0 -0
  52. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/pdg.py +0 -0
  53. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/purity.py +0 -0
  54. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/reaching_defs.py +0 -0
  55. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/analyses/symbols.py +0 -0
  56. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/api/__init__.py +0 -0
  57. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/api/lsp_server.py +0 -0
  58. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/api/mcp_server.py +0 -0
  59. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/api/server.py +0 -0
  60. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/config.py +0 -0
  61. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/export/__init__.py +0 -0
  62. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/export/graphml.py +0 -0
  63. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/export/html_viz.py +0 -0
  64. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/export/json_export.py +0 -0
  65. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/export/mermaid.py +0 -0
  66. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/export/neo4j.py +0 -0
  67. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/hooks.py +0 -0
  68. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/ir/__init__.py +0 -0
  69. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/ir/component_spec.py +0 -0
  70. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/ir/edges.py +0 -0
  71. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/ir/graph.py +0 -0
  72. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/ir/nodes.py +0 -0
  73. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/languages/__init__.py +0 -0
  74. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/languages/cache.py +0 -0
  75. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/languages/go.py +0 -0
  76. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/languages/python.py +0 -0
  77. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/languages/typescript.py +0 -0
  78. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/manifest.py +0 -0
  79. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/pipeline.py +0 -0
  80. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/py.typed +0 -0
  81. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/regenerate/__init__.py +0 -0
  82. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/regenerate/prompt_pack.py +0 -0
  83. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/regenerate/regenerator.py +0 -0
  84. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/__init__.py +0 -0
  85. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/diff.py +0 -0
  86. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/flow.py +0 -0
  87. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/impact.py +0 -0
  88. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/lint.py +0 -0
  89. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/pack.py +0 -0
  90. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/pins.py +0 -0
  91. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/search.py +0 -0
  92. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/report/stats.py +0 -0
  93. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/slicing/__init__.py +0 -0
  94. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/slicing/slicer.py +0 -0
  95. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/sources/__init__.py +0 -0
  96. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/sources/base.py +0 -0
  97. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/sources/codeql_source.py +0 -0
  98. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/sources/joern_source.py +0 -0
  99. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/sources/tree_sitter_source.py +0 -0
  100. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/trace/__init__.py +0 -0
  101. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/trace/trace_map.py +0 -0
  102. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/verify.py +0 -0
  103. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/src/cgir/watch.py +0 -0
  104. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/__init__.py +0 -0
  105. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/conftest.py +0 -0
  106. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/fixtures/python_sample/orchestrator.py +0 -0
  107. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/fixtures/python_sample/pricing.py +0 -0
  108. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/fixtures/ts_sample/api/service.ts +0 -0
  109. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/fixtures/ts_sample/util.ts +0 -0
  110. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/integration/__init__.py +0 -0
  111. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/integration/test_api.py +0 -0
  112. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/integration/test_cli_scan.py +0 -0
  113. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/integration/test_hook.py +0 -0
  114. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/integration/test_init.py +0 -0
  115. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/__init__.py +0 -0
  116. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_action.py +0 -0
  117. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_call_graph.py +0 -0
  118. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_cfg.py +0 -0
  119. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_component_spec.py +0 -0
  120. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_confidence.py +0 -0
  121. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_coverage_link.py +0 -0
  122. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_diff.py +0 -0
  123. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_effects.py +0 -0
  124. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_entrypoints.py +0 -0
  125. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_go_adapter.py +0 -0
  126. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_graphml.py +0 -0
  127. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_html_viz.py +0 -0
  128. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_impact.py +0 -0
  129. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_impact_run.py +0 -0
  130. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_ir_graph.py +0 -0
  131. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_language_adapter.py +0 -0
  132. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_lint.py +0 -0
  133. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_lsp.py +0 -0
  134. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_manifest.py +0 -0
  135. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_mcp_tools.py +0 -0
  136. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_mermaid.py +0 -0
  137. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_pack.py +0 -0
  138. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_param_flow.py +0 -0
  139. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_pdg.py +0 -0
  140. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_purity.py +0 -0
  141. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_python_di.py +0 -0
  142. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_reaching_defs.py +0 -0
  143. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_regenerator.py +0 -0
  144. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_search.py +0 -0
  145. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_shapes.py +0 -0
  146. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_slicer.py +0 -0
  147. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_source_cache.py +0 -0
  148. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_stats.py +0 -0
  149. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_symbols.py +0 -0
  150. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_trace_map.py +0 -0
  151. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_tree_sitter_source.py +0 -0
  152. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_typescript_adapter.py +0 -0
  153. {codegraph_ir-0.3.0 → codegraph_ir-0.4.0}/tests/unit/test_verify.py +0 -0
  154. {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.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.25,>=0.23
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 498 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):
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.3.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.25",
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
 
@@ -3,4 +3,4 @@
3
3
  See ``Code-IR.md`` at the repo root for the product specification.
4
4
  """
5
5
 
6
- __version__ = "0.3.0"
6
+ __version__ = "0.4.0"