codeanalyzer-python 1.2.0__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 (98) hide show
  1. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/.gitignore +4 -0
  2. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/PKG-INFO +109 -5
  3. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/README.md +107 -4
  4. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/__main__.py +29 -0
  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.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/core.py +91 -0
  13. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/builder.py +15 -1
  14. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/project.py +192 -0
  15. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/schema.py +57 -0
  16. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/options/options.py +5 -0
  17. codeanalyzer_python-1.3.0/codeanalyzer/schema/ids.py +44 -0
  18. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/schema/py_schema.py +118 -0
  19. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/syntactic_analysis/symbol_table_builder.py +11 -0
  20. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/pyproject.toml +4 -1
  21. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/schema.neo4j.json +142 -1
  22. codeanalyzer_python-1.2.0/codeanalyzer/schema/ids.py +0 -23
  23. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/LICENSE +0 -0
  24. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/NOTICE +0 -0
  25. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/__init__.py +0 -0
  26. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/__init__.py +0 -0
  27. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/access_paths.py +0 -0
  28. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/alias.py +0 -0
  29. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/cfg.py +0 -0
  30. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/defuse.py +0 -0
  31. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/dominance.py +0 -0
  32. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/identity.py +0 -0
  33. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/pdg.py +0 -0
  34. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/LICENSE +0 -0
  35. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/README.md +0 -0
  36. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/SSA/__init__.py +0 -0
  37. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/SSA/const.py +0 -0
  38. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/__init__.py +0 -0
  39. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/cfg/__init__.py +0 -0
  40. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/cfg/builder.py +0 -0
  41. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/cfg/model.py +0 -0
  42. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/core/__init__.py +0 -0
  43. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/core/func_call_visitor.py +0 -0
  44. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel/core/vars_visitor.py +0 -0
  45. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scalpel_oracle.py +0 -0
  46. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/scc.py +0 -0
  47. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/sdg.py +0 -0
  48. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/slicing.py +0 -0
  49. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/summaries.py +0 -0
  50. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/dataflow/syntactic.py +0 -0
  51. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/entrypoints/__init__.py +0 -0
  52. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/entrypoints/detect.py +0 -0
  53. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/entrypoints/matching.py +0 -0
  54. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/entrypoints/pipeline.py +0 -0
  55. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/entrypoints/rules.py +0 -0
  56. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/entrypoints/rules.yml +0 -0
  57. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/jedi/__init__.py +0 -0
  58. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/jedi/jedi.py +0 -0
  59. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/__init__.py +0 -0
  60. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/bolt.py +0 -0
  61. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/cypher.py +0 -0
  62. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/emit.py +0 -0
  63. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/neo4j/rows.py +0 -0
  64. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/options/__init__.py +0 -0
  65. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/provenance.py +0 -0
  66. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/py.typed +0 -0
  67. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/schema/__init__.py +0 -0
  68. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/schema/assign_ids.py +0 -0
  69. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/schema/call_graph_ids.py +0 -0
  70. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/schema/l1_body.py +0 -0
  71. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/schema/l2_callees.py +0 -0
  72. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/semantic_analysis/__init__.py +0 -0
  73. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/semantic_analysis/call_graph.py +0 -0
  74. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/semantic_analysis/defuse_linker.py +0 -0
  75. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/syntactic_analysis/__init__.py +0 -0
  76. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/syntactic_analysis/exceptions.py +0 -0
  77. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/syntactic_analysis/import_resolver.py +0 -0
  78. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/utils/__init__.py +0 -0
  79. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/utils/logging.py +0 -0
  80. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/codeanalyzer/utils/progress_bar.py +0 -0
  81. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/docs/handoff/README.md +0 -0
  82. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/docs/handoff/schema.neo4j.json +0 -0
  83. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/schema-uml.drawio +0 -0
  84. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/flask/README.md +0 -0
  85. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/flask/examples/celery/README.md +0 -0
  86. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/flask/requirements-skip/README.md +0 -0
  87. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/flask/src/flask/sansio/README.md +0 -0
  88. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/requests/LICENSE +0 -0
  89. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/requests/NOTICE +0 -0
  90. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/requests/README.md +0 -0
  91. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/requests/docs/_themes/LICENSE +0 -0
  92. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/requests/ext/LICENSE +0 -0
  93. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/xarray/LICENSE +0 -0
  94. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/xarray/README.md +0 -0
  95. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/xarray/properties/README.md +0 -0
  96. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/LICENSE +0 -0
  97. {codeanalyzer_python-1.2.0 → codeanalyzer_python-1.3.0}/test/fixtures/whole_applications/xarray/xarray/datatree_/README.md +0 -0
  98. {codeanalyzer_python-1.2.0 → 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
1
  Metadata-Version: 2.5
2
2
  Name: codeanalyzer-python
3
- Version: 1.2.0
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
@@ -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'
@@ -315,6 +316,16 @@ $ canpy --help
315
316
  │ environment │
316
317
  │ instead. │
317
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… │
318
329
  │ --file-name <path> Analyze only the │
319
330
  │ specified file │
320
331
  │ (relative to │
@@ -346,6 +357,30 @@ $ canpy --help
346
357
  │ shipped rules. A │
347
358
  │ malformed file │
348
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). │
369
+ │ [default: │
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] │
349
384
  │ --help Show this │
350
385
  │ message and │
351
386
  │ exit. │
@@ -517,14 +552,29 @@ A **callable** (function or method) carries its own CPG, keyed by node id:
517
552
  }
