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