codeanalyzer-python 1.2.0__tar.gz → 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/.gitignore +4 -0
  2. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/PKG-INFO +117 -7
  3. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/README.md +115 -6
  4. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/__main__.py +24 -1
  5. codeanalyzer_python-1.4.0/codeanalyzer/artifacts/__init__.py +20 -0
  6. codeanalyzer_python-1.4.0/codeanalyzer/artifacts/config_keys.py +588 -0
  7. codeanalyzer_python-1.4.0/codeanalyzer/artifacts/config_use.py +597 -0
  8. codeanalyzer_python-1.4.0/codeanalyzer/artifacts/config_use_rules.yml +58 -0
  9. codeanalyzer_python-1.4.0/codeanalyzer/artifacts/dependencies.py +237 -0
  10. codeanalyzer_python-1.4.0/codeanalyzer/artifacts/discovery.py +141 -0
  11. codeanalyzer_python-1.4.0/codeanalyzer/artifacts/parsers.py +248 -0
  12. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/core.py +90 -0
  13. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/builder.py +22 -3
  14. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/sdg.py +17 -2
  15. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/summaries.py +27 -4
  16. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/neo4j/bolt.py +35 -6
  17. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/neo4j/emit.py +3 -2
  18. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/neo4j/project.py +191 -0
  19. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/neo4j/schema.py +74 -0
  20. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/options/options.py +4 -0
  21. codeanalyzer_python-1.4.0/codeanalyzer/schema/ids.py +44 -0
  22. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/schema/py_schema.py +117 -0
  23. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/syntactic_analysis/symbol_table_builder.py +11 -0
  24. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/pyproject.toml +4 -1
  25. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/schema.neo4j.json +148 -2
  26. codeanalyzer_python-1.2.0/codeanalyzer/schema/ids.py +0 -23
  27. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/LICENSE +0 -0
  28. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/NOTICE +0 -0
  29. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/__init__.py +0 -0
  30. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/__init__.py +0 -0
  31. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/access_paths.py +0 -0
  32. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/alias.py +0 -0
  33. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/cfg.py +0 -0
  34. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/defuse.py +0 -0
  35. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/dominance.py +0 -0
  36. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/identity.py +0 -0
  37. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/pdg.py +0 -0
  38. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/LICENSE +0 -0
  39. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/README.md +0 -0
  40. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/SSA/__init__.py +0 -0
  41. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/SSA/const.py +0 -0
  42. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/__init__.py +0 -0
  43. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/cfg/__init__.py +0 -0
  44. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/cfg/builder.py +0 -0
  45. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/cfg/model.py +0 -0
  46. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/core/__init__.py +0 -0
  47. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/core/func_call_visitor.py +0 -0
  48. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel/core/vars_visitor.py +0 -0
  49. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scalpel_oracle.py +0 -0
  50. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/scc.py +0 -0
  51. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/slicing.py +0 -0
  52. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/dataflow/syntactic.py +0 -0
  53. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/entrypoints/__init__.py +0 -0
  54. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/entrypoints/detect.py +0 -0
  55. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/entrypoints/matching.py +0 -0
  56. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/entrypoints/pipeline.py +0 -0
  57. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/entrypoints/rules.py +0 -0
  58. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/entrypoints/rules.yml +0 -0
  59. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/jedi/__init__.py +0 -0
  60. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/jedi/jedi.py +0 -0
  61. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/neo4j/__init__.py +0 -0
  62. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/neo4j/cypher.py +0 -0
  63. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/neo4j/rows.py +0 -0
  64. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/options/__init__.py +0 -0
  65. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/provenance.py +0 -0
  66. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/py.typed +0 -0
  67. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/schema/__init__.py +0 -0
  68. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/schema/assign_ids.py +0 -0
  69. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/schema/call_graph_ids.py +0 -0
  70. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/schema/l1_body.py +0 -0
  71. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/schema/l2_callees.py +0 -0
  72. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/semantic_analysis/__init__.py +0 -0
  73. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/semantic_analysis/call_graph.py +0 -0
  74. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/semantic_analysis/defuse_linker.py +0 -0
  75. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/syntactic_analysis/__init__.py +0 -0
  76. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/syntactic_analysis/exceptions.py +0 -0
  77. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/syntactic_analysis/import_resolver.py +0 -0
  78. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/utils/__init__.py +0 -0
  79. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/utils/logging.py +0 -0
  80. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/codeanalyzer/utils/progress_bar.py +0 -0
  81. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/docs/handoff/README.md +0 -0
  82. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/docs/handoff/schema.neo4j.json +0 -0
  83. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/schema-uml.drawio +0 -0
  84. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/flask/README.md +0 -0
  85. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/flask/examples/celery/README.md +0 -0
  86. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/flask/requirements-skip/README.md +0 -0
  87. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/flask/src/flask/sansio/README.md +0 -0
  88. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/requests/LICENSE +0 -0
  89. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/requests/NOTICE +0 -0
  90. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/requests/README.md +0 -0
  91. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/requests/docs/_themes/LICENSE +0 -0
  92. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/requests/ext/LICENSE +0 -0
  93. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/xarray/LICENSE +0 -0
  94. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/xarray/README.md +0 -0
  95. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/xarray/properties/README.md +0 -0
  96. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/LICENSE +0 -0
  97. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/README.md +0 -0
  98. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.4.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