518
553
  ```
519
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
+
520
568
  Notable properties:
521
569
 
522
570
  - **Durable `can://` ids** identify every node at callable granularity and above
523
571
  (`can://python/<app>/<file>/<callable-sig>`); nodes below a callable use ordinal ids
524
572
  (`@entry`, `@exit`, `line:col`, `@formal_in:N`, `line:col/actual_in:N`).
525
573
  - **`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;
574
+ - **Cross-function edges** — `call_graph`, `param_in`, `param_out`, `config_uses` — live at **application** scope;
527
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`.
528
578
  - **No dangling endpoints** — every `call_graph` `src`/`dst` joins the id space: declared
529
579
  callables by their tree id, imported/builtin targets by a `…/@external/<module>/<name>` id
530
580
  homed in `application.external_symbols`.
@@ -545,9 +595,13 @@ binary format).
545
595
 
546
596
  ### Neo4j graph
547
597
 
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.
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.
551
605
  Declarations are keyed by their **`can://` id** under a shared `:PySymbol` label; calls, imports,
552
606
  inheritance, decorators, and call sites are relationships. At `-a 3`/`-a 4` the projection gains the
553
607
  **CPG overlay** — `:PyBodyNode` nodes (statements, and at level 4 the parameter vertices) wired by
@@ -608,6 +662,56 @@ runtime (Docker or Podman) and is enabled with an environment variable:
608
662
  RUN_CONTAINER_TESTS=1 uv run pytest test/test_neo4j_bolt.py -s
609
663
  ```
610
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
+
611
715
  ## License
612
716
 
613
717
  Apache 2.0 — see [LICENSE](./LICENSE).
@@ -283,6 +283,16 @@ $ canpy --help
283
283
  │ environment │
284
284
  │ instead. │
285
285
  │ [default: venv] │
286
+ │ --resolve-instal… Additionally │
287
+ │ bind imports via │
288
+ │ the project │
289
+ │ venv's installed │
290
+ │ metadata │
291
+ │ (*.dist-info); │
292
+ │ output becomes │
293
+ │ machine-depende… │
294
+ │ (prov: │
295
+ │ installed-metad… │
286
296
  │ --file-name <path> Analyze only the │
287
297
  │ specified file │
288
298
  │ (relative to │
@@ -314,6 +324,30 @@ $ canpy --help
314
324
  │ shipped rules. A │
315
325
  │ malformed file │
316
326
  │ is an error. │
327
+ │ --artifact-text --no-artifact-… Capture verbatim │
328
+ │ `source` text on │
329
+ │ discovered │
330
+ │ artifacts. │
331
+ │ --no-artifact-t… │
332
+ │ empties `source` │
333
+ │ everywhere │
334
+ │ (inventory │
335
+ │ unchanged). │
336
+ │ [default: │
337
+ │ artifact-text] │
338
+ │ --artifact-text-… <int range> Per-file byte │
339
+ │ [x>=1] cap on captured │
340
+ │ artifact │
341
+ │ `source`; a │
342
+ │ decodable file │
343
+ │ over the cap is │
344
+ │ truncated │
345
+ │ (text_truncated… │
346
+ │ sha256/size_byt… │
347
+ │ always reflect │
348
+ │ the full file. │
349
+ │ [default: │
350
+ │ 262144] │
317
351
  │ --help Show this │
318
352
  │ message and │
319
353
  │ exit. │
@@ -485,14 +519,29 @@ A **callable** (function or method) carries its own CPG, keyed by node id:
485
519
  }
486
520
  ```
