codeanalyzer-python 1.1.1__tar.gz → 1.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 (106) hide show
  1. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/.gitignore +4 -0
  2. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/PKG-INFO +143 -164
  3. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/README.md +139 -154
  4. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/__main__.py +119 -118
  5. codeanalyzer_python-1.3.0/codeanalyzer/artifacts/__init__.py +20 -0
  6. codeanalyzer_python-1.3.0/codeanalyzer/artifacts/config_keys.py +588 -0
  7. codeanalyzer_python-1.3.0/codeanalyzer/artifacts/config_use.py +597 -0
  8. codeanalyzer_python-1.3.0/codeanalyzer/artifacts/config_use_rules.yml +58 -0
  9. codeanalyzer_python-1.3.0/codeanalyzer/artifacts/dependencies.py +237 -0
  10. codeanalyzer_python-1.3.0/codeanalyzer/artifacts/discovery.py +167 -0
  11. codeanalyzer_python-1.3.0/codeanalyzer/artifacts/parsers.py +248 -0
  12. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/core.py +112 -45
  13. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/access_paths.py +26 -4
  14. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/builder.py +22 -1
  15. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/identity.py +1 -1
  16. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/pdg.py +7 -2
  17. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scc.py +1 -1
  18. codeanalyzer_python-1.3.0/codeanalyzer/entrypoints/__init__.py +3 -0
  19. codeanalyzer_python-1.3.0/codeanalyzer/entrypoints/detect.py +124 -0
  20. codeanalyzer_python-1.3.0/codeanalyzer/entrypoints/matching.py +182 -0
  21. codeanalyzer_python-1.3.0/codeanalyzer/entrypoints/pipeline.py +131 -0
  22. codeanalyzer_python-1.3.0/codeanalyzer/entrypoints/rules.py +159 -0
  23. codeanalyzer_python-1.3.0/codeanalyzer/entrypoints/rules.yml +88 -0
  24. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/bolt.py +1 -1
  25. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/project.py +277 -60
  26. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/schema.py +92 -34
  27. codeanalyzer_python-1.3.0/codeanalyzer/options/__init__.py +3 -0
  28. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/options/options.py +7 -26
  29. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/schema/__init__.py +48 -0
  30. codeanalyzer_python-1.3.0/codeanalyzer/schema/ids.py +44 -0
  31. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/schema/l1_body.py +11 -1
  32. codeanalyzer_python-1.3.0/codeanalyzer/schema/l2_callees.py +52 -0
  33. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/schema/py_schema.py +213 -103
  34. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/semantic_analysis/call_graph.py +20 -4
  35. codeanalyzer_python-1.3.0/codeanalyzer/semantic_analysis/defuse_linker.py +1499 -0
  36. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/syntactic_analysis/symbol_table_builder.py +99 -3
  37. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/pyproject.toml +18 -14
  38. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/schema.neo4j.json +182 -55
  39. codeanalyzer_python-1.1.1/codeanalyzer/config/__init__.py +0 -3
  40. codeanalyzer_python-1.1.1/codeanalyzer/config/config.py +0 -8
  41. codeanalyzer_python-1.1.1/codeanalyzer/options/__init__.py +0 -3
  42. codeanalyzer_python-1.1.1/codeanalyzer/schema/ids.py +0 -23
  43. codeanalyzer_python-1.1.1/codeanalyzer/schema/l2_callees.py +0 -36
  44. codeanalyzer_python-1.1.1/codeanalyzer/semantic_analysis/pycg/__init__.py +0 -20
  45. codeanalyzer_python-1.1.1/codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +0 -1115
  46. codeanalyzer_python-1.1.1/codeanalyzer/semantic_analysis/pycg/pycg_exceptions.py +0 -23
  47. codeanalyzer_python-1.1.1/codeanalyzer/semantic_analysis/pycg/shard_planner.py +0 -401
  48. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/LICENSE +0 -0
  49. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/NOTICE +0 -0
  50. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/__init__.py +0 -0
  51. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/__init__.py +0 -0
  52. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/alias.py +0 -0
  53. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/cfg.py +0 -0
  54. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/defuse.py +0 -0
  55. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/dominance.py +0 -0
  56. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/LICENSE +0 -0
  57. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/README.md +0 -0
  58. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/SSA/__init__.py +0 -0
  59. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/SSA/const.py +0 -0
  60. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/__init__.py +0 -0
  61. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/cfg/__init__.py +0 -0
  62. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/cfg/builder.py +0 -0
  63. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/cfg/model.py +0 -0
  64. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/core/__init__.py +0 -0
  65. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/core/func_call_visitor.py +0 -0
  66. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/core/vars_visitor.py +0 -0
  67. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel_oracle.py +0 -0
  68. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/sdg.py +0 -0
  69. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/slicing.py +0 -0
  70. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/summaries.py +0 -0
  71. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/syntactic.py +0 -0
  72. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/jedi/__init__.py +0 -0
  73. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/jedi/jedi.py +0 -0
  74. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/__init__.py +0 -0
  75. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/cypher.py +0 -0
  76. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/emit.py +0 -0
  77. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/rows.py +0 -0
  78. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/provenance.py +0 -0
  79. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/py.typed +0 -0
  80. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/schema/assign_ids.py +0 -0
  81. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/schema/call_graph_ids.py +0 -0
  82. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/semantic_analysis/__init__.py +0 -0
  83. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/syntactic_analysis/__init__.py +0 -0
  84. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/syntactic_analysis/exceptions.py +0 -0
  85. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/syntactic_analysis/import_resolver.py +0 -0
  86. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/utils/__init__.py +0 -0
  87. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/utils/logging.py +0 -0
  88. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/codeanalyzer/utils/progress_bar.py +0 -0
  89. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/docs/handoff/README.md +0 -0
  90. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/docs/handoff/schema.neo4j.json +0 -0
  91. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/schema-uml.drawio +0 -0
  92. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/flask/README.md +0 -0
  93. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/flask/examples/celery/README.md +0 -0
  94. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/flask/requirements-skip/README.md +0 -0
  95. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/flask/src/flask/sansio/README.md +0 -0
  96. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/requests/LICENSE +0 -0
  97. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/requests/NOTICE +0 -0
  98. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/requests/README.md +0 -0
  99. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/requests/docs/_themes/LICENSE +0 -0
  100. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/requests/ext/LICENSE +0 -0
  101. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/xarray/LICENSE +0 -0
  102. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/xarray/README.md +0 -0
  103. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/xarray/properties/README.md +0 -0
  104. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/LICENSE +0 -0
  105. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/README.md +0 -0
  106. {codeanalyzer_python-1.1.1 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/docs/README.md +0 -0
@@ -195,3 +195,7 @@ node_modules/
195
195
  !.claude/
196
196
  .claude/*
197
197
  !.claude/SCHEMA_DECISIONS.md
198
+
199
+ # Track fixture lock files past the repo-root uv.lock ignore
200
+ !test/fixtures/**/uv.lock
201
+ !test/fixtures/**/.env
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: codeanalyzer-python
3
- Version: 1.1.1
3
+ Version: 1.3.0
4
4
  Summary: Static analysis for Python — canonical schema v2 (symbol table, call graph, and native CFG/PDG/SDG dataflow) as analysis.json or a Neo4j property graph.
5
5
  Author-email: Rahul Krishna <i.m.ralk@gmail.com>
6
6
  License-File: LICENSE
@@ -9,25 +9,19 @@ Requires-Python: >=3.9
9
9
  Requires-Dist: astor<0.9.0,>=0.8.1
10
10
  Requires-Dist: jedi<0.20.0,>=0.18.0; python_version < '3.11'
11
11
  Requires-Dist: jedi<=0.19.2; python_version >= '3.11'
12
- Requires-Dist: msgpack<1.0.7,>=1.0.0; python_version < '3.11'
13
- Requires-Dist: msgpack<2.0.0,>=1.0.7; python_version >= '3.11'
14
12
  Requires-Dist: networkx<3.2.0,>=2.6.0; python_version < '3.11'
15
13
  Requires-Dist: networkx<4.0.0,>=3.0.0; python_version >= '3.11'
16
- Requires-Dist: numpy<1.24.0,>=1.21.0; python_version < '3.11'
17
- Requires-Dist: numpy<2.0.0,>=1.24.0; python_version >= '3.11' and python_version < '3.12'
18
- Requires-Dist: numpy<2.0.0,>=1.26.0; python_version >= '3.12'
19
14
  Requires-Dist: packaging>=25.0
20
- Requires-Dist: pandas<2.0.0,>=1.3.0; python_version < '3.11'
21
- Requires-Dist: pandas<3.0.0,>=2.0.0; python_version >= '3.11'
22
15
  Requires-Dist: parso>=0.8.5
23
- Requires-Dist: pycg>=0.0.6
24
16
  Requires-Dist: pydantic<2.0.0,>=1.8.0; python_version < '3.11'
25
17
  Requires-Dist: pydantic<3.0.0,>=2.0.0; python_version >= '3.11'
18
+ Requires-Dist: pyyaml<7.0,>=6.0
26
19
  Requires-Dist: ray<3.0.0,>=2.10.0; python_version >= '3.11'
27
20
  Requires-Dist: ray==2.0.0; python_version < '3.11'
28
21
  Requires-Dist: requests<3.0.0,>=2.20.0; python_version >= '3.11'
29
22
  Requires-Dist: rich<14.0.0,>=12.6.0; python_version < '3.11'
30
23
  Requires-Dist: rich<15.0.0,>=14.0.0; python_version >= '3.11'
24
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
31
25
  Requires-Dist: typer<1.0.0,>=0.9.0; python_version < '3.11'
32
26
  Requires-Dist: typer<2.0.0,>=0.9.0; python_version >= '3.11'
33
27
  Requires-Dist: typing-extensions<5.0.0,>=4.0.0; python_version < '3.11'
@@ -55,7 +49,7 @@ Description-Content-Type: text/markdown
55
49
  ---
56
50
 
57
51
  `canpy` is a static analyzer for Python built on [Jedi](https://jedi.readthedocs.io/),
58
- [PyCG](https://github.com/vitsalis/PyCG), and [Tree-sitter](https://tree-sitter.github.io/). It
52
+ and [Tree-sitter](https://tree-sitter.github.io/). It
59
53
  emits the **canonical CodeLLM-DevKit (CLDK) schema v2** — a single, additive Code Property Graph
60
54
  tree — either as `analysis.json` or projected into a **Neo4j property graph**. It is the Python
61
55
  backend behind [CLDK](https://github.com/codellm-devkit/python-sdk), mirroring its
@@ -97,8 +91,9 @@ needs.
97
91
  at a single `application` node with durable `can://` ids on every callable and above.
98
92
  - **Symbol table** — modules, classes, functions, methods, variables, decorators, imports, and
99
93
  docstrings, with precise byte-offset source spans; each module carries its `source` once.
100
- - **Call graph** — Jedi's lexical resolver at level 1, enriched with **PyCG**-resolved edges at
101
- level 2 (provenance-tagged, coupling-aware sharding for large apps).
94
+ - **Call graph** — Jedi's lexical resolver at level 1, enriched at level 2 by a per-callable
95
+ **defuse linker** that resolves call sites through local def-use chains and module-scope
96
+ bindings (provenance-tagged, deterministic, no global fixpoint).
102
97
  - **Dataflow graphs** — native, per-callable exceptional **CFG** plus **control-** and
103
98
  **data-dependence** edges (`cfg`/`cdg`/`ddg`) at level 3, stitched into a whole-program
104
99
  **interprocedural SDG** (synthetic parameter vertices, `param_in`/`param_out`/`summary`,
@@ -109,7 +104,7 @@ needs.
109
104
  checked in as `schema.neo4j.json` (`2.0.0`) and shipped with every release.
110
105
  - **Incremental cache** — per-file results are cached under `.codeanalyzer`; `--lazy` (default)
111
106
  reuses them, `--eager` forces a clean rebuild. `--ray` distributes the work across cores.
112
- - **Compact output** — canonical `analysis.json`, or binary `analysis.msgpack` for smaller artifacts.
107
+ - **Compact output** — one canonical `analysis.json` per run.
113
108
 
114
109
  ## Installation
115
110
 
@@ -182,8 +177,7 @@ canpy --input /path/to/python/project
182
177
  ```
183
178
 
184
179
  With no `--output`, the analysis is printed to stdout as compact JSON; with `--output <dir>` it is
185
- written to `analysis.json` (or `graph.cypher` for `--emit neo4j`, or `analysis.msgpack` with
186
- `--format msgpack`) in that directory.
180
+ written to `analysis.json` (or `graph.cypher` for `--emit neo4j`) in that directory.
187
181
 
188
182
  ### Options
189
183
 
@@ -194,7 +188,7 @@ $ canpy --help
194
188
 
195
189
  Usage: canpy [OPTIONS] COMMAND [ARGS]...
196
190
 
197
- Static Analysis on Python source code using Jedi, PyCG and Tree sitter.
191
+ Static Analysis on Python source code using Jedi and Tree sitter.
198
192
 
199
193
  ╭─ Options ────────────────────────────────────────────────────────────────────╮
200
194
  │ --version Show the canpy │
@@ -207,10 +201,6 @@ $ canpy --help
207
201
  │ --emit schema). │
208
202
  │ --output -o <path> Output directory │
209
203
  │ for artifacts. │
210
- │ --format -f <json|msgpack> Output format │
211
- │ for --emit json: │
212
- │ json or msgpack. │
213
- │ [default: json] │
214
204
  │ --emit <json|neo4j|sche Output target: │
215
205
  │ ma> json │
216
206
  │ (analysis.json, │
@@ -258,7 +248,8 @@ $ canpy --help
258
248
  │ --analysis-level -a <int range> Analysis depth: │
259
249
  │ [1<=x<=4] 1=symbol │
260
250
  │ table+Jedi call │
261
- │ graph, 2=+PyCG
251
+ │ graph,
252
+ │ 2=+defuse-linker │
262
253
  │ call graph, │
263
254
  │ 3=+native │
264
255
  │ intraprocedural │
@@ -270,7 +261,7 @@ $ canpy --help
270
261
  │ edges, │
271
262
  │ alias-aware │
272
263
  │ DDG). │
273
- │ [default: 1]
264
+ │ [default: (1)]
274
265
  │ --graphs <str> Level 3+ only: │
275
266
  │ comma-separated │
276
267
  │ program-graph │
@@ -282,8 +273,12 @@ $ canpy --help
282
273
  │ PDG's data edges │
283
274
  │ only; `sdg` │
284
275
  │ requires -a 4. │
276
+ │ Incompatible │
277
+ │ with --emit │
278
+ │ neo4j (always │
279
+ │ full-depth). │
285
280
  │ [default: │
286
- │ cfg,dfg,pdg]
281
+ (cfg,dfg,pdg)]
287
282
  │ --graph-field-de… <int range> Level 3 only: │
288
283
  │ [x>=1] k-limit on │
289
284
  │ access-path │
@@ -321,6 +316,16 @@ $ canpy --help
321
316
  │ environment │
322
317
  │ instead. │
323
318
  │ [default: venv] │
319
+ │ --resolve-instal… Additionally │
320
+ │ bind imports via │
321
+ │ the project │
322
+ │ venv's installed │
323
+ │ metadata │
324
+ │ (*.dist-info); │
325
+ │ output becomes │
326
+ │ machine-depende… │
327
+ │ (prov: │
328
+ │ installed-metad… │
324
329
  │ --file-name <path> Analyze only the │
325
330
  │ specified file │
326
331
  │ (relative to │
@@ -344,134 +349,38 @@ $ canpy --help
344
349
  │ verbosity: -v, │
345
350
  │ -vv, -vvv │
346
351
  │ [default: 0] │
347
- │ --pycg-shard --no-pycg-shard Shard PyCG
348
- call-graph
349
- analysis by
350
- Python package
351
- (level 2 only).
352
- When the project
353
- exceeds the
354
- 500-file
355
- ceiling, PyCG is
356
- run
357
- independently
358
- per top-level
359
- package with
360
- cross-package
361
- imports treated
362
- as ghost nodes.
363
- Without this
364
- │ flag, projects │
365
- │ over the ceiling │
366
- │ fall back to │
367
- │ Jedi-only edges. │
352
+ │ --entrypoint-rul… <path> Extra entrypoint
353
+ rules file
354
+ (YAML).
355
+ Repeatable;
356
+ merges with the
357
+ shipped rules. A
358
+ malformed file
359
+ is an error.
360
+ --artifact-text --no-artifact-… Capture verbatim
361
+ `source` text on
362
+ discovered
363
+ artifacts.
364
+ --no-artifact-t…
365
+ empties `source`
366
+ everywhere
367
+ (inventory
368
+ unchanged).
368
369
  │ [default: │
369
- no-pycg-shard] │
370
- │ --pycg-shard-cei… <int range> Maximum files
371
- │ [x>=1] per shard when
372
- --pycg-shard is
373
- active (default
374
- 100). Shards
375
- exceeding this
376
- limit are
377
- skipped; their
378
- call edges are
379
- omitted from the
380
- call graph (Jedi
381
- edges for those
382
- packages are
383
- │ still included). │
384
- │ Lower values are │
385
- │ safer for │
386
- │ packages with │
387
- │ deep class │
388
- │ hierarchies or │
389
- │ heavy import │
390
- │ graphs. │
391
- │ [default: 100] │
392
- │ --pycg-shard-tim… <int range> Per-shard │
393
- │ [x>=0] wall-clock │
394
- │ timeout in │
395
- │ seconds when │
396
- │ --pycg-shard is │
397
- │ active (default │
398
- │ 120). A shard │
399
- │ that exceeds │
400
- │ this limit is │
401
- │ skipped │
402
- │ gracefully. │
403
- │ PyCG's fixpoint │
404
- │ is bimodal: it │
405
- │ either converges │
406
- │ quickly or │
407
- │ diverges │
408
- │ indefinitely, so │
409
- │ the timeout acts │
410
- │ as a final │
411
- │ safety net after │
412
- │ the file-count │
413
- │ ceiling. Set to │
414
- │ 0 to disable. │
415
- │ POSIX only │
416
- │ (macOS / Linux); │
417
- │ ignored on │
418
- │ Windows. │
419
- │ [default: 120] │
420
- │ --pycg-shard-str… <jedi|package> How --pycg-shard │
421
- │ groups files │
422
- │ (level 2 only). │
423
- │ 'jedi' (default) │
424
- │ partitions the │
425
- │ Jedi │
426
- │ module-dependen… │
427
- │ graph (SCC + │
428
- │ Louvain) so │
429
- │ tightly-coupled │
430
- │ modules │
431
- │ co-compute and │
432
- │ few call edges │
433
- │ are severed │
434
- │ between shards; │
435
- │ import cycles │
436
- │ are never split. │
437
- │ 'package' uses │
438
- │ the legacy │
439
- │ one-shard-per-p… │
440
- │ grouping. │
441
- │ [default: jedi] │
442
- │ --pycg-max-iter <int range> Cap on PyCG's │
443
- │ [x>=-1] fixpoint passes │
444
- │ per │
445
- │ shard/project │
446
- │ (level 2; │
447
- │ default 50). │
448
- │ PyCG iterates │
449
- │ until its │
450
- │ points-to state │
451
- │ stops changing, │
452
- │ but its │
453
- │ access-path │
454
- │ domain has no │
455
- │ convergence │
456
- │ bound, so heavy │
457
- │ metaclass/mixin │
458
- │ code (e.g. an │
459
- │ ORM) can loop │
460
- │ with each pass │
461
- │ costing seconds. │
462
- │ The cap returns │
463
- │ a │
464
- │ sound-but-incom… │
465
- │ call graph │
466
- │ instead of │
467
- │ looping until │
468
- │ the timeout │
469
- │ kills it. Set to │
470
- │ -1 for PyCG's │
471
- │ unbounded │
472
- │ run-to-converge… │
473
- │ behaviour. │
474
- │ [default: 50] │
370
+ artifact-text] │
371
+ │ --artifact-text-… <int range> Per-file byte
372
+ │ [x>=1] cap on captured
373
+ artifact
374
+ `source`; a
375
+ decodable file
376
+ over the cap is
377
+ truncated
378
+ (text_truncated…
379
+ sha256/size_byt…
380
+ always reflect
381
+ the full file.
382
+ [default:
383
+ 262144]
475
384
  │ --help Show this │
476
385
  │ message and │
477
386
  │ exit. │
@@ -493,13 +402,14 @@ $ canpy --help
493
402
  canpy --input ./my-python-project --output ./out --format msgpack # → ./out/analysis.msgpack
494
403
  ```
495
404
 
496
- 3. **Enrich the call graph with PyCG (level 2):**
405
+ 3. **Enrich the call graph with the defuse linker (level 2):**
497
406
  ```sh
498
407
  canpy --input ./my-python-project -a 2
499
408
  ```
500
- Level 1 edges come from Jedi's lexical resolution. `-a 2` runs **PyCG** and merges its
501
- flow-sensitive edges in (RPC / third-party / dynamically-dispatched targets), backfilling
502
- callees Jedi could not resolve. Every edge is provenance-tagged (e.g. `jedi`, `pycg`).
409
+ Level 1 edges come from Jedi's lexical resolution. `-a 2` runs the **defuse linker**
410
+ per-callable resolution over lexical scopes, import bindings, class hierarchies, and a
411
+ bounded type-propagation round and merges its edges with Jedi's, backfilling the
412
+ callees Jedi could not resolve. Every edge is provenance-tagged (`jedi`, `defuse`).
503
413
 
504
414
  4. **Emit a Neo4j snapshot, or push to a live database:**
505
415
  ```sh
@@ -528,7 +438,7 @@ $ canpy --help
528
438
  canpy --input ./my-python-project -a 3 --graph-field-depth 2 # tighter access-path k-limit
529
439
  ```
530
440
  Levels 3 and 4 also enrich the Neo4j projection (`--emit neo4j`) with the CPG overlay
531
- (`:PyCFGNode` nodes wired by `PY_CFG_NEXT`/`PY_CDG`/`PY_DDG`, plus the level-4
441
+ (`:PyBodyNode` nodes wired by `PY_CFG_NEXT`/`PY_CDG`/`PY_DDG`, plus the level-4
532
442
  `PY_PARAM_IN`/`PY_PARAM_OUT`/`PY_SUMMARY` edges — the cross-language dataflow vocabulary,
533
443
  PY_-namespaced like every other row family so multi-language databases never mingle
534
444
  analyzers' edges).
@@ -541,7 +451,7 @@ levels are cumulative and additive — `analysis.json(-a 1) ⊆ … ⊆ analysis
541
451
  | Level | Flag | What it adds | Where it lands |
542
452
  | --- | --- | --- | --- |
543
453
  | **1** | `-a 1` (default) | Symbol table, Jedi call graph, and `call` nodes in each callable's `body` | `body` calls (`callee: null`) |
544
- | **2** | `-a 2` | PyCG call-graph enrichment; each call's `callee` backfilled to a `can://` id | `call_graph`, `body` callees |
454
+ | **2** | `-a 2` | Defuse-linker call-graph enrichment; each call's `callee` backfilled to a `can://` id | `call_graph`, `body` callees |
545
455
  | **3** | `-a 3` | Native **intraprocedural** CFG/CDG/DDG (syntactic, name-equality, `prov: ["ssa"]`) | `cfg`, `cdg`, `ddg`, `@entry`/`@exit` on each callable |
546
456
  | **4** | `-a 4` | **Interprocedural** SDG: synthetic param vertices, alias-aware DDG (`prov: ["points-to"]`) | `param_in`, `param_out`, `summary`, semantic `ddg` |
547
457
 
@@ -570,7 +480,7 @@ symbol-table signature by construction
570
480
  external dependency to install; the analyzer falls back to the built-in `TypeBasedAliasOracle`
571
481
  (Jedi-inferred types; unknown types conservatively alias) only when Scalpel can't resolve a
572
482
  construct or a per-callable build fails, keeping the `may_alias` interface total. Call dispatch
573
- comes from the merged Jedi(+PyCG) call graph, treated as a frozen oracle.
483
+ comes from the merged Jedi + defuse-linker call graph, treated as a frozen oracle.
574
484
  - **Summaries:** relational formal-in → formal-out flows composed bottom-up over the Tarjan SCC
575
485
  condensation of the call graph, a monotone fixpoint within SCCs; globals ride as extra formals,
576
486
  closure captures bind at definition sites.
@@ -609,7 +519,7 @@ just populate more of the same tree:
609
519
  }
610
520
  },
611
521
  "call_graph": [ { "src": "can://…/main(a)", "dst": "can://…/helper(x)",
612
- "weight": 1, "prov": ["jedi", "pycg"] } ],
522
+ "weight": 1, "prov": ["defuse", "jedi"] } ],
613
523
  "external_symbols": { // imported/builtin call targets, keyed by id
614
524
  "can://python/<app>/@external/os/getcwd":
615
525
  { "id": "can://python/<app>/@external/os/getcwd", "kind": "external",
@@ -642,14 +552,29 @@ A **callable** (function or method) carries its own CPG, keyed by node id:
642
552
  }
643
553
  ```
644
554
 
555
+ The application envelope also contains three substrate sections:
556
+
557
+ - **`artifacts`** — discovered non-code files (manifests, configs, Docker files, CI workflows,
558
+ packaging files, scripts, docs, and legal files) with extraction status (`none`, `partial`, or
559
+ `full`; default `none`), keyed by relative path; each artifact carries the
560
+ `can://artifact/<app>/<path>` id namespace. Config files carry extracted `config_keys`
561
+ (keys, values, namespaces, and references) and `DEFINES_CONFIG` Neo4j edges.
562
+ - **`dependencies`** — declared packages with kind (`runtime`/`dev`/`optional`/`build`), spec,
563
+ locked version, and provenance (`prov`): where each binding came from (manifest file, lock file,
564
+ installed metadata).
565
+ - **`unresolved_imports`** — modules imported but not resolvable in the declared dependency set,
566
+ one entry per module.
567
+
645
568
  Notable properties:
646
569
 
647
570
  - **Durable `can://` ids** identify every node at callable granularity and above
648
571
  (`can://python/<app>/<file>/<callable-sig>`); nodes below a callable use ordinal ids
649
572
  (`@entry`, `@exit`, `line:col`, `@formal_in:N`, `line:col/actual_in:N`).
650
573
  - **`source` lives once per module**; every node's text is the `module.source[span.bytes]` slice.
651
- - **Cross-function edges** — `call_graph`, `param_in`, `param_out` — live at **application** scope;
574
+ - **Cross-function edges** — `call_graph`, `param_in`, `param_out`, `config_uses` — live at **application** scope;
652
575
  the intraprocedural `cfg`/`cdg`/`ddg` and the `summary` edges live **on the callable**.
576
+ `config_uses` resolve from call-site key arguments to `ConfigKey` nodes; unresolved reads
577
+ go to `config_reads_unresolved`.
653
578
  - **No dangling endpoints** — every `call_graph` `src`/`dst` joins the id space: declared
654
579
  callables by their tree id, imported/builtin targets by a `…/@external/<module>/<name>` id
655
580
  homed in `application.external_symbols`.
@@ -670,12 +595,16 @@ binary format).
670
595
 
671
596
  ### Neo4j graph
672
597
 
673
- `--emit neo4j` projects the same schema v2.0.0 analysis into a labeled property graph. Every node
674
- label is `Py`-prefixed and every relationship type is `PY_`-prefixed (e.g. `:PyClass`, `PY_CALLS`)
675
- so multiple language analyzers can share one database without label or relationship-type collisions.
598
+ `--emit neo4j` projects the same schema v2.0.0 analysis into a labeled property graph. Every
599
+ Python-specific node label is `Py`-prefixed and every Python-specific relationship type is
600
+ `PY_`-prefixed (e.g. `:PyClass`, `PY_CALLS`) so multiple language analyzers can share one database
601
+ without label or relationship-type collisions. The one deliberate exception is the language-neutral
602
+ `Artifact`/`Package` subgraph (non-code files and third-party dependencies) — those nodes carry no
603
+ `Py` prefix, since they are meant as cross-language merge targets: a sibling-language analyzer over
604
+ the same repo should land on the same `Artifact`/`Package` nodes, not a per-language duplicate.
676
605
  Declarations are keyed by their **`can://` id** under a shared `:PySymbol` label; calls, imports,
677
606
  inheritance, decorators, and call sites are relationships. At `-a 3`/`-a 4` the projection gains the
678
- **CPG overlay** — `:PyCFGNode` nodes (statements, and at level 4 the parameter vertices) wired by
607
+ **CPG overlay** — `:PyBodyNode` nodes (statements, and at level 4 the parameter vertices) wired by
679
608
  `PY_CFG_NEXT`/`PY_CDG`/`PY_DDG`, plus the level-4 `PY_PARAM_IN`/`PY_PARAM_OUT`/`PY_SUMMARY` edges:
680
609
 
681
610
  - **Without `--neo4j-uri`** — writes a self-contained `graph.cypher` (constraints + indexes, a scoped
@@ -733,6 +662,56 @@ runtime (Docker or Podman) and is enabled with an environment variable:
733
662
  RUN_CONTAINER_TESTS=1 uv run pytest test/test_neo4j_bolt.py -s
734
663
  ```
735
664
 
665
+ ## Graph query cookbook
666
+
667
+ Example Cypher over the projected graph (`--emit neo4j`, then load `graph.cypher` or push via Bolt).
668
+
669
+ ```cypher
670
+ // who calls this function? (direct callers)
671
+ MATCH (c:PyCallable)-[:PY_CALLS]->(t:PyCallable {name: "process_payment"})
672
+ RETURN c.id
673
+
674
+ // every callable that reaches a given library, via the external ghosts
675
+ MATCH (c:PyCallable)-[:PY_CALLS]->(e:PyExternal)
676
+ WHERE e.id CONTAINS "/@external/requests/"
677
+ RETURN DISTINCT c.id
678
+
679
+ // entrypoints and the frameworks that invoke them
680
+ MATCH (m:PyCallable {is_entrypoint: true})
681
+ RETURN m.id, m.entrypoint_frameworks
682
+
683
+ // data dependences into one statement (level 3+)
684
+ MATCH (s:PyBodyNode {id: $stmt})<-[d:PY_DDG]-(src:PyBodyNode)
685
+ RETURN src.id, d.var, d.prov
686
+
687
+ // interprocedural flow through a parameter (level 4)
688
+ MATCH (a:PyBodyNode)-[:PY_PARAM_IN]->(f:PyBodyNode)
689
+ WHERE f.id STARTS WITH "can://python/myapp/src/api.py"
690
+ RETURN a.id, f.id
691
+ ```
692
+
693
+ Artifact and dependency queries (1.3.0+):
694
+
695
+ ```cypher
696
+ // all container/orchestration configs in the app
697
+ MATCH (a:PyApplication)-[:HAS_ARTIFACT]->(f:Artifact)
698
+ WHERE any(r IN f.roles WHERE r IN ["service-topology", "container-image"])
699
+ RETURN f.id, f.format
700
+
701
+ // every callable that reaches code from a declared package
702
+ MATCH (c:PyCallable)-[:PY_CALLS]->(:PyExternal)<-[:PY_PROVIDES]-(p:Package {id: "pkg:pypi/requests"})
703
+ RETURN c.id
704
+
705
+ // undeclared imports (dependency hygiene)
706
+ MATCH (a:PyApplication)-[u:PY_UNRESOLVED_IMPORT]->(e:PyExternal)
707
+ WHERE NOT (e)<-[:PY_PROVIDES]-(:Package)
708
+ RETURN e.id, u.prov
709
+
710
+ // which lock file pins this package, and to what
711
+ MATCH (f:Artifact)-[l:LOCKS]->(p:Package {id: "pkg:pypi/numpy"})
712
+ RETURN f.id, l.version
713
+ ```
714
+
736
715
  ## License
737
716
 
738
717
  Apache 2.0 — see [LICENSE](./LICENSE).