1
  Metadata-Version: 2.5
2
2
  Name: codeanalyzer-python
3
- Version: 1.2.0
3
+ Version: 1.4.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
@@ -21,6 +21,7 @@ Requires-Dist: ray==2.0.0; python_version < '3.11'
21
21
  Requires-Dist: requests<3.0.0,>=2.20.0; python_version >= '3.11'
22
22
  Requires-Dist: rich<14.0.0,>=12.6.0; python_version < '3.11'
23
23
  Requires-Dist: rich<15.0.0,>=14.0.0; python_version >= '3.11'
24
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
24
25
  Requires-Dist: typer<1.0.0,>=0.9.0; python_version < '3.11'
25
26
  Requires-Dist: typer<2.0.0,>=0.9.0; python_version >= '3.11'
26
27
  Requires-Dist: typing-extensions<5.0.0,>=4.0.0; python_version < '3.11'
@@ -98,7 +99,9 @@ needs.
98
99
  **interprocedural SDG** (synthetic parameter vertices, `param_in`/`param_out`/`summary`,
99
100
  alias-aware DDG) at level 4 — all built in-process from the stdlib `ast`.
100
101
  - **Neo4j output** — project the analysis into a labeled property graph: a self-contained
101
- `graph.cypher` snapshot, or an **incremental** push to a live database over Bolt.
102
+ `graph.cypher` snapshot, or an **incremental** push to a live database over Bolt. A push is
103
+ **additive by default** and never deletes: `--eager` is what permits it to remove declarations
104
+ and edges the source no longer has.
102
105
  - **Versioned schema** — a machine-readable, version-stamped Neo4j schema contract (`--emit schema`),
103
106
  checked in as `schema.neo4j.json` (`2.0.0`) and shipped with every release.
104
107
  - **Incremental cache** — per-file results are cached under `.codeanalyzer`; `--lazy` (default)
@@ -299,7 +302,19 @@ $ canpy --help
299
302
  │ --eager --lazy Enable eager or │
300
303
  │ lazy analysis. │
301
304
  │ Defaults to │
302
- │ lazy.
305
+ │ lazy. Also gates
306
+ │ every │
307
+ │ destructive step │
308
+ │ of a '--emit │
309
+ │ neo4j' Bolt │
310
+ │ push: a lazy │
311
+ │ push only adds │
312
+ │ and updates, an │
313
+ │ eager one also │
314
+ │ removes │
315
+ │ declarations and │
316
+ │ edges the source │
317
+ │ no longer has. │
303
318
  │ [default: lazy] │
304
319
  │ --skip-tests --include-tests Skip test files │
305
320
  │ in analysis. │
@@ -315,6 +330,16 @@ $ canpy --help
315
330
  │ environment │
316
331
  │ instead. │
317
332
  │ [default: venv] │
333
+ │ --resolve-instal… Additionally │
334
+ │ bind imports via │
335
+ │ the project │
336
+ │ venv's installed │
337
+ │ metadata │
338
+ │ (*.dist-info); │
339
+ │ output becomes │
340
+ │ machine-depende… │
341
+ │ (prov: │
342
+ │ installed-metad… │
318
343
  │ --file-name <path> Analyze only the │
319
344
  │ specified file │
320
345
  │ (relative to │
@@ -346,6 +371,22 @@ $ canpy --help
346
371
  │ shipped rules. A │
347
372
  │ malformed file │
348
373
  │ is an error. │
374
+ │ --artifact-text --no-artifact-… Capture verbatim │
375
+ │ `source` text on │
376
+ │ discovered │
377
+ │ artifacts. │
378
+ │ `source` is the │
379
+ │ whole file; │
380
+ │ --no-artifact-t… │
381
+ │ empties it │
382
+ │ everywhere │
383
+ │ (inventory │
384
+ │ unchanged). │
385
+ │ sha256/size_byt… │
386
+ │ always reflect │
387
+ │ the full file. │
388
+ │ [default: │
389
+ │ artifact-text] │
349
390
  │ --help Show this │
350
391
  │ message and │
351
392
  │ exit. │
@@ -517,14 +558,29 @@ A **callable** (function or method) carries its own CPG, keyed by node id:
517
558
  }
518
559
  ```
519
560
 
561
+ The application envelope also contains three substrate sections:
562
+
563
+ - **`artifacts`** — discovered non-code files (manifests, configs, Docker files, CI workflows,
564
+ packaging files, scripts, docs, and legal files) with extraction status (`none`, `partial`, or
565
+ `full`; default `none`), keyed by relative path; each artifact carries the
566
+ `can://artifact/<app>/<path>` id namespace. Config files carry extracted `config_keys`
567
+ (keys, values, namespaces, and references) and `DEFINES_CONFIG` Neo4j edges.
568
+ - **`dependencies`** — declared packages with kind (`runtime`/`dev`/`optional`/`build`), spec,
569
+ locked version, and provenance (`prov`): where each binding came from (manifest file, lock file,
570
+ installed metadata).
571
+ - **`unresolved_imports`** — modules imported but not resolvable in the declared dependency set,
572
+ one entry per module.
573
+
520
574
  Notable properties:
521
575
 
522
576
  - **Durable `can://` ids** identify every node at callable granularity and above
523
577
  (`can://python/<app>/<file>/<callable-sig>`); nodes below a callable use ordinal ids
524
578
  (`@entry`, `@exit`, `line:col`, `@formal_in:N`, `line:col/actual_in:N`).
525
579
  - **`source` lives once per module**; every node's text is the `module.source[span.bytes]` slice.
526
- - **Cross-function edges** — `call_graph`, `param_in`, `param_out` — live at **application** scope;
580
+ - **Cross-function edges** — `call_graph`, `param_in`, `param_out`, `config_uses` — live at **application** scope;
527
581
  the intraprocedural `cfg`/`cdg`/`ddg` and the `summary` edges live **on the callable**.
582
+ `config_uses` resolve from call-site key arguments to `ConfigKey` nodes; unresolved reads
583
+ go to `config_reads_unresolved`.
528
584
  - **No dangling endpoints** — every `call_graph` `src`/`dst` joins the id space: declared
529
585
  callables by their tree id, imported/builtin targets by a `…/@external/<module>/<name>` id
530
586
  homed in `application.external_symbols`.
@@ -545,9 +601,13 @@ binary format).
545
601
 
546
602
  ### Neo4j graph
547
603
 
548
- `--emit neo4j` projects the same schema v2.0.0 analysis into a labeled property graph. Every node
549
- label is `Py`-prefixed and every relationship type is `PY_`-prefixed (e.g. `:PyClass`, `PY_CALLS`)
550
- so multiple language analyzers can share one database without label or relationship-type collisions.
604
+ `--emit neo4j` projects the same schema v2.0.0 analysis into a labeled property graph. Every
605
+ Python-specific node label is `Py`-prefixed and every Python-specific relationship type is
606
+ `PY_`-prefixed (e.g. `:PyClass`, `PY_CALLS`) so multiple language analyzers can share one database
607
+ without label or relationship-type collisions. The one deliberate exception is the language-neutral
608
+ `Artifact`/`Package` subgraph (non-code files and third-party dependencies) — those nodes carry no
609
+ `Py` prefix, since they are meant as cross-language merge targets: a sibling-language analyzer over
610
+ the same repo should land on the same `Artifact`/`Package` nodes, not a per-language duplicate.
551
611
  Declarations are keyed by their **`can://` id** under a shared `:PySymbol` label; calls, imports,
552
612
  inheritance, decorators, and call sites are relationships. At `-a 3`/`-a 4` the projection gains the
553
613
  **CPG overlay** — `:PyBodyNode` nodes (statements, and at level 4 the parameter vertices) wired by
@@ -608,6 +668,56 @@ runtime (Docker or Podman) and is enabled with an environment variable:
608
668
  RUN_CONTAINER_TESTS=1 uv run pytest test/test_neo4j_bolt.py -s
609
669
  ```
610
670
 
671
+ ## Graph query cookbook
672
+
673
+ Example Cypher over the projected graph (`--emit neo4j`, then load `graph.cypher` or push via Bolt).
674
+
675
+ ```cypher
676
+ // who calls this function? (direct callers)
677
+ MATCH (c:PyCallable)-[:PY_CALLS]->(t:PyCallable {name: "process_payment"})
678
+ RETURN c.id
679
+
680
+ // every callable that reaches a given library, via the external ghosts
681
+ MATCH (c:PyCallable)-[:PY_CALLS]->(e:PyExternal)
682
+ WHERE e.id CONTAINS "/@external/requests/"
683
+ RETURN DISTINCT c.id
684
+
685
+ // entrypoints and the frameworks that invoke them
686
+ MATCH (m:PyCallable {is_entrypoint: true})
687
+ RETURN m.id, m.entrypoint_frameworks
688
+
689
+ // data dependences into one statement (level 3+)
690
+ MATCH (s:PyBodyNode {id: $stmt})<-[d:PY_DDG]-(src:PyBodyNode)
691
+ RETURN src.id, d.var, d.prov
692
+
693
+ // interprocedural flow through a parameter (level 4)
694
+ MATCH (a:PyBodyNode)-[:PY_PARAM_IN]->(f:PyBodyNode)
695
+ WHERE f.id STARTS WITH "can://python/myapp/src/api.py"
696
+ RETURN a.id, f.id
697
+ ```
698
+
699
+ Artifact and dependency queries (1.3.0+):
700
+
701
+ ```cypher
702
+ // all container/orchestration configs in the app
703
+ MATCH (a:PyApplication)-[:HAS_ARTIFACT]->(f:Artifact)
704
+ WHERE any(r IN f.roles WHERE r IN ["service-topology", "container-image"])
705
+ RETURN f.id, f.format
706
+
707
+ // every callable that reaches code from a declared package
708
+ MATCH (c:PyCallable)-[:PY_CALLS]->(:PyExternal)<-[:PY_PROVIDES]-(p:Package {id: "pkg:pypi/requests"})
709
+ RETURN c.id
710
+
711
+ // undeclared imports (dependency hygiene)
712
+ MATCH (a:PyApplication)-[u:PY_UNRESOLVED_IMPORT]->(e:PyExternal)
713
+ WHERE NOT (e)<-[:PY_PROVIDES]-(:Package)
714
+ RETURN e.id, u.prov
715
+
716
+ // which lock file pins this package, and to what
717
+ MATCH (f:Artifact)-[l:LOCKS]->(p:Package {id: "pkg:pypi/numpy"})
718
+ RETURN f.id, l.version
719
+ ```
720
+
611
721
  ## License
612
722
 
613
723
  Apache 2.0 — see [LICENSE](./LICENSE).
@@ -66,7 +66,9 @@ needs.
66
66
  **interprocedural SDG** (synthetic parameter vertices, `param_in`/`param_out`/`summary`,
67
67
  alias-aware DDG) at level 4 — all built in-process from the stdlib `ast`.
68
68
  - **Neo4j output** — project the analysis into a labeled property graph: a self-contained
69
- `graph.cypher` snapshot, or an **incremental** push to a live database over Bolt.
69
+ `graph.cypher` snapshot, or an **incremental** push to a live database over Bolt. A push is
70
+ **additive by default** and never deletes: `--eager` is what permits it to remove declarations
71
+ and edges the source no longer has.
70
72
  - **Versioned schema** — a machine-readable, version-stamped Neo4j schema contract (`--emit schema`),
71
73
  checked in as `schema.neo4j.json` (`2.0.0`) and shipped with every release.
72
74
  - **Incremental cache** — per-file results are cached under `.codeanalyzer`; `--lazy` (default)
@@ -267,7 +269,19 @@ $ canpy --help
267
269
  │ --eager --lazy Enable eager or │
268
270
  │ lazy analysis. │
269
271
  │ Defaults to │
270
- │ lazy.
272
+ │ lazy. Also gates
273
+ │ every │
274
+ │ destructive step │
275
+ │ of a '--emit │
276
+ │ neo4j' Bolt │
277
+ │ push: a lazy │
278
+ │ push only adds │
279
+ │ and updates, an │
280
+ │ eager one also │
281
+ │ removes │
282
+ │ declarations and │
283
+ │ edges the source │
284
+ │ no longer has. │
271
285
  │ [default: lazy] │
272
286
  │ --skip-tests --include-tests Skip test files │
273
287
  │ in analysis. │
@@ -283,6 +297,16 @@ $ canpy --help
283
297
  │ environment │
284
298
  │ instead. │
285
299
  │ [default: venv] │
300
+ │ --resolve-instal… Additionally │
301
+ │ bind imports via │
302
+ │ the project │
303
+ │ venv's installed │
304
+ │ metadata │
305
+ │ (*.dist-info); │
306
+ │ output becomes │
307
+ │ machine-depende… │
308
+ │ (prov: │
309
+ │ installed-metad… │
286
310
  │ --file-name <path> Analyze only the │
287
311
  │ specified file │
288
312
  │ (relative to │
@@ -314,6 +338,22 @@ $ canpy --help
314
338
  │ shipped rules. A │
315
339
  │ malformed file │
316
340
  │ is an error. │
341
+ │ --artifact-text --no-artifact-… Capture verbatim │
342
+ │ `source` text on │
343
+ │ discovered │
344
+ │ artifacts. │
345
+ │ `source` is the │
346
+ │ whole file; │
347
+ │ --no-artifact-t… │
348
+ │ empties it │
349
+ │ everywhere │
350
+ │ (inventory │
351
+ │ unchanged). │
352
+ │ sha256/size_byt… │
353
+ │ always reflect │
354
+ │ the full file. │
355
+ │ [default: │
356
+ │ artifact-text] │
317
357
  │ --help Show this │
318
358
  │ message and │
319
359
  │ exit. │
@@ -485,14 +525,29 @@ A **callable** (function or method) carries its own CPG, keyed by node id:
485
525
  }
486
526
  ```
487
527
 
528
+ The application envelope also contains three substrate sections:
529
+
530
+ - **`artifacts`** — discovered non-code files (manifests, configs, Docker files, CI workflows,
531
+ packaging files, scripts, docs, and legal files) with extraction status (`none`, `partial`, or
532
+ `full`; default `none`), keyed by relative path; each artifact carries the
533
+ `can://artifact/<app>/<path>` id namespace. Config files carry extracted `config_keys`
534
+ (keys, values, namespaces, and references) and `DEFINES_CONFIG` Neo4j edges.
535
+ - **`dependencies`** — declared packages with kind (`runtime`/`dev`/`optional`/`build`), spec,
536
+ locked version, and provenance (`prov`): where each binding came from (manifest file, lock file,
537
+ installed metadata).
538
+ - **`unresolved_imports`** — modules imported but not resolvable in the declared dependency set,
539
+ one entry per module.
540
+
488
541
  Notable properties:
489
542
 
490
543
  - **Durable `can://` ids** identify every node at callable granularity and above
491
544
  (`can://python/<app>/<file>/<callable-sig>`); nodes below a callable use ordinal ids
492
545
  (`@entry`, `@exit`, `line:col`, `@formal_in:N`, `line:col/actual_in:N`).
493
546
  - **`source` lives once per module**; every node's text is the `module.source[span.bytes]` slice.
494
- - **Cross-function edges** — `call_graph`, `param_in`, `param_out` — live at **application** scope;
547
+ - **Cross-function edges** — `call_graph`, `param_in`, `param_out`, `config_uses` — live at **application** scope;
495
548
  the intraprocedural `cfg`/`cdg`/`ddg` and the `summary` edges live **on the callable**.
549
+ `config_uses` resolve from call-site key arguments to `ConfigKey` nodes; unresolved reads
550
+ go to `config_reads_unresolved`.
496
551
  - **No dangling endpoints** — every `call_graph` `src`/`dst` joins the id space: declared
497
552
  callables by their tree id, imported/builtin targets by a `…/@external/<module>/<name>` id
498
553
  homed in `application.external_symbols`.
@@ -513,9 +568,13 @@ binary format).
513
568
 
514
569
  ### Neo4j graph
515
570
 
516
- `--emit neo4j` projects the same schema v2.0.0 analysis into a labeled property graph. Every node
517
- label is `Py`-prefixed and every relationship type is `PY_`-prefixed (e.g. `:PyClass`, `PY_CALLS`)
518
- so multiple language analyzers can share one database without label or relationship-type collisions.
571
+ `--emit neo4j` projects the same schema v2.0.0 analysis into a labeled property graph. Every
572
+ Python-specific node label is `Py`-prefixed and every Python-specific relationship type is
573
+ `PY_`-prefixed (e.g. `:PyClass`, `PY_CALLS`) so multiple language analyzers can share one database
574
+ without label or relationship-type collisions. The one deliberate exception is the language-neutral
575
+ `Artifact`/`Package` subgraph (non-code files and third-party dependencies) — those nodes carry no
576
+ `Py` prefix, since they are meant as cross-language merge targets: a sibling-language analyzer over
577
+ the same repo should land on the same `Artifact`/`Package` nodes, not a per-language duplicate.
519
578
  Declarations are keyed by their **`can://` id** under a shared `:PySymbol` label; calls, imports,
520
579
  inheritance, decorators, and call sites are relationships. At `-a 3`/`-a 4` the projection gains the
521
580
  **CPG overlay** — `:PyBodyNode` nodes (statements, and at level 4 the parameter vertices) wired by
@@ -576,6 +635,56 @@ runtime (Docker or Podman) and is enabled with an environment variable:
576
635
  RUN_CONTAINER_TESTS=1 uv run pytest test/test_neo4j_bolt.py -s
577
636
  ```
578
637
 
638
+ ## Graph query cookbook
639
+
640
+ Example Cypher over the projected graph (`--emit neo4j`, then load `graph.cypher` or push via Bolt).
641
+
642
+ ```cypher
643
+ // who calls this function? (direct callers)
644
+ MATCH (c:PyCallable)-[:PY_CALLS]->(t:PyCallable {name: "process_payment"})
645
+ RETURN c.id
646
+
647
+ // every callable that reaches a given library, via the external ghosts
648
+ MATCH (c:PyCallable)-[:PY_CALLS]->(e:PyExternal)
649
+ WHERE e.id CONTAINS "/@external/requests/"
650
+ RETURN DISTINCT c.id
651
+
652
+ // entrypoints and the frameworks that invoke them
653
+ MATCH (m:PyCallable {is_entrypoint: true})
654
+ RETURN m.id, m.entrypoint_frameworks
655
+
656
+ // data dependences into one statement (level 3+)
657
+ MATCH (s:PyBodyNode {id: $stmt})<-[d:PY_DDG]-(src:PyBodyNode)
658
+ RETURN src.id, d.var, d.prov
659
+
660
+ // interprocedural flow through a parameter (level 4)
661
+ MATCH (a:PyBodyNode)-[:PY_PARAM_IN]->(f:PyBodyNode)
662
+ WHERE f.id STARTS WITH "can://python/myapp/src/api.py"
663
+ RETURN a.id, f.id
664
+ ```
665
+
666
+ Artifact and dependency queries (1.3.0+):
667
+
668
+ ```cypher
669
+ // all container/orchestration configs in the app
670
+ MATCH (a:PyApplication)-[:HAS_ARTIFACT]->(f:Artifact)
671
+ WHERE any(r IN f.roles WHERE r IN ["service-topology", "container-image"])
672
+ RETURN f.id, f.format
673
+
674
+ // every callable that reaches code from a declared package
675
+ MATCH (c:PyCallable)-[:PY_CALLS]->(:PyExternal)<-[:PY_PROVIDES]-(p:Package {id: "pkg:pypi/requests"})
676
+ RETURN c.id
677
+
678
+ // undeclared imports (dependency hygiene)
679
+ MATCH (a:PyApplication)-[u:PY_UNRESOLVED_IMPORT]->(e:PyExternal)
680
+ WHERE NOT (e)<-[:PY_PROVIDES]-(:Package)
681
+ RETURN e.id, u.prov
682
+
683
+ // which lock file pins this package, and to what
684
+ MATCH (f:Artifact)-[l:LOCKS]->(p:Package {id: "pkg:pypi/numpy"})
685
+ RETURN f.id, l.version
686
+ ```
687
+
579
688
  ## License
580
689
 
581
690
  Apache 2.0 — see [LICENSE](./LICENSE).
@@ -175,7 +175,10 @@ def main(
175
175
  bool,
176
176
  typer.Option(
177
177
  "--eager/--lazy",
178
- help="Enable eager or lazy analysis. Defaults to lazy.",
178
+ help="Enable eager or lazy analysis. Defaults to lazy. Also gates every "
179
+ "destructive step of a '--emit neo4j' Bolt push: a lazy push only adds and "
180
+ "updates, an eager one also removes declarations and edges the source no "
181
+ "longer has.",
179
182
  ),
180
183
  ] = False,
181
184
  skip_tests: Annotated[
@@ -193,6 +196,14 @@ def main(
193
196
  "imports against the ambient Python environment instead.",
194
197
  ),
195
198
  ] = False,
199
+ resolve_installed: Annotated[
200
+ bool,
201
+ typer.Option(
202
+ "--resolve-installed",
203
+ help="Additionally bind imports via the project venv's installed metadata "
204
+ "(*.dist-info); output becomes machine-dependent (prov: installed-metadata).",
205
+ ),
206
+ ] = False,
196
207
  file_name: Annotated[
197
208
  Optional[Path],
198
209
  typer.Option(
@@ -226,6 +237,16 @@ def main(
226
237
  "the shipped rules. A malformed file is an error.",
227
238
  ),
228
239
  ] = None,
240
+ artifact_text: Annotated[
241
+ bool,
242
+ typer.Option(
243
+ "--artifact-text/--no-artifact-text",
244
+ help="Capture verbatim `source` text on discovered artifacts. "
245
+ "`source` is the whole file; --no-artifact-text empties it "
246
+ "everywhere (inventory unchanged). sha256/size_bytes always "
247
+ "reflect the full file.",
248
+ ),
249
+ ] = True,
229
250
  ):
230
251
  # Determinism: pin the interpreter hash seed before any analysis (no-op
231
252
  # when PYTHONHASHSEED is already set; --version exits before this).
@@ -303,11 +324,13 @@ def main(
303
324
  rebuild_analysis=rebuild_analysis,
304
325
  skip_tests=skip_tests,
305
326
  no_venv=no_venv,
327
+ resolve_installed=resolve_installed,
306
328
  file_name=file_name,
307
329
  cache_dir=cache_dir,
308
330
  clear_cache=clear_cache,
309
331
  verbosity=verbosity,
310
332
  entrypoint_rules=tuple(entrypoint_rules or ()),
333
+ artifact_text=artifact_text,
311
334
  )
312
335
 
313
336
  _set_log_level(options.verbosity)
@@ -0,0 +1,20 @@
1
+ """Non-code artifact capture and dependency extraction (spec 2026-08-27).
2
+
3
+ Capture never drops a file (every non-`.py` file becomes a
4
+ :class:`~codeanalyzer.schema.py_schema.PyArtifact`, rule-matched or not,
5
+ text or binary -- issue #157 follow-up); extraction is narrow (only
6
+ dependency manifests are parsed for meaning in this unit)."""
7
+
8
+ from codeanalyzer.artifacts.config_keys import extract_config_keys, is_config_eligible
9
+ from codeanalyzer.artifacts.config_use import (
10
+ dataflow_intra_tier, dataflow_interproc_tier, detect_config_reads, resolve_uses,
11
+ )
12
+ from codeanalyzer.artifacts.dependencies import build_dependency_view
13
+ from codeanalyzer.artifacts.discovery import discover_artifacts
14
+
15
+ __all__ = [
16
+ "discover_artifacts", "build_dependency_view",
17
+ "extract_config_keys", "is_config_eligible",
18
+ "detect_config_reads", "resolve_uses",
19
+ "dataflow_intra_tier", "dataflow_interproc_tier",
20
+ ]