codegraph-ir 0.2.0__tar.gz → 0.3.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/.pre-commit-hooks.yaml +19 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/Code-IR.md +3 -1
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/PKG-INFO +7 -1
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/README.md +4 -0
- codegraph_ir-0.3.0/docs/agents.md +70 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/docs/architecture-rules.md +20 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/docs/gate-noise.md +9 -1
- codegraph_ir-0.3.0/docs/lsp.md +40 -0
- codegraph_ir-0.3.0/docs/plan-0.3.md +109 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/docs/roadmap.md +1 -1
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/docs/status.md +9 -1
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/pyproject.toml +8 -4
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/schemas/component_spec.schema.json +6 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/__init__.py +1 -1
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/analyses/cfg.py +15 -1
- codegraph_ir-0.3.0/src/cgir/analyses/coverage_link.py +152 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/analyses/effects.py +49 -15
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/analyses/symbols.py +46 -1
- codegraph_ir-0.3.0/src/cgir/api/lsp_server.py +168 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/api/mcp_server.py +10 -14
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/cli.py +38 -1
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/hooks.py +4 -1
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/ir/component_spec.py +3 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/languages/base.py +16 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/languages/go.py +24 -11
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/languages/python.py +82 -28
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/languages/typescript.py +45 -16
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/pipeline.py +40 -2
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/report/diff.py +29 -1
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/report/lint.py +90 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/report/pack.py +6 -1
- codegraph_ir-0.3.0/src/cgir/report/search.py +182 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/slicing/slicer.py +8 -1
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/sources/tree_sitter_source.py +20 -1
- codegraph_ir-0.3.0/tests/unit/test_confidence.py +122 -0
- codegraph_ir-0.3.0/tests/unit/test_coverage_link.py +128 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_go_adapter.py +44 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_lint.py +72 -0
- codegraph_ir-0.3.0/tests/unit/test_lsp.py +112 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_pins.py +24 -0
- codegraph_ir-0.3.0/tests/unit/test_search.py +136 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_slicer.py +66 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/.github/workflows/ci.yml +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/.github/workflows/release.yml +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/.gitignore +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/.pre-commit-config.yaml +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/.python-version +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/CLAUDE.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/LICENSE +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/RELEASING.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/action.yml +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/docs/README.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/docs/architecture.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/docs/development.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/docs/experiment-log.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/docs/feature-research.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/docs/github-action.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/docs/languages.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/docs/plan-0.2-0.4.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/docs/strategy.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/examples/agent-pr-contract-drift.md +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/analyses/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/analyses/call_graph.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/analyses/entrypoints.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/analyses/param_flow.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/analyses/pdg.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/analyses/purity.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/analyses/reaching_defs.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/api/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/api/server.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/config.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/export/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/export/graphml.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/export/html_viz.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/export/json_export.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/export/mermaid.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/export/neo4j.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/ir/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/ir/edges.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/ir/graph.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/ir/nodes.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/languages/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/languages/cache.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/languages/registry.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/manifest.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/py.typed +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/regenerate/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/regenerate/prompt_pack.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/regenerate/regenerator.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/report/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/report/flow.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/report/impact.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/report/pins.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/report/stats.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/slicing/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/sources/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/sources/base.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/sources/codeql_source.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/sources/joern_source.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/trace/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/trace/trace_map.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/verify.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/src/cgir/watch.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/conftest.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/fixtures/python_sample/orchestrator.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/fixtures/python_sample/pricing.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/fixtures/ts_sample/api/service.ts +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/fixtures/ts_sample/util.ts +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/integration/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/integration/test_api.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/integration/test_cli_scan.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/integration/test_hook.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/integration/test_init.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/__init__.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_action.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_call_graph.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_cfg.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_component_spec.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_diff.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_effects.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_entrypoints.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_graphml.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_html_viz.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_impact.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_impact_run.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_ir_graph.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_language_adapter.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_manifest.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_mcp_tools.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_mermaid.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_pack.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_param_flow.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_pdg.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_purity.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_python_di.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_reaching_defs.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_regenerator.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_shapes.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_source_cache.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_stats.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_symbols.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_trace_map.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_tree_sitter_source.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_typescript_adapter.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_verify.py +0 -0
- {codegraph_ir-0.2.0 → codegraph_ir-0.3.0}/tests/unit/test_watch.py +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# CGIR contract seatbelt for the pre-commit framework (https://pre-commit.com).
|
|
2
|
+
# In your repo's .pre-commit-config.yaml:
|
|
3
|
+
#
|
|
4
|
+
# repos:
|
|
5
|
+
# - repo: https://github.com/asonkiya/llm-semantic-compilers
|
|
6
|
+
# rev: v0.2.1
|
|
7
|
+
# hooks:
|
|
8
|
+
# - id: cgir-contract-check
|
|
9
|
+
#
|
|
10
|
+
- id: cgir-contract-check
|
|
11
|
+
name: cgir contract seatbelt
|
|
12
|
+
description: >-
|
|
13
|
+
Block commits that alter component contracts: effect gain/loss on
|
|
14
|
+
net/fs/db, pin violations (cgir: pure / stable-signature / frozen),
|
|
15
|
+
and referenced type-shape drift.
|
|
16
|
+
entry: cgir hook run
|
|
17
|
+
language: python
|
|
18
|
+
pass_filenames: false
|
|
19
|
+
types_or: [python, ts, tsx, go]
|
|
@@ -60,7 +60,9 @@ H --> I[LLM regenerate / test / trace]
|
|
|
60
60
|
"trace":{"type":"array","items":{"type":"string"}}}}
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
`
|
|
63
|
+
`lexical_effects` marks the subset of `effects` backed only by lexical
|
|
64
|
+
(suffix/receiver-name) evidence — lower confidence, reported but not
|
|
65
|
+
build-failing by default. `pins` are developer-declared invariants extracted from `cgir:` comment
|
|
64
66
|
pragmas (`pure`, `no-<tag>`, `stable-signature`, `frozen`): state pins hold on
|
|
65
67
|
every scan; change pins hold across scan pairs and are always enforced.
|
|
66
68
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codegraph-ir
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -61,6 +61,8 @@ Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
|
61
61
|
Requires-Dist: types-jsonschema; extra == 'dev'
|
|
62
62
|
Provides-Extra: llm
|
|
63
63
|
Requires-Dist: anthropic>=0.40; extra == 'llm'
|
|
64
|
+
Provides-Extra: lsp
|
|
65
|
+
Requires-Dist: pygls>=1.3; extra == 'lsp'
|
|
64
66
|
Provides-Extra: mcp
|
|
65
67
|
Requires-Dist: mcp>=1.2; extra == 'mcp'
|
|
66
68
|
Description-Content-Type: text/markdown
|
|
@@ -97,6 +99,7 @@ cgir impact app.service.charge # blast radius: affected callers, entryp
|
|
|
97
99
|
cgir impact app.service.charge --candidate new.py --repo . # radius narrowed by the real delta
|
|
98
100
|
cgir verify app.service.charge --candidate new.py --repo . # contract-check an edit
|
|
99
101
|
cgir hook install # pre-commit seatbelt: block contract-breaking commits
|
|
102
|
+
cgir lsp # editor squiggles for pin violations + drift (cgir[lsp])
|
|
100
103
|
```
|
|
101
104
|
|
|
102
105
|
`pack` → edit → `impact` → `verify` → `hook`, with `watch` keeping the index
|
|
@@ -127,6 +130,9 @@ change touches, and `verify` / `impact_of_change` to contract-check its own
|
|
|
127
130
|
edit before proposing it. See [`examples/`](./examples) for a worked
|
|
128
131
|
agent-PR case study.
|
|
129
132
|
|
|
133
|
+
Setup guide for agents (MCP config + CLAUDE.md snippet): [`docs/agents.md`](./docs/agents.md).
|
|
134
|
+
Or via the [pre-commit framework](https://pre-commit.com): hook id `cgir-contract-check`.
|
|
135
|
+
|
|
130
136
|
## Docs
|
|
131
137
|
|
|
132
138
|
- [`docs/strategy.md`](./docs/strategy.md) — positioning: the deterministic contract layer
|
|
@@ -30,6 +30,7 @@ cgir impact app.service.charge # blast radius: affected callers, entryp
|
|
|
30
30
|
cgir impact app.service.charge --candidate new.py --repo . # radius narrowed by the real delta
|
|
31
31
|
cgir verify app.service.charge --candidate new.py --repo . # contract-check an edit
|
|
32
32
|
cgir hook install # pre-commit seatbelt: block contract-breaking commits
|
|
33
|
+
cgir lsp # editor squiggles for pin violations + drift (cgir[lsp])
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
`pack` → edit → `impact` → `verify` → `hook`, with `watch` keeping the index
|
|
@@ -60,6 +61,9 @@ change touches, and `verify` / `impact_of_change` to contract-check its own
|
|
|
60
61
|
edit before proposing it. See [`examples/`](./examples) for a worked
|
|
61
62
|
agent-PR case study.
|
|
62
63
|
|
|
64
|
+
Setup guide for agents (MCP config + CLAUDE.md snippet): [`docs/agents.md`](./docs/agents.md).
|
|
65
|
+
Or via the [pre-commit framework](https://pre-commit.com): hook id `cgir-contract-check`.
|
|
66
|
+
|
|
63
67
|
## Docs
|
|
64
68
|
|
|
65
69
|
- [`docs/strategy.md`](./docs/strategy.md) — positioning: the deterministic contract layer
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# CGIR for agents — setup and protocol
|
|
2
|
+
|
|
3
|
+
Agents are CGIR's first-class users: instead of grepping and reading whole
|
|
4
|
+
files, an agent queries the semantic index and contract-checks its own edits
|
|
5
|
+
before proposing them.
|
|
6
|
+
|
|
7
|
+
## 1. Serve the index over MCP
|
|
8
|
+
|
|
9
|
+
Scan once (or run `cgir watch` to keep it live), then register the server:
|
|
10
|
+
|
|
11
|
+
```jsonc
|
|
12
|
+
// .mcp.json at the repo root (Claude Code picks this up automatically)
|
|
13
|
+
{
|
|
14
|
+
"mcpServers": {
|
|
15
|
+
"cgir": {
|
|
16
|
+
"command": "cgir",
|
|
17
|
+
"args": ["mcp", "--index", ".cgir"]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires `cgir` on PATH with the mcp extra (`uv tool install "codegraph-ir[mcp]"`
|
|
24
|
+
or `uv pip install "codegraph-ir[mcp]"`).
|
|
25
|
+
|
|
26
|
+
## 2. Teach the agent the protocol (CLAUDE.md / AGENTS.md snippet)
|
|
27
|
+
|
|
28
|
+
Paste into the repo's agent instructions:
|
|
29
|
+
|
|
30
|
+
```markdown
|
|
31
|
+
## Semantic index (CGIR)
|
|
32
|
+
|
|
33
|
+
This repo has a semantic contract index served over MCP (server: `cgir`).
|
|
34
|
+
Prefer it over grepping:
|
|
35
|
+
|
|
36
|
+
- **Finding code**: `search(query)` — ranked free terms + contract
|
|
37
|
+
predicates: `kind:pure`, `effects:net`, `effects:none`, `lexical:false`,
|
|
38
|
+
`callers:>3`, `pins:pure`, `entrypoint:HTTP`, `lang:go`, `covered:false`.
|
|
39
|
+
E.g. "effects:net lexical:false callers:>2" = verified network code with
|
|
40
|
+
real fan-in — queries grep and vector search cannot answer.
|
|
41
|
+
- **Loading context to edit a component**: `pack(component_id)` — the
|
|
42
|
+
minimal contract bundle (signature, effects, pins, callee interfaces,
|
|
43
|
+
linked tests). Do NOT read whole files for context first.
|
|
44
|
+
- **Before changing a component**: `impact(component_id)` — blast radius:
|
|
45
|
+
affected callers, entrypoints at risk, exactly which tests to run.
|
|
46
|
+
- **After drafting an edit**: `impact_of_change(repo, component_id,
|
|
47
|
+
candidate)` — the radius narrowed by your edit's real contract delta;
|
|
48
|
+
`verify(repo, component_id, candidate)` — contract-check before proposing.
|
|
49
|
+
|
|
50
|
+
Respect `Pinned:` lines in packs — they are hard invariants (`pure`,
|
|
51
|
+
`no-net`, `stable-signature`, `frozen`), enforced by the pre-commit hook
|
|
52
|
+
and CI. A rewrite that violates a pin will be rejected; don't attempt it.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 3. What the agent gets out of it
|
|
56
|
+
|
|
57
|
+
- **Fewer tokens, better context**: a pack is ~60–800 tokens vs ~400–20,000
|
|
58
|
+
for raw files, and benchmarks show it *beats* whole-file context as
|
|
59
|
+
rewrite input (`docs/experiment-log.md`).
|
|
60
|
+
- **Fewer rejected edits**: `verify`/`impact_of_change` catch contract
|
|
61
|
+
drift before the human (or the hook, or CI) does.
|
|
62
|
+
- **No hallucinated wiring**: packs name DI receivers (`self.svc.method` /
|
|
63
|
+
`this.svc.method`), the exact field names an edit must use.
|
|
64
|
+
|
|
65
|
+
## 4. The enforcement backstop
|
|
66
|
+
|
|
67
|
+
The same contracts the agent consults are enforced deterministically:
|
|
68
|
+
`cgir hook install` (pre-commit), the [GitHub Action](./github-action.md)
|
|
69
|
+
(per-PR), and `cgir: ` pins in source. The agent can't drift from what the
|
|
70
|
+
index says without something noticing — that's the point.
|
|
@@ -58,3 +58,23 @@ component id (`aspen.zones.*` matches `aspen.zones.contains.point_in_polygon`).
|
|
|
58
58
|
- **Kind-aware**: "handlers must be orchestrators, not effect adapters."
|
|
59
59
|
- **Call-target-aware**: layer boundaries over *resolved calls*, catching
|
|
60
60
|
a cross-layer call even when the import is indirect.
|
|
61
|
+
|
|
62
|
+
## Cycles and layers
|
|
63
|
+
|
|
64
|
+
```toml
|
|
65
|
+
[[rule]]
|
|
66
|
+
name = "core is acyclic"
|
|
67
|
+
in = "app.*"
|
|
68
|
+
forbid-cycle = true # Tarjan SCCs over resolved CALLS; self-recursion OK
|
|
69
|
+
|
|
70
|
+
[[rule]]
|
|
71
|
+
name = "layered architecture"
|
|
72
|
+
layers = ["app.api.*", "app.core.*", "app.db.*"] # top -> bottom
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`forbid-cycle` fires once per strongly-connected component (size >= 2),
|
|
76
|
+
naming the cycle. `layers` requires dependencies to point *downward*:
|
|
77
|
+
a `db` component calling an `api` component is a violation; same-layer and
|
|
78
|
+
layer-skipping downward calls are fine; components matching no layer are
|
|
79
|
+
ignored. Unlike import linters, these run over the resolved *call* graph —
|
|
80
|
+
and the same index tells you whether the violating edge also carries `net`.
|
|
@@ -88,7 +88,15 @@ the `io` / `nondeterm` effect tags (a `print`/timestamp shouldn't fail CI).
|
|
|
88
88
|
Trade-off (documented in `diff.violations`): a true removal paired with a
|
|
89
89
|
new unrelated effectful call is masked — the loss stays visible in the
|
|
90
90
|
diff report, it just doesn't fail the build.
|
|
91
|
-
2. **
|
|
91
|
+
2. **Confidence tiers** *(landed 0.3.0)* — every effect tag now carries
|
|
92
|
+
provenance: `high` (exact/prefix table match) vs `lexical` (bare-suffix /
|
|
93
|
+
receiver-name heuristics — `self.now()`, `db.query()`). Gate rules fire
|
|
94
|
+
on high-confidence tags by default; `:any` opts in
|
|
95
|
+
(`effect-gain:db:any`). Measured on camera-tracking: fs evidence is 89%
|
|
96
|
+
lexical, and 48% of the `calls_effectful` taint tree rests on lexical
|
|
97
|
+
evidence — all still *reported*, no longer build-failing. Lexical means
|
|
98
|
+
unverified, not wrong.
|
|
99
|
+
3. **Import-resolution stability** — kind/purity flipping on stub-vs-real
|
|
92
100
|
imports is the dominant `kind-change`/`purity-drop` noise source. A
|
|
93
101
|
scan run compares two checkouts in one environment (CI does this), so it
|
|
94
102
|
only bites when the stub *source itself* changes — but it's worth
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# LSP diagnostics — contract drift in your editor
|
|
2
|
+
|
|
3
|
+
`cgir lsp` is a diagnostics-only language server: on every save it rescans
|
|
4
|
+
the repo and publishes
|
|
5
|
+
|
|
6
|
+
- **errors** for pin violations (`# cgir: pure` that isn't) — visible the
|
|
7
|
+
moment you open the workspace;
|
|
8
|
+
- **warnings** for contract drift vs your previous save (the default
|
|
9
|
+
low-noise gate rules: effect gain/loss on net/fs/db, change pins).
|
|
10
|
+
|
|
11
|
+
Side effect worth having: every refresh rewrites `.cgir`, so MCP, `pack`,
|
|
12
|
+
and `impact` stay fresh while you edit.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv tool install "codegraph-ir[lsp]" # or add the lsp extra to your env
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Neovim
|
|
19
|
+
|
|
20
|
+
```lua
|
|
21
|
+
vim.api.nvim_create_autocmd("FileType", {
|
|
22
|
+
pattern = { "python", "typescript", "go" },
|
|
23
|
+
callback = function()
|
|
24
|
+
vim.lsp.start({ name = "cgir", cmd = { "cgir", "lsp" },
|
|
25
|
+
root_dir = vim.fs.root(0, ".cgir") or vim.fn.getcwd() })
|
|
26
|
+
end,
|
|
27
|
+
})
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## VS Code
|
|
31
|
+
|
|
32
|
+
Use any generic LSP client extension (e.g. "LSP Proxy"/"Generic LSP
|
|
33
|
+
Client") with command `cgir lsp`, or wire it into an existing extension's
|
|
34
|
+
`serverCommand` setting. A dedicated extension is on the roadmap.
|
|
35
|
+
|
|
36
|
+
## Latency honesty
|
|
37
|
+
|
|
38
|
+
Each save triggers a full rescan (~0.5s small repos, ~3–5s large ones).
|
|
39
|
+
Diagnostics update when the scan lands; per-component incremental analysis
|
|
40
|
+
(docs/plan-0.3.md) is the planned fix.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Plan: 0.3 and beyond — trust, reach, and the developer loop
|
|
2
|
+
|
|
3
|
+
Written 2026-07-12 as a durable handoff: any session (any model) should be
|
|
4
|
+
able to pick up any thread from here. Prior art: `plan-0.2-0.4.md` (all
|
|
5
|
+
landed), `gate-noise.md` (measured rule noise), `experiment-log.md`
|
|
6
|
+
(benchmarks), `status.md` (what runs).
|
|
7
|
+
|
|
8
|
+
## Shipped in the 0.3 cook (see git log 2026-07-12)
|
|
9
|
+
|
|
10
|
+
1. **Hook UX** — blocked-commit message explains the staged-state trap
|
|
11
|
+
(`git restore -S -W <file>`), found dogfooding.
|
|
12
|
+
2. **pre-commit framework integration** — `.pre-commit-hooks.yaml` so
|
|
13
|
+
`repo: llm-semantic-compilers` works in anyone's `.pre-commit-config.yaml`.
|
|
14
|
+
3. **Agent onboarding** — `docs/agents.md`: the MCP config + CLAUDE.md/
|
|
15
|
+
AGENTS.md snippet teaching agents the protocol (search → pack → edit →
|
|
16
|
+
impact_of_change → verify).
|
|
17
|
+
4. **go.mod cross-package resolution** — ingest reads the `module` directive
|
|
18
|
+
and stashes it on the Repository node; symbols strips the prefix from Go
|
|
19
|
+
import targets and binds the alias to the target *package directory*
|
|
20
|
+
(any module in it — the directory merge exposes all package members).
|
|
21
|
+
5. **Confidence tiers on effects** — every effect tag carries provenance:
|
|
22
|
+
`high` (exact/prefix table match, import-alias verified) vs `lexical`
|
|
23
|
+
(bare suffix / receiver-name heuristics: `.now`, db-receiver gating).
|
|
24
|
+
New spec field `lexical_effects` (subset of `effects`). Gate rules
|
|
25
|
+
(`effect-gain`/`effect-loss`) fire on high-confidence tags by default;
|
|
26
|
+
`:any` suffix opts into lexical (e.g. `effect-gain:db:any`). This is the
|
|
27
|
+
systematic fix for the `self.now()` false-positive class measured in
|
|
28
|
+
gate-noise.md.
|
|
29
|
+
|
|
30
|
+
## In flight: market-driven quick strikes (designed 2026-07-12)
|
|
31
|
+
|
|
32
|
+
**A. Ranked, structured search** (`report/search.py`, pure over specs; MCP
|
|
33
|
+
`search` delegates; new `cgir search` CLI command).
|
|
34
|
+
|
|
35
|
+
- Free terms rank: exact name (100) > name-word (50) > id substring (20) >
|
|
36
|
+
entrypoint (15) > doc (5); sum across terms, sort desc, cap 25.
|
|
37
|
+
- Structured predicates (colon syntax, AND-combined, mixable with terms):
|
|
38
|
+
`kind:pure_function` (alias `kind:pure`), `effects:net`, `effects:none`,
|
|
39
|
+
`lexical:true|false`, `callers:>3|0`, `calls:>5`, `pins:pure`,
|
|
40
|
+
`pinned:true`, `lang:go`, `entrypoint:true|HTTP`, `covered:false`.
|
|
41
|
+
Caller counts computed from the spec set (reverse of `calls`).
|
|
42
|
+
- The moat angle: contract predicates no vector index can answer
|
|
43
|
+
("effects:net lexical:false callers:>3").
|
|
44
|
+
|
|
45
|
+
**B. Cycles + layers in `cgir lint`** (existing `[[rule]]` config).
|
|
46
|
+
|
|
47
|
+
- `forbid-cycle = true` (+ `in` scope glob): Tarjan SCCs over the spec-level
|
|
48
|
+
call graph within scope; every SCC of size ≥2 is a violation naming the
|
|
49
|
+
cycle. Self-recursion (size-1) is NOT a violation — recursion is normal.
|
|
50
|
+
- `layers = ["app.api.*", "app.core.*", "app.db.*"]` (top→bottom): a
|
|
51
|
+
component in a lower layer calling one in a higher layer is a violation
|
|
52
|
+
(dependencies must point down; same-layer calls fine; unmatched
|
|
53
|
+
components ignored).
|
|
54
|
+
- Neutralizes the import-linter/ArchUnit objection using graph data we
|
|
55
|
+
already have — and our violations can say the edge *also* carries `net`.
|
|
56
|
+
|
|
57
|
+
**C. Coverage-grounded test linkage** *(landed)* — `.coverage` (sqlite,
|
|
58
|
+
numbits decoded via stdlib) / `coverage.json --show-contexts` per-test
|
|
59
|
+
contexts mapped onto component spans, unioned with static linkage. On CGIR
|
|
60
|
+
itself (pytest-cov --cov-context=test): components with test linkage went
|
|
61
|
+
48 → 345 of 389 (12% → 89%). Hot components link many tests (integration
|
|
62
|
+
tests execute the pipeline — accurate); pack embeds at most 5 test sources,
|
|
63
|
+
impact --run keeps the full set. Staleness limit: an old .coverage may
|
|
64
|
+
reference moved lines; static linkage is the floor.
|
|
65
|
+
|
|
66
|
+
## Not built — next up, in value order
|
|
67
|
+
|
|
68
|
+
1. **Per-component incremental analysis** (the latency lever). Watch/hook
|
|
69
|
+
re-scan everything (~0.5–4.5s). Design sketch: content-hash manifest
|
|
70
|
+
identifies changed files → re-ingest only those modules → recompute
|
|
71
|
+
analyses for changed components + their upstream closure
|
|
72
|
+
(`compute_typed_impact` IS the scoping machinery) → splice into the
|
|
73
|
+
cached graph. Correctness traps: effect closure is global (a leaf change
|
|
74
|
+
can flip distant orchestrators — the closure must be re-run over the
|
|
75
|
+
merged spec set, which is cheap since it's pure over specs); symbol
|
|
76
|
+
tables must be rebuilt when imports/decls change, else only call-graph
|
|
77
|
+
edges from changed files. Validate: randomized edit-replay equivalence
|
|
78
|
+
(incremental result == full-scan result on N random historical edits).
|
|
79
|
+
2. **vitest/jest execution for `impact --run`** — runner detection from
|
|
80
|
+
package.json; currently prints the command for TS.
|
|
81
|
+
3. **Go: struct-in-one-file/methods-in-another field DI** — the class-stub
|
|
82
|
+
merge (registry of package-level types before method attachment).
|
|
83
|
+
4. **Data-shape v1.1** — full type text for Python fields (needs a second,
|
|
84
|
+
non-DI fields channel), literal-key fingerprints for un-annotated dict
|
|
85
|
+
returns, base-class field inheritance.
|
|
86
|
+
5. **LSP diagnostics** — watch already computes live drift; publish as
|
|
87
|
+
editor diagnostics (pygls; diagnostics-only server, no completion).
|
|
88
|
+
6. **`cgir decompose`** — the spec's long-term flagship (PDG-sliced
|
|
89
|
+
functional-core/imperative-shell suggestions + verify loop).
|
|
90
|
+
7. **Marketplace listing** for the Action; badges; a demo GIF in README.
|
|
91
|
+
|
|
92
|
+
## The developer loop (what "useful for me" means, concretely)
|
|
93
|
+
|
|
94
|
+
The user's repos and their CGIR state:
|
|
95
|
+
- **camera-tracking** (Py+TS): pins live on branch `cgir-pins`; pre-commit
|
|
96
|
+
hook INSTALLED on the repo (uses PATH cgir = uv tool). `.mcp.json` +
|
|
97
|
+
CLAUDE.md snippet added in this cook so agents use the semantic index.
|
|
98
|
+
- **Indra** (Go): scans clean (301 components). Candidate for pins next.
|
|
99
|
+
- **novel-chrome-extension** (TS/Angular): benchmark corpus, index at
|
|
100
|
+
~/.cgir-indexes/novel-frontend.
|
|
101
|
+
|
|
102
|
+
Daily loop: `cgir watch` in a terminal (live drift), agents route through
|
|
103
|
+
MCP (`pack`/`impact`/`verify` instead of grep), the hook gates commits, CI
|
|
104
|
+
gates PRs (Action), pins encode the invariants worth keeping.
|
|
105
|
+
|
|
106
|
+
## Release discipline
|
|
107
|
+
|
|
108
|
+
Tag-triggered trusted publishing (RELEASING.md). Bump BOTH version spots.
|
|
109
|
+
The maintainer pushes tags — releases are a human decision.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Roadmap
|
|
2
2
|
|
|
3
|
-
**Active plan: [`plan-0.
|
|
3
|
+
**Active plan: [`plan-0.3.md`](./plan-0.3.md)** — trust (confidence tiers), reach (go.mod resolution, pre-commit, agents), and the incremental-analysis design. `plan-0.2-0.4.md` is fully landed.
|
|
4
4
|
|
|
5
5
|
Forward-looking sequencing. The grouping mirrors `Code-IR.md` §Architecture: **P0** is "you can produce a `ComponentSpec`," **P1** is "you can trust it," **P2** is "you can scale it." Within each tier the order below reflects current dependencies, not strict chronology — feel free to interleave when dependencies allow.
|
|
6
6
|
|
|
@@ -71,6 +71,14 @@ cgir scan tests/fixtures/python_sample --out /tmp/cgir-out
|
|
|
71
71
|
| `cgir impact --run` (execute the blast-radius tests via pytest node-ids) | done | `report/impact.py:runnable_selectors` |
|
|
72
72
|
| Data-shape contracts (`types` diff section + opt-in `shape-change` rule; TS interfaces/type-aliases, Py TypedDict/dataclass) | done | `report/diff.py:_type_shape_changes`, `json_export.read_types` |
|
|
73
73
|
| Go adapter (package=dir symbol merge, struct-field DI, panic≙raise, effects tables) | done | `src/cgir/languages/go.py` |
|
|
74
|
+
| Go cross-package resolution (go.mod module-prefix strip; unique dir-suffix fallback) | done | `analyses/symbols.py:_resolve_go_package` |
|
|
75
|
+
| Confidence tiers (`lexical_effects`; gates fire high-confidence by default, `:any` opts in; pack marks `tag?`) | done | `analyses/effects.py:classify_with_confidence` |
|
|
76
|
+
| Global/nonlocal rebinding as mutation (`global_declared_names` hook) | done | `languages/base.py`, `analyses/cfg.py` |
|
|
77
|
+
| pre-commit framework hook (`cgir-contract-check`) + agents guide | done | `.pre-commit-hooks.yaml`, `docs/agents.md` |
|
|
78
|
+
| Ranked/structured search (`cgir search`, MCP `search`): term ranking + contract predicates (kind/effects/lexical/callers/pins/entrypoint/lang/covered) | done | `src/cgir/report/search.py` |
|
|
79
|
+
| `forbid-cycle` (Tarjan SCCs) + `layers` rules in lint | done | `report/lint.py:_cycle_violations`, `_layer_violations` |
|
|
80
|
+
| Coverage-grounded test linkage (.coverage/coverage.json contexts ∪ static; pack test sources capped) | done | `analyses/coverage_link.py` |
|
|
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` |
|
|
74
82
|
| Entrypoint recognition (HTTP/CLI/task decorators) | done | `src/cgir/analyses/entrypoints.py` |
|
|
75
83
|
| Context packer (`cgir pack`, budget-aware, type closure + docstrings + raises) | done | `src/cgir/report/pack.py` |
|
|
76
84
|
| Docstring / raises / module-variable extraction | done | `tree_sitter_source._docstring_text`, `_raised_names`, `_add_module_variables` |
|
|
@@ -90,7 +98,7 @@ cgir scan tests/fixtures/python_sample --out /tmp/cgir-out
|
|
|
90
98
|
|
|
91
99
|
## Test coverage
|
|
92
100
|
|
|
93
|
-
`pytest -q` runs
|
|
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):
|
|
94
102
|
|
|
95
103
|
| File | Covers |
|
|
96
104
|
|---|---|
|
|
@@ -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.3.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" }
|
|
@@ -51,6 +51,7 @@ dependencies = [
|
|
|
51
51
|
api = ["fastapi>=0.110", "uvicorn>=0.27"]
|
|
52
52
|
llm = ["anthropic>=0.40"]
|
|
53
53
|
mcp = ["mcp>=1.2"]
|
|
54
|
+
lsp = ["pygls>=1.3"]
|
|
54
55
|
dev = [
|
|
55
56
|
"pytest>=8.0",
|
|
56
57
|
"pytest-cov>=5.0",
|
|
@@ -90,13 +91,16 @@ warn_unused_ignores = true
|
|
|
90
91
|
warn_redundant_casts = true
|
|
91
92
|
|
|
92
93
|
[[tool.mypy.overrides]]
|
|
93
|
-
module = ["tree_sitter.*", "tree_sitter_python.*", "networkx.*", "anthropic.*", "mcp.*"]
|
|
94
|
+
module = ["tree_sitter.*", "tree_sitter_python.*", "networkx.*", "anthropic.*", "mcp.*", "pygls.*", "lsprotocol.*"]
|
|
94
95
|
ignore_missing_imports = true
|
|
95
96
|
|
|
96
97
|
[[tool.mypy.overrides]]
|
|
97
|
-
# FastMCP
|
|
98
|
-
|
|
98
|
+
# FastMCP/pygls are lazily imported (optional extras), so their decorators are
|
|
99
|
+
# Any — and the pygls 1.x-fallback ignore is only "used" when pygls is
|
|
100
|
+
# installed, so unused-ignore warnings are off for these modules.
|
|
101
|
+
module = ["cgir.api.mcp_server", "cgir.api.lsp_server"]
|
|
99
102
|
disallow_untyped_decorators = false
|
|
103
|
+
warn_unused_ignores = false
|
|
100
104
|
|
|
101
105
|
[tool.pytest.ini_options]
|
|
102
106
|
testpaths = ["tests"]
|
|
@@ -79,7 +79,13 @@ def build(graph: RepoGraph, repo_path: Path, adapter: LanguageAdapter | None = N
|
|
|
79
79
|
body = file_adapter.function_body(func_ts)
|
|
80
80
|
if body is None:
|
|
81
81
|
continue
|
|
82
|
-
builder = _CFGBuilder(
|
|
82
|
+
builder = _CFGBuilder(
|
|
83
|
+
graph=graph,
|
|
84
|
+
owner=func,
|
|
85
|
+
source=source,
|
|
86
|
+
adapter=file_adapter,
|
|
87
|
+
global_names=file_adapter.global_declared_names(func_ts, source),
|
|
88
|
+
)
|
|
83
89
|
builder.build_block(body, predecessors=[func.id], controller=None)
|
|
84
90
|
|
|
85
91
|
|
|
@@ -89,6 +95,9 @@ class _CFGBuilder:
|
|
|
89
95
|
owner: Node
|
|
90
96
|
source: bytes
|
|
91
97
|
adapter: LanguageAdapter
|
|
98
|
+
# names declared global/nonlocal in this function: writes to them mutate
|
|
99
|
+
# outer state and are recorded as `mutates`, not local `writes`.
|
|
100
|
+
global_names: set[str] = field(default_factory=set)
|
|
92
101
|
_counter: int = field(default=0, init=False)
|
|
93
102
|
|
|
94
103
|
def build_block(
|
|
@@ -102,6 +111,11 @@ class _CFGBuilder:
|
|
|
102
111
|
|
|
103
112
|
def build_stmt(self, ts_node: TSNode, preds: list[str], controller: str | None) -> list[str]:
|
|
104
113
|
desc = self.adapter.describe_statement(ts_node, self.source)
|
|
114
|
+
if isinstance(desc, AssignDesc) and self.global_names:
|
|
115
|
+
outer = [w for w in desc.writes if w in self.global_names]
|
|
116
|
+
if outer:
|
|
117
|
+
desc.writes = [w for w in desc.writes if w not in self.global_names]
|
|
118
|
+
desc.mutates = list(desc.mutates) + outer
|
|
105
119
|
if isinstance(desc, BranchDesc):
|
|
106
120
|
return self._build_branch(ts_node, desc, preds, controller)
|
|
107
121
|
if isinstance(desc, LoopDesc):
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Coverage-grounded test linkage — measured ``covered_by``, not inferred.
|
|
2
|
+
|
|
3
|
+
Static call-edge linkage says a test *statically references* a component;
|
|
4
|
+
coverage contexts say a test *actually executed its lines*. When per-test
|
|
5
|
+
contexts exist (pytest-cov ``--cov-context=test`` or coverage.py's
|
|
6
|
+
``dynamic_context = test_function``), the pipeline maps covered lines onto
|
|
7
|
+
component spans and unions the result with static linkage: coverage adds
|
|
8
|
+
tests reached through indirection (fixtures, integration paths); static
|
|
9
|
+
keeps tests the coverage run skipped.
|
|
10
|
+
|
|
11
|
+
Sources, in preference order:
|
|
12
|
+
* ``.coverage`` — coverage.py's SQLite store, read via stdlib sqlite3. The
|
|
13
|
+
line data is a "numbits" blob (bit N set = line N covered); the format is
|
|
14
|
+
internal to coverage.py but has been stable for years — decode failures
|
|
15
|
+
degrade gracefully to no coverage linkage.
|
|
16
|
+
* ``coverage.json`` — ``coverage json --show-contexts`` output.
|
|
17
|
+
|
|
18
|
+
No new dependency; no coverage data → identical behavior to before.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import sqlite3
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
# rel_path -> normalized test id -> covered line numbers
|
|
28
|
+
CoverageData = dict[str, dict[str, set[int]]]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def numbits_to_lines(blob: bytes) -> set[int]:
|
|
32
|
+
"""Decode coverage.py's numbits: bit N set means line N covered."""
|
|
33
|
+
lines: set[int] = set()
|
|
34
|
+
for byte_index, byte in enumerate(blob):
|
|
35
|
+
for bit in range(8):
|
|
36
|
+
if byte & (1 << bit):
|
|
37
|
+
lines.add(byte_index * 8 + bit)
|
|
38
|
+
return lines
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def normalize_context(context: str) -> str | None:
|
|
42
|
+
"""A coverage context name as a CGIR component id, or None to skip.
|
|
43
|
+
|
|
44
|
+
pytest-cov: ``tests/test_m.py::TestX::test_f|run`` → ``tests.test_m.TestX.test_f``
|
|
45
|
+
dynamic_context=test_function: already dotted. The empty string is the
|
|
46
|
+
global (non-test) context.
|
|
47
|
+
"""
|
|
48
|
+
if not context:
|
|
49
|
+
return None
|
|
50
|
+
context = context.split("|", 1)[0].split(" (", 1)[0].strip()
|
|
51
|
+
if "::" in context:
|
|
52
|
+
path, _, rest = context.partition("::")
|
|
53
|
+
module = path.removesuffix(".py").replace("/", ".").replace("\\", ".")
|
|
54
|
+
return f"{module}.{rest.replace('::', '.')}"
|
|
55
|
+
return context
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def read_coverage_contexts(repo: Path) -> CoverageData | None:
|
|
59
|
+
"""Per-test line coverage from ``.coverage`` or ``coverage.json``."""
|
|
60
|
+
dot = repo / ".coverage"
|
|
61
|
+
if dot.exists():
|
|
62
|
+
data = _read_sqlite(dot, repo)
|
|
63
|
+
if data:
|
|
64
|
+
return data
|
|
65
|
+
js = repo / "coverage.json"
|
|
66
|
+
if js.exists():
|
|
67
|
+
return _read_json(js)
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _read_sqlite(path: Path, repo: Path) -> CoverageData | None:
|
|
72
|
+
try:
|
|
73
|
+
db = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
|
|
74
|
+
try:
|
|
75
|
+
files = dict(db.execute("SELECT id, path FROM file"))
|
|
76
|
+
contexts = dict(db.execute("SELECT id, context FROM context"))
|
|
77
|
+
rows = list(db.execute("SELECT file_id, context_id, numbits FROM line_bits"))
|
|
78
|
+
finally:
|
|
79
|
+
db.close()
|
|
80
|
+
except sqlite3.Error:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
out: CoverageData = {}
|
|
84
|
+
for file_id, context_id, numbits in rows:
|
|
85
|
+
test_id = normalize_context(str(contexts.get(context_id, "")))
|
|
86
|
+
if test_id is None:
|
|
87
|
+
continue
|
|
88
|
+
rel = _relativize(str(files.get(file_id, "")), repo)
|
|
89
|
+
if rel is None:
|
|
90
|
+
continue
|
|
91
|
+
lines = numbits_to_lines(numbits)
|
|
92
|
+
if lines:
|
|
93
|
+
out.setdefault(rel, {}).setdefault(test_id, set()).update(lines)
|
|
94
|
+
return out or None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _read_json(path: Path) -> CoverageData | None:
|
|
98
|
+
try:
|
|
99
|
+
payload = json.loads(path.read_text())
|
|
100
|
+
except (OSError, json.JSONDecodeError):
|
|
101
|
+
return None
|
|
102
|
+
out: CoverageData = {}
|
|
103
|
+
for file_path, file_data in (payload.get("files") or {}).items():
|
|
104
|
+
contexts = file_data.get("contexts") or {}
|
|
105
|
+
for line_str, ctx_names in contexts.items():
|
|
106
|
+
try:
|
|
107
|
+
line = int(line_str)
|
|
108
|
+
except ValueError:
|
|
109
|
+
continue
|
|
110
|
+
for name in ctx_names:
|
|
111
|
+
test_id = normalize_context(str(name))
|
|
112
|
+
if test_id is not None:
|
|
113
|
+
out.setdefault(file_path, {}).setdefault(test_id, set()).add(line)
|
|
114
|
+
return out or None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _relativize(file_path: str, repo: Path) -> str | None:
|
|
118
|
+
try:
|
|
119
|
+
return str(Path(file_path).resolve().relative_to(repo.resolve()))
|
|
120
|
+
except ValueError:
|
|
121
|
+
return file_path if file_path and not Path(file_path).is_absolute() else None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def coverage_covered_by(
|
|
125
|
+
cov: CoverageData, spans: list[tuple[str, str, int, int]]
|
|
126
|
+
) -> dict[str, set[str]]:
|
|
127
|
+
"""Map coverage data onto component spans.
|
|
128
|
+
|
|
129
|
+
``spans`` is ``(component_id, rel_path, start_line, end_line)``. A test
|
|
130
|
+
covers a component when any of its covered lines falls inside the span.
|
|
131
|
+
A test never covers itself (its own body's lines are its execution).
|
|
132
|
+
"""
|
|
133
|
+
out: dict[str, set[str]] = {}
|
|
134
|
+
for component_id, rel_path, start, end in spans:
|
|
135
|
+
per_test = cov.get(rel_path)
|
|
136
|
+
if not per_test:
|
|
137
|
+
continue
|
|
138
|
+
for test_id, lines in per_test.items():
|
|
139
|
+
if test_id == component_id:
|
|
140
|
+
continue
|
|
141
|
+
if any(start <= line <= end for line in lines):
|
|
142
|
+
out.setdefault(component_id, set()).add(test_id)
|
|
143
|
+
return out
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
__all__ = [
|
|
147
|
+
"CoverageData",
|
|
148
|
+
"coverage_covered_by",
|
|
149
|
+
"normalize_context",
|
|
150
|
+
"numbits_to_lines",
|
|
151
|
+
"read_coverage_contexts",
|
|
152
|
+
]
|