487
521
 
522
+ The application envelope also contains three substrate sections:
523
+
524
+ - **`artifacts`** — discovered non-code files (manifests, configs, Docker files, CI workflows,
525
+ packaging files, scripts, docs, and legal files) with extraction status (`none`, `partial`, or
526
+ `full`; default `none`), keyed by relative path; each artifact carries the
527
+ `can://artifact/<app>/<path>` id namespace. Config files carry extracted `config_keys`
528
+ (keys, values, namespaces, and references) and `DEFINES_CONFIG` Neo4j edges.
529
+ - **`dependencies`** — declared packages with kind (`runtime`/`dev`/`optional`/`build`), spec,
530
+ locked version, and provenance (`prov`): where each binding came from (manifest file, lock file,
531
+ installed metadata).
532
+ - **`unresolved_imports`** — modules imported but not resolvable in the declared dependency set,
533
+ one entry per module.
534
+
488
535
  Notable properties:
489
536
 
490
537
  - **Durable `can://` ids** identify every node at callable granularity and above
491
538
  (`can://python/<app>/<file>/<callable-sig>`); nodes below a callable use ordinal ids
492
539
  (`@entry`, `@exit`, `line:col`, `@formal_in:N`, `line:col/actual_in:N`).
493
540
  - **`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;
541
+ - **Cross-function edges** — `call_graph`, `param_in`, `param_out`, `config_uses` — live at **application** scope;
495
542
  the intraprocedural `cfg`/`cdg`/`ddg` and the `summary` edges live **on the callable**.
543
+ `config_uses` resolve from call-site key arguments to `ConfigKey` nodes; unresolved reads
544
+ go to `config_reads_unresolved`.
496
545
  - **No dangling endpoints** — every `call_graph` `src`/`dst` joins the id space: declared
497
546
  callables by their tree id, imported/builtin targets by a `…/@external/<module>/<name>` id
498
547
  homed in `application.external_symbols`.
@@ -513,9 +562,13 @@ binary format).
513
562
 
514
563
  ### Neo4j graph
515
564
 
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.
565
+ `--emit neo4j` projects the same schema v2.0.0 analysis into a labeled property graph. Every
566
+ Python-specific node label is `Py`-prefixed and every Python-specific relationship type is
567
+ `PY_`-prefixed (e.g. `:PyClass`, `PY_CALLS`) so multiple language analyzers can share one database
568
+ without label or relationship-type collisions. The one deliberate exception is the language-neutral
569
+ `Artifact`/`Package` subgraph (non-code files and third-party dependencies) — those nodes carry no
570
+ `Py` prefix, since they are meant as cross-language merge targets: a sibling-language analyzer over
571
+ the same repo should land on the same `Artifact`/`Package` nodes, not a per-language duplicate.
519
572
  Declarations are keyed by their **`can://` id** under a shared `:PySymbol` label; calls, imports,
520
573
  inheritance, decorators, and call sites are relationships. At `-a 3`/`-a 4` the projection gains the
521
574
  **CPG overlay** — `:PyBodyNode` nodes (statements, and at level 4 the parameter vertices) wired by
@@ -576,6 +629,56 @@ runtime (Docker or Podman) and is enabled with an environment variable:
576
629
  RUN_CONTAINER_TESTS=1 uv run pytest test/test_neo4j_bolt.py -s
577
630
  ```
578
631
 
632
+ ## Graph query cookbook
633
+
634
+ Example Cypher over the projected graph (`--emit neo4j`, then load `graph.cypher` or push via Bolt).
635
+
636
+ ```cypher
637
+ // who calls this function? (direct callers)
638
+ MATCH (c:PyCallable)-[:PY_CALLS]->(t:PyCallable {name: "process_payment"})
639
+ RETURN c.id
640
+
641
+ // every callable that reaches a given library, via the external ghosts
642
+ MATCH (c:PyCallable)-[:PY_CALLS]->(e:PyExternal)
643
+ WHERE e.id CONTAINS "/@external/requests/"
644
+ RETURN DISTINCT c.id
645
+
646
+ // entrypoints and the frameworks that invoke them
647
+ MATCH (m:PyCallable {is_entrypoint: true})
648
+ RETURN m.id, m.entrypoint_frameworks
649
+
650
+ // data dependences into one statement (level 3+)
651
+ MATCH (s:PyBodyNode {id: $stmt})<-[d:PY_DDG]-(src:PyBodyNode)
652
+ RETURN src.id, d.var, d.prov
653
+
654
+ // interprocedural flow through a parameter (level 4)
655
+ MATCH (a:PyBodyNode)-[:PY_PARAM_IN]->(f:PyBodyNode)
656
+ WHERE f.id STARTS WITH "can://python/myapp/src/api.py"
657
+ RETURN a.id, f.id
658
+ ```
659
+
660
+ Artifact and dependency queries (1.3.0+):
661
+
662
+ ```cypher
663
+ // all container/orchestration configs in the app
664
+ MATCH (a:PyApplication)-[:HAS_ARTIFACT]->(f:Artifact)
665
+ WHERE any(r IN f.roles WHERE r IN ["service-topology", "container-image"])
666
+ RETURN f.id, f.format
667
+
668
+ // every callable that reaches code from a declared package
669
+ MATCH (c:PyCallable)-[:PY_CALLS]->(:PyExternal)<-[:PY_PROVIDES]-(p:Package {id: "pkg:pypi/requests"})
670
+ RETURN c.id
671
+
672
+ // undeclared imports (dependency hygiene)
673
+ MATCH (a:PyApplication)-[u:PY_UNRESOLVED_IMPORT]->(e:PyExternal)
674
+ WHERE NOT (e)<-[:PY_PROVIDES]-(:Package)
675
+ RETURN e.id, u.prov
676
+
677
+ // which lock file pins this package, and to what
678
+ MATCH (f:Artifact)-[l:LOCKS]->(p:Package {id: "pkg:pypi/numpy"})
679
+ RETURN f.id, l.version
680
+ ```
681
+
579
682
  ## License
580
683
 
581
684
  Apache 2.0 — see [LICENSE](./LICENSE).
@@ -193,6 +193,14 @@ def main(
193
193
  "imports against the ambient Python environment instead.",
194
194
  ),
195
195
  ] = False,
196
+ resolve_installed: Annotated[
197
+ bool,
198
+ typer.Option(
199
+ "--resolve-installed",
200
+ help="Additionally bind imports via the project venv's installed metadata "
201
+ "(*.dist-info); output becomes machine-dependent (prov: installed-metadata).",
202
+ ),
203
+ ] = False,
196
204
  file_name: Annotated[
197
205
  Optional[Path],
198
206
  typer.Option(
@@ -226,6 +234,24 @@ def main(
226
234
  "the shipped rules. A malformed file is an error.",
227
235
  ),
228
236
  ] = None,
237
+ artifact_text: Annotated[
238
+ bool,
239
+ typer.Option(
240
+ "--artifact-text/--no-artifact-text",
241
+ help="Capture verbatim `source` text on discovered artifacts. "
242
+ "--no-artifact-text empties `source` everywhere (inventory unchanged).",
243
+ ),
244
+ ] = True,
245
+ artifact_text_max_bytes: Annotated[
246
+ int,
247
+ typer.Option(
248
+ "--artifact-text-max-bytes",
249
+ help="Per-file byte cap on captured artifact `source`; a decodable "
250
+ "file over the cap is truncated (text_truncated=True). "
251
+ "sha256/size_bytes always reflect the full file.",
252
+ min=1,
253
+ ),
254
+ ] = 262144,
229
255
  ):
230
256
  # Determinism: pin the interpreter hash seed before any analysis (no-op
231
257
  # when PYTHONHASHSEED is already set; --version exits before this).
@@ -303,11 +329,14 @@ def main(
303
329
  rebuild_analysis=rebuild_analysis,
304
330
  skip_tests=skip_tests,
305
331
  no_venv=no_venv,
332
+ resolve_installed=resolve_installed,
306
333
  file_name=file_name,
307
334
  cache_dir=cache_dir,
308
335
  clear_cache=clear_cache,
309
336
  verbosity=verbosity,
310
337
  entrypoint_rules=tuple(entrypoint_rules or ()),
338
+ artifact_text=artifact_text,
339
+ artifact_text_max_bytes=artifact_text_max_bytes,
311
340
  )
312
341
 
313
342
  _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
+ ]