entrygraph 0.1.27__tar.gz → 0.1.28__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 (198) hide show
  1. {entrygraph-0.1.27 → entrygraph-0.1.28}/PKG-INFO +1 -1
  2. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/_version.py +2 -2
  3. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/api.py +23 -3
  4. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/cli/main.py +10 -0
  5. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/base.py +11 -0
  6. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/csharp.py +22 -3
  7. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/golang.py +7 -8
  8. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/java.py +41 -4
  9. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/javascript.py +26 -0
  10. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/python.py +8 -2
  11. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/extract/javascript.py +18 -9
  12. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/graph/adjacency.py +49 -9
  13. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/graph/cte.py +9 -6
  14. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/pipeline/scanner.py +61 -10
  15. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/resolve/hierarchy.py +9 -1
  16. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/resolve/resolver.py +18 -6
  17. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/resolve/symbol_table.py +23 -4
  18. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_entrypoint_expansion.py +175 -0
  19. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_extract_go.py +30 -0
  20. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_extract_java.py +6 -5
  21. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_extract_javascript.py +27 -0
  22. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_indexer.py +27 -0
  23. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_reachability.py +34 -0
  24. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_resolver.py +82 -0
  25. {entrygraph-0.1.27 → entrygraph-0.1.28}/.github/workflows/ci.yml +0 -0
  26. {entrygraph-0.1.27 → entrygraph-0.1.28}/.github/workflows/release.yml +0 -0
  27. {entrygraph-0.1.27 → entrygraph-0.1.28}/.gitignore +0 -0
  28. {entrygraph-0.1.27 → entrygraph-0.1.28}/LICENSE +0 -0
  29. {entrygraph-0.1.27 → entrygraph-0.1.28}/README.md +0 -0
  30. {entrygraph-0.1.27 → entrygraph-0.1.28}/RELEASING.md +0 -0
  31. {entrygraph-0.1.27 → entrygraph-0.1.28}/pyproject.toml +0 -0
  32. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/__init__.py +0 -0
  33. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/__main__.py +0 -0
  34. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/cli/__init__.py +0 -0
  35. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/cli/render.py +0 -0
  36. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/data/sinks/csharp.toml +0 -0
  37. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/data/sinks/go.toml +0 -0
  38. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/data/sinks/java.toml +0 -0
  39. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/data/sinks/javascript.toml +0 -0
  40. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/data/sinks/lib_javascript.toml +0 -0
  41. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/data/sinks/lib_python.toml +0 -0
  42. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/data/sinks/php.toml +0 -0
  43. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/data/sinks/python.toml +0 -0
  44. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/data/sinks/ruby.toml +0 -0
  45. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/data/sinks/rust.toml +0 -0
  46. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/db/__init__.py +0 -0
  47. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/db/engine.py +0 -0
  48. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/db/meta.py +0 -0
  49. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/db/models.py +0 -0
  50. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/db/queries.py +0 -0
  51. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/detect/__init__.py +0 -0
  52. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/__init__.py +0 -0
  53. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/configs.py +0 -0
  54. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/php.py +0 -0
  55. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/ruby.py +0 -0
  56. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/detect/entrypoints/rust.py +0 -0
  57. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/detect/frameworks.py +0 -0
  58. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/detect/manifests.py +0 -0
  59. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/detect/taint.py +0 -0
  60. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/errors.py +0 -0
  61. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/extract/__init__.py +0 -0
  62. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/extract/base.py +0 -0
  63. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/extract/csharp.py +0 -0
  64. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/extract/golang.py +0 -0
  65. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/extract/ir.py +0 -0
  66. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/extract/java.py +0 -0
  67. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/extract/php.py +0 -0
  68. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/extract/python.py +0 -0
  69. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/extract/registry.py +0 -0
  70. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/extract/ruby.py +0 -0
  71. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/extract/rust.py +0 -0
  72. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/fs/__init__.py +0 -0
  73. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/fs/hashing.py +0 -0
  74. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/fs/lang.py +0 -0
  75. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/fs/walker.py +0 -0
  76. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/graph/__init__.py +0 -0
  77. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/graph/scoring.py +0 -0
  78. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/kinds.py +0 -0
  79. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/parsing/__init__.py +0 -0
  80. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/parsing/parsers.py +0 -0
  81. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/parsing/queries.py +0 -0
  82. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/pipeline/__init__.py +0 -0
  83. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/pipeline/worker.py +0 -0
  84. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/pipeline/writer.py +0 -0
  85. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/py.typed +0 -0
  86. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/csharp/calls.scm +0 -0
  87. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/csharp/definitions.scm +0 -0
  88. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/csharp/imports.scm +0 -0
  89. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/go/calls.scm +0 -0
  90. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/go/definitions.scm +0 -0
  91. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/go/imports.scm +0 -0
  92. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/java/calls.scm +0 -0
  93. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/java/definitions.scm +0 -0
  94. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/java/imports.scm +0 -0
  95. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/javascript/calls.scm +0 -0
  96. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/javascript/definitions.scm +0 -0
  97. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/javascript/imports.scm +0 -0
  98. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/php/calls.scm +0 -0
  99. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/php/definitions.scm +0 -0
  100. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/php/imports.scm +0 -0
  101. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/python/calls.scm +0 -0
  102. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/python/definitions.scm +0 -0
  103. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/python/imports.scm +0 -0
  104. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/ruby/calls.scm +0 -0
  105. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/ruby/definitions.scm +0 -0
  106. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/ruby/imports.scm +0 -0
  107. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/rust/calls.scm +0 -0
  108. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/rust/definitions.scm +0 -0
  109. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/queries/rust/imports.scm +0 -0
  110. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/resolve/__init__.py +0 -0
  111. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/resolve/externals.py +0 -0
  112. {entrygraph-0.1.27 → entrygraph-0.1.28}/src/entrygraph/results.py +0 -0
  113. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/conftest.py +0 -0
  114. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/csharp/aspnet_app/Controllers/ReportsController.cs +0 -0
  115. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/csharp/aspnet_app/Program.cs +0 -0
  116. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/csharp/aspnet_app/Services/ReportService.cs +0 -0
  117. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/csharp/aspnet_app/app.csproj +0 -0
  118. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/csharp/minimalapi_app/Program.cs +0 -0
  119. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/csharp/minimalapi_app/app.csproj +0 -0
  120. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/go/gin_app/go.mod +0 -0
  121. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/go/gin_app/main.go +0 -0
  122. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/go/gin_app/service.go +0 -0
  123. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/go/nethttp_app/go.mod +0 -0
  124. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/go/nethttp_app/main.go +0 -0
  125. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/java/methodref_app/pom.xml +0 -0
  126. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/java/methodref_app/src/main/java/com/example/App.java +0 -0
  127. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/java/spring_app/pom.xml +0 -0
  128. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/java/spring_app/src/main/java/com/example/Application.java +0 -0
  129. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportRunner.java +0 -0
  130. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/java/spring_app/src/main/java/com/example/ReportService.java +0 -0
  131. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/java/spring_app/src/main/java/com/example/UserController.java +0 -0
  132. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/javascript/commonjs_app/server.js +0 -0
  133. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/javascript/express_app/package.json +0 -0
  134. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/javascript/express_app/src/routes.js +0 -0
  135. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/javascript/express_app/src/services.js +0 -0
  136. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/php/laravel_app/app/Http/Controllers/ReportController.php +0 -0
  137. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/php/laravel_app/artisan +0 -0
  138. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/php/laravel_app/composer.json +0 -0
  139. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/php/laravel_app/routes/web.php +0 -0
  140. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/python/chained_sinks/app.py +0 -0
  141. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/python/flask_app/app/__init__.py +0 -0
  142. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/python/flask_app/app/db.py +0 -0
  143. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/python/flask_app/app/routes.py +0 -0
  144. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/python/flask_app/app/services.py +0 -0
  145. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/python/flask_app/cli.py +0 -0
  146. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/python/flask_app/requirements.txt +0 -0
  147. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/python/fuzzy_sink/app.py +0 -0
  148. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/python/heal_fidelity/caller.py +0 -0
  149. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/python/heal_fidelity/worker.py +0 -0
  150. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/python/may_continue/app.py +0 -0
  151. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/python/sanitizer/app.py +0 -0
  152. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/python/taint_source/handler.py +0 -0
  153. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/ruby/sinatra_app/Gemfile +0 -0
  154. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/ruby/sinatra_app/app.rb +0 -0
  155. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/ruby/sinatra_app/services/runner.rb +0 -0
  156. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/rust/axum_app/Cargo.toml +0 -0
  157. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/rust/axum_app/src/handlers.rs +0 -0
  158. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/rust/axum_app/src/main.rs +0 -0
  159. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/rust/axum_callback_app/Cargo.toml +0 -0
  160. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/rust/axum_callback_app/src/main.rs +0 -0
  161. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/rust/scoped_sink_app/Cargo.toml +0 -0
  162. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/fixtures/rust/scoped_sink_app/src/main.rs +0 -0
  163. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_api.py +0 -0
  164. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_cli.py +0 -0
  165. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_commonjs.py +0 -0
  166. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_csharp_callbacks.py +0 -0
  167. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_cte_bounds.py +0 -0
  168. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_engine_pragmas.py +0 -0
  169. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_entrypoints.py +0 -0
  170. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_extract_csharp.py +0 -0
  171. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_extract_php.py +0 -0
  172. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_extract_python.py +0 -0
  173. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_extract_ruby.py +0 -0
  174. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_extract_rust.py +0 -0
  175. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_frameworks.py +0 -0
  176. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_fuzzy_sink.py +0 -0
  177. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_go_callbacks.py +0 -0
  178. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_hardening.py +0 -0
  179. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_heal_fidelity.py +0 -0
  180. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_incremental.py +0 -0
  181. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_java_callbacks.py +0 -0
  182. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_lang.py +0 -0
  183. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_manifests.py +0 -0
  184. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_may_continue.py +0 -0
  185. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_models.py +0 -0
  186. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_pool.py +0 -0
  187. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_registry_cache.py +0 -0
  188. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_render.py +0 -0
  189. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_rust_callbacks.py +0 -0
  190. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_rust_scoped_sinks.py +0 -0
  191. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_sanitizer_languages.py +0 -0
  192. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_scoring.py +0 -0
  193. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_sink_catalog.py +0 -0
  194. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_taint.py +0 -0
  195. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_taint_sanitizers.py +0 -0
  196. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_taint_sources.py +0 -0
  197. {entrygraph-0.1.27 → entrygraph-0.1.28}/tests/test_walker.py +0 -0
  198. {entrygraph-0.1.27 → entrygraph-0.1.28}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entrygraph
3
- Version: 0.1.27
3
+ Version: 0.1.28
4
4
  Summary: Language-agnostic code graph: query symbols, entrypoints, and source-to-sink call paths from a SQLite index
5
5
  Project-URL: Repository, https://github.com/brettbergin/entrygraph
6
6
  Author-email: Brett Bergin <brettberginbc@yahoo.com>
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.27'
22
- __version_tuple__ = version_tuple = (0, 1, 27)
21
+ __version__ = version = '0.1.28'
22
+ __version_tuple__ = version_tuple = (0, 1, 28)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -88,6 +88,17 @@ class _FilteredAdjacency:
88
88
  )
89
89
 
90
90
 
91
+ class PathResults(list):
92
+ """CallPath list that also reports whether enumeration was budget-truncated.
93
+
94
+ It is a plain list for every existing use (iteration, len, indexing); the
95
+ `truncated` flag lets the CLI warn when 0 paths may mean "budget spent", not
96
+ "no reachable sink".
97
+ """
98
+
99
+ truncated: bool = False
100
+
101
+
91
102
  def _has_sanitizer(path: CallPath) -> bool:
92
103
  """True if a category sanitizer is called on the path (heuristic, no dataflow).
93
104
 
@@ -317,11 +328,15 @@ class CodeGraph:
317
328
  sources = self._source_ids(session, source, source_category)
318
329
  sinks = self._sink_ids(session, sink, sink_category)
319
330
  if not sources or not sinks:
320
- return []
331
+ return PathResults()
321
332
  traverser = self._traverser(session, engine, kinds, floor, include_cha)
322
333
  raw_paths = traverser.paths(sources, sinks, max_depth=max_depth, max_paths=max_paths)
323
334
  if not raw_paths:
324
- return []
335
+ # 0 paths may mean the visit budget was spent before any sink was
336
+ # reached — propagate the flag so this isn't mistaken for "safe".
337
+ empty = PathResults()
338
+ empty.truncated = bool(getattr(raw_paths, "truncated", False))
339
+ return empty
325
340
  all_ids = {node for path in raw_paths for node, _ in path}
326
341
  symbol_map = q.symbols_by_ids(session, all_ids)
327
342
  registry = self._registry(session)
@@ -347,7 +362,12 @@ class CodeGraph:
347
362
  results.sort(
348
363
  key=lambda p: (-(p.risk_score or 0.0), len(p.symbols), [s.id for s in p.symbols])
349
364
  )
350
- return results
365
+ # Enumeration collected a candidate pool; return the top max_paths by risk.
366
+ # Truncating after the risk rank (not during DFS) is what makes the widen
367
+ # flags monotonic — a wider edge set can only add candidates to rank.
368
+ out = PathResults(results[:max_paths])
369
+ out.truncated = bool(getattr(raw_paths, "truncated", False))
370
+ return out
351
371
 
352
372
  def reachable(
353
373
  self,
@@ -8,6 +8,7 @@ directly testable.
8
8
  from __future__ import annotations
9
9
 
10
10
  import argparse
11
+ import sys
11
12
  from dataclasses import asdict
12
13
  from pathlib import Path
13
14
 
@@ -277,6 +278,15 @@ def cmd_paths(args) -> int:
277
278
  include_callbacks=args.include_callbacks,
278
279
  prune_sanitized=args.prune_sanitized,
279
280
  )
281
+ truncated = bool(getattr(paths, "truncated", False))
282
+ if truncated:
283
+ # A budget-truncated search may have missed paths — never let an empty or
284
+ # short result read as a clean "no reachable sinks" on a large graph.
285
+ print(
286
+ "warning: path search hit its work budget and may be incomplete; "
287
+ "narrow --source/--sink or lower --max-depth for a complete answer.",
288
+ file=sys.stderr,
289
+ )
280
290
  if args.json:
281
291
  print(
282
292
  to_json(
@@ -74,6 +74,17 @@ def first_string_arg(decorator_text: str) -> str | None:
74
74
  return match.group(1) if match else None
75
75
 
76
76
 
77
+ def compose_route(prefix: str | None, path: str | None) -> str:
78
+ """Join a class/group-level route prefix with a method-level path.
79
+
80
+ Either side may be None/empty or carry stray leading/trailing slashes. The
81
+ result is a single slash-prefixed route ("/" when both are empty), matching
82
+ the NestJS rule so composed routes read consistently across frameworks.
83
+ """
84
+ parts = [seg.strip("/") for seg in (prefix, path) if seg and seg.strip("/")]
85
+ return "/" + "/".join(parts)
86
+
87
+
77
88
  def methods_kwarg(decorator_text: str) -> list[str]:
78
89
  match = _METHODS_KWARG.search(decorator_text)
79
90
  if not match:
@@ -14,6 +14,7 @@ import re
14
14
 
15
15
  from entrygraph.detect.entrypoints.base import (
16
16
  EntrypointRule,
17
+ compose_route,
17
18
  first_string_arg,
18
19
  register,
19
20
  )
@@ -48,14 +49,32 @@ def _is_controller(t: RawSymbol) -> bool:
48
49
  return any("Controller" in b for b in t.bases)
49
50
 
50
51
 
52
+ def _controller_token(qname: str) -> str:
53
+ """The `[controller]` route-token value: the class name minus a Controller suffix."""
54
+ name = qname.rsplit(".", 1)[-1]
55
+ return name[: -len("Controller")] if name.endswith("Controller") else name
56
+
57
+
58
+ def _class_route_prefix(t: RawSymbol) -> str:
59
+ """A controller's class-level [Route("...")] prefix, with [controller] expanded."""
60
+ route_dec = next((d for d in t.decorators if _ROUTE_ATTR.match(d)), None)
61
+ prefix = first_string_arg(route_dec) if route_dec else None
62
+ if not prefix:
63
+ return ""
64
+ return prefix.replace("[controller]", _controller_token(t.qualified_name))
65
+
66
+
51
67
  def _aspnet_controller_routes(x: FileExtraction) -> list[EntrypointHint]:
52
- controllers = {t.qualified_name for t in _type_symbols(x) if _is_controller(t)}
68
+ controller_syms = [t for t in _type_symbols(x) if _is_controller(t)]
69
+ controllers = {t.qualified_name for t in controller_syms}
53
70
  if not controllers:
54
71
  return []
72
+ prefixes = {t.qualified_name: _class_route_prefix(t) for t in controller_syms}
55
73
  hints: list[EntrypointHint] = []
56
74
  for method in _methods(x):
57
75
  if method.parent_qualified_name not in controllers:
58
76
  continue
77
+ prefix = prefixes.get(method.parent_qualified_name or "", "")
59
78
  for decorator in method.decorators:
60
79
  m = _HTTP_ATTR.match(decorator)
61
80
  if m:
@@ -64,7 +83,7 @@ def _aspnet_controller_routes(x: FileExtraction) -> list[EntrypointHint]:
64
83
  rule_id="csharp.aspnet.controller-route",
65
84
  kind=EntrypointKind.HTTP_ROUTE,
66
85
  handler_qualified_name=method.qualified_name,
67
- route=first_string_arg(decorator) or "",
86
+ route=compose_route(prefix, first_string_arg(decorator)),
68
87
  http_methods=[m.group(1).upper()],
69
88
  framework="aspnetcore",
70
89
  )
@@ -79,7 +98,7 @@ def _aspnet_controller_routes(x: FileExtraction) -> list[EntrypointHint]:
79
98
  rule_id="csharp.aspnet.controller-route",
80
99
  kind=EntrypointKind.HTTP_ROUTE,
81
100
  handler_qualified_name=method.qualified_name,
82
- route=first_string_arg(route_dec) or "",
101
+ route=compose_route(prefix, first_string_arg(route_dec)),
83
102
  http_methods=["*"],
84
103
  framework="aspnetcore",
85
104
  )
@@ -11,8 +11,11 @@ from entrygraph.detect.entrypoints.base import (
11
11
  from entrygraph.extract.ir import EntrypointHint, FileExtraction
12
12
  from entrygraph.kinds import EntrypointKind, SymbolKind
13
13
 
14
- _GIN_METHODS = frozenset(
15
- {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "Any", "Handle"}
14
+ # gin exports UPPERCASE verbs (r.GET); chi/fiber use TitleCase (r.Get). Accept
15
+ # both so the shared _gin_style matcher recognizes chi/fiber, not just gin.
16
+ _HTTP_VERBS = frozenset({"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"})
17
+ _GIN_METHODS = (
18
+ _HTTP_VERBS | frozenset(v.title() for v in _HTTP_VERBS) | frozenset({"Any", "Handle"})
16
19
  )
17
20
  _NETHTTP_REGISTER = frozenset({"HandleFunc", "Handle"})
18
21
 
@@ -73,12 +76,8 @@ def _gin_routes(x: FileExtraction) -> list[EntrypointHint]:
73
76
  ):
74
77
  route = first_string_arg("(" + ref.arg_preview.lstrip("("))
75
78
  if route is not None and route.startswith("/"):
76
- method = (
77
- ref.callee_name.upper()
78
- if ref.callee_name
79
- in {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
80
- else "*"
81
- )
79
+ verb = ref.callee_name.upper()
80
+ method = verb if verb in _HTTP_VERBS else "*"
82
81
  hints.append(
83
82
  EntrypointHint(
84
83
  rule_id="go.gin.route",
@@ -13,6 +13,7 @@ import re
13
13
 
14
14
  from entrygraph.detect.entrypoints.base import (
15
15
  EntrypointRule,
16
+ compose_route,
16
17
  first_string_arg,
17
18
  register,
18
19
  )
@@ -38,6 +39,10 @@ def _annotation_names(decorators: list[str], pattern: re.Pattern) -> bool:
38
39
  return any(pattern.match(d) for d in decorators)
39
40
 
40
41
 
42
+ def _class_prefix(qname: str | None, prefixes: dict[str, str]) -> str:
43
+ return prefixes.get(qname or "", "")
44
+
45
+
41
46
  def _type_symbols(x: FileExtraction) -> list[RawSymbol]:
42
47
  return [s for s in x.symbols if s.kind in (SymbolKind.CLASS, SymbolKind.INTERFACE)]
43
48
 
@@ -50,6 +55,17 @@ def _enclosing_type(method: RawSymbol, types: list[RawSymbol]) -> RawSymbol | No
50
55
  return next((t for t in types if t.qualified_name == method.parent_qualified_name), None)
51
56
 
52
57
 
58
+ def _class_route_prefixes(types: list[RawSymbol], pattern: re.Pattern) -> dict[str, str]:
59
+ """Map controller/resource qname -> its class-level route prefix (via `pattern`)."""
60
+ prefixes: dict[str, str] = {}
61
+ for t in types:
62
+ for decorator in t.decorators:
63
+ if pattern.match(decorator):
64
+ prefixes[t.qualified_name] = first_string_arg(decorator) or ""
65
+ break
66
+ return prefixes
67
+
68
+
53
69
  def _spring_routes(x: FileExtraction) -> list[EntrypointHint]:
54
70
  types = _type_symbols(x)
55
71
  controllers = {
@@ -57,6 +73,10 @@ def _spring_routes(x: FileExtraction) -> list[EntrypointHint]:
57
73
  }
58
74
  if not controllers:
59
75
  return []
76
+ # A controller's own @RequestMapping("/api") is its route prefix.
77
+ prefixes = _class_route_prefixes(
78
+ [t for t in types if t.qualified_name in controllers], _SPRING_MAPPING
79
+ )
60
80
  hints = []
61
81
  for method in _methods(x):
62
82
  if method.parent_qualified_name not in controllers:
@@ -67,12 +87,15 @@ def _spring_routes(x: FileExtraction) -> list[EntrypointHint]:
67
87
  continue
68
88
  verb = _MAPPING_VERB.get(match.group(1))
69
89
  methods = [verb] if verb else ["*"]
90
+ route = compose_route(
91
+ _class_prefix(method.parent_qualified_name, prefixes), first_string_arg(decorator)
92
+ )
70
93
  hints.append(
71
94
  EntrypointHint(
72
95
  rule_id="java.spring.route",
73
96
  kind=EntrypointKind.HTTP_ROUTE,
74
97
  handler_qualified_name=method.qualified_name,
75
- route=first_string_arg(decorator) or "",
98
+ route=route,
76
99
  http_methods=methods,
77
100
  framework="spring-boot",
78
101
  )
@@ -85,6 +108,10 @@ def _jaxrs_routes(x: FileExtraction) -> list[EntrypointHint]:
85
108
  resources = {t.qualified_name for t in types if _annotation_names(t.decorators, _JAXRS_PATH)}
86
109
  if not resources:
87
110
  return []
111
+ # A resource class's own @Path("/api") prefixes every sub-resource method.
112
+ prefixes = _class_route_prefixes(
113
+ [t for t in types if t.qualified_name in resources], _JAXRS_PATH
114
+ )
88
115
  hints = []
89
116
  for method in _methods(x):
90
117
  if method.parent_qualified_name not in resources:
@@ -95,13 +122,16 @@ def _jaxrs_routes(x: FileExtraction) -> list[EntrypointHint]:
95
122
  )
96
123
  if verb is None:
97
124
  continue
98
- route = next((first_string_arg(d) for d in method.decorators if _JAXRS_PATH.match(d)), None)
125
+ method_path = next(
126
+ (first_string_arg(d) for d in method.decorators if _JAXRS_PATH.match(d)), None
127
+ )
128
+ route = compose_route(_class_prefix(method.parent_qualified_name, prefixes), method_path)
99
129
  hints.append(
100
130
  EntrypointHint(
101
131
  rule_id="java.jaxrs",
102
132
  kind=EntrypointKind.HTTP_ROUTE,
103
133
  handler_qualified_name=method.qualified_name,
104
- route=route or "",
134
+ route=route,
105
135
  http_methods=[verb],
106
136
  framework="jax-rs",
107
137
  )
@@ -121,6 +151,9 @@ def _micronaut_routes(x: FileExtraction) -> list[EntrypointHint]:
121
151
  }
122
152
  if not controllers:
123
153
  return []
154
+ prefixes = _class_route_prefixes(
155
+ [t for t in types if t.qualified_name in controllers], _MICRONAUT_CONTROLLER
156
+ )
124
157
  hints = []
125
158
  for method in _methods(x):
126
159
  if method.parent_qualified_name not in controllers:
@@ -128,12 +161,16 @@ def _micronaut_routes(x: FileExtraction) -> list[EntrypointHint]:
128
161
  for decorator in method.decorators:
129
162
  m = _MICRONAUT_MAPPING.match(decorator)
130
163
  if m:
164
+ route = compose_route(
165
+ _class_prefix(method.parent_qualified_name, prefixes),
166
+ first_string_arg(decorator),
167
+ )
131
168
  hints.append(
132
169
  EntrypointHint(
133
170
  rule_id="java.micronaut.route",
134
171
  kind=EntrypointKind.HTTP_ROUTE,
135
172
  handler_qualified_name=method.qualified_name,
136
- route=first_string_arg(decorator) or "",
173
+ route=route,
137
174
  http_methods=[m.group(1).upper()],
138
175
  framework="micronaut",
139
176
  )
@@ -14,6 +14,31 @@ _NEST_DECORATOR = re.compile(r"^@(Get|Post|Put|Delete|Patch|All)\b")
14
14
 
15
15
 
16
16
  _TRAILING_IDENT = re.compile(r",\s*([A-Za-z_$][\w$]*)\s*\)?\s*$")
17
+ _RECEIVER_ROOT = re.compile(r"^\s*([A-Za-z_$][\w$]*)")
18
+ # HTTP-client and test-harness roots whose .get()/.post() are *requests*, not
19
+ # route registrations: supertest/nock/chai/superagent and Node/browser clients.
20
+ # Excludes them so test suites don't flood the route table (ghost 72%, strapi 99%).
21
+ _CLIENT_ROOTS = frozenset(
22
+ {
23
+ "nock",
24
+ "supertest",
25
+ "request",
26
+ "superagent",
27
+ "agent",
28
+ "chai",
29
+ "axios",
30
+ "got",
31
+ "fetch",
32
+ "http",
33
+ "https",
34
+ "fastify",
35
+ } # fastify.inject(...).get is a test call
36
+ )
37
+
38
+
39
+ def _receiver_root(receiver_text: str) -> str | None:
40
+ m = _RECEIVER_ROOT.match(receiver_text)
41
+ return m.group(1) if m else None
17
42
 
18
43
 
19
44
  def _router_routes(framework: str):
@@ -29,6 +54,7 @@ def _router_routes(framework: str):
29
54
  if (
30
55
  ref.kind == "call"
31
56
  and ref.receiver_text is not None
57
+ and _receiver_root(ref.receiver_text) not in _CLIENT_ROOTS
32
58
  and ref.callee_name in _HTTP_METHODS
33
59
  and ref.arg_preview
34
60
  ):
@@ -53,8 +53,14 @@ def _flask_routes(x: FileExtraction) -> list[EntrypointHint]:
53
53
  else:
54
54
  verb = _HTTP_VERB.match(decorator)
55
55
  if verb:
56
- route = first_string_arg(decorator)
57
- methods = [verb.group(2).upper()]
56
+ candidate = first_string_arg(decorator)
57
+ # The @app.get/@router.post shorthand shares its shape with
58
+ # @mock.patch("dotted.target"), which the verb `patch` matches.
59
+ # A real Flask/FastAPI route is a URL path ("/..."); a mock target
60
+ # is a dotted import path. Require the URL shape to exclude mocks.
61
+ if candidate is not None and candidate.startswith("/"):
62
+ route = candidate
63
+ methods = [verb.group(2).upper()]
58
64
  if route is not None:
59
65
  hints.append(
60
66
  EntrypointHint(
@@ -36,6 +36,22 @@ _SCOPE_TYPES = frozenset(
36
36
  _NAMED_SCOPES = frozenset({"function_declaration", "method_definition", "class_declaration"})
37
37
  _SRC_ROOTS = ("src", "lib", "app")
38
38
  _EXPORT_PARENTS = frozenset({"export_statement"})
39
+ _JS_EXTS = (".d.ts", ".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".jsx")
40
+
41
+
42
+ def _atomic_segment(filename: str) -> str:
43
+ """A filename -> one module-path segment: strip a JS/TS extension, then map any
44
+ remaining internal dots to `_`.
45
+
46
+ A dotted stem like ``article.service`` must NOT become two dotted segments, or
47
+ ``.split(".")`` in relative-import resolution mis-counts package depth and
48
+ duplicates a directory name (``article.article.article.service``) — issue #42.
49
+ """
50
+ for ext in _JS_EXTS:
51
+ if filename.endswith(ext):
52
+ filename = filename[: -len(ext)]
53
+ break
54
+ return filename.replace(".", "_")
39
55
 
40
56
 
41
57
  class JavaScriptExtractor:
@@ -45,11 +61,7 @@ class JavaScriptExtractor:
45
61
  parts = repo_relative_path.split("/")
46
62
  if parts[0] in _SRC_ROOTS and len(parts) > 1:
47
63
  parts = parts[1:]
48
- stem = parts[-1]
49
- for ext in (".d.ts", ".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".jsx"):
50
- if stem.endswith(ext):
51
- stem = stem[: -len(ext)]
52
- break
64
+ stem = _atomic_segment(parts[-1])
53
65
  is_package = stem in ("index", "mod")
54
66
  parts = parts[:-1] if is_package else [*parts[:-1], stem]
55
67
  return ".".join(p for p in parts if p) or "_root", is_package
@@ -358,10 +370,7 @@ class JavaScriptExtractor:
358
370
  if segment == "..":
359
371
  parts = parts[:-1]
360
372
  else:
361
- seg = segment
362
- for ext in (".ts", ".tsx", ".js", ".jsx"):
363
- if seg.endswith(ext):
364
- seg = seg[: -len(ext)]
373
+ seg = _atomic_segment(segment)
365
374
  if seg not in ("index", "mod"):
366
375
  parts.append(seg)
367
376
  return ".".join(p for p in parts if p) or "_root"
@@ -20,7 +20,39 @@ from sqlalchemy.orm import Session
20
20
  from entrygraph.db.models import Edge
21
21
  from entrygraph.kinds import EdgeKind
22
22
 
23
- _MAX_DFS_VISITS = 200_000 # hard bound on path-enumeration work
23
+ # Path-enumeration work bound. The budget scales with graph size so large graphs
24
+ # aren't cut off before any sink is reached (a flat cap silently returned 0 paths
25
+ # on big repos — a false "no reachable sinks"). Floor keeps small graphs cheap;
26
+ # ceiling bounds pathological ones. When the budget is spent, the result is flagged
27
+ # `truncated` so callers can warn instead of treating 0 paths as "safe".
28
+ _MIN_DFS_VISITS = 200_000
29
+ _DFS_VISIT_CEILING = 40_000_000
30
+ _DFS_VISIT_FACTOR = 200
31
+ # Enumerate a candidate pool larger than the caller's max_paths, then let the
32
+ # caller risk-rank and truncate. Decoupling enumeration from max_paths makes the
33
+ # widen flags (--include-fuzzy/-unresolved/-callbacks) monotonic: a wider edge set
34
+ # yields a superset of the pool instead of a different DFS-order slice.
35
+ _CANDIDATE_FACTOR = 4
36
+ _MIN_CANDIDATES = 64
37
+
38
+
39
+ class PathList(list):
40
+ """A path list that also reports whether enumeration was budget-truncated."""
41
+
42
+ truncated: bool = False
43
+
44
+ @classmethod
45
+ def of(cls, items, truncated: bool = False) -> PathList:
46
+ out = cls(items)
47
+ out.truncated = truncated
48
+ return out
49
+
50
+
51
+ def _candidate_cap(max_paths: int) -> int:
52
+ # reachable() passes max_paths=1 and only needs existence — don't over-enumerate.
53
+ if max_paths <= 1:
54
+ return 1
55
+ return max(max_paths * _CANDIDATE_FACTOR, _MIN_CANDIDATES)
24
56
 
25
57
 
26
58
  @dataclass(frozen=True, slots=True)
@@ -150,22 +182,30 @@ class AdjacencyCache:
150
182
  ) -> list[list[tuple[int, Hop | None]]]:
151
183
  """Enumerate simple paths as [(symbol_id, hop_into_it | None), ...].
152
184
 
153
- DFS with an on-path visited set; neighbor order is deterministic.
154
- Results are sorted shortest-first. A global visit budget bounds work on
155
- pathological graphs; enumeration also stops at max_paths.
185
+ DFS with an on-path visited set; neighbor order is deterministic. Results
186
+ are sorted shortest-first. Enumeration collects up to a candidate pool
187
+ (>= max_paths) so the caller can risk-rank and truncate; a size-scaled
188
+ visit budget bounds work, and the returned PathList reports `truncated`
189
+ when that budget was spent before enumeration finished.
156
190
  """
157
191
  results: list[list[tuple[int, Hop | None]]] = []
158
- budget = _MAX_DFS_VISITS
192
+ total_hops = sum(len(hops) for hops in self.forward.values())
193
+ budget = min(_DFS_VISIT_CEILING, max(_MIN_DFS_VISITS, total_hops * _DFS_VISIT_FACTOR))
194
+ cap = _candidate_cap(max_paths)
195
+ exhausted = False
159
196
 
160
197
  for source in sorted(sources):
161
- if budget <= 0 or len(results) >= max_paths:
198
+ if budget <= 0 or len(results) >= cap:
162
199
  break
163
200
  stack: list[tuple[int, Hop | None]] = [(source, None)]
164
201
  on_path = {source}
165
202
 
166
203
  def dfs(node: int, depth: int) -> None:
167
- nonlocal budget
168
- if budget <= 0 or len(results) >= max_paths:
204
+ nonlocal budget, exhausted
205
+ if len(results) >= cap:
206
+ return
207
+ if budget <= 0:
208
+ exhausted = True
169
209
  return
170
210
  budget -= 1
171
211
  if node in sinks:
@@ -187,4 +227,4 @@ class AdjacencyCache:
187
227
  dfs(source, 0)
188
228
 
189
229
  results.sort(key=lambda p: (len(p), [n for n, _ in p]))
190
- return results[:max_paths]
230
+ return PathList.of(results, truncated=exhausted)
@@ -15,7 +15,7 @@ from __future__ import annotations
15
15
  from sqlalchemy import bindparam, text
16
16
  from sqlalchemy.orm import Session
17
17
 
18
- from entrygraph.graph.adjacency import Hop
18
+ from entrygraph.graph.adjacency import Hop, PathList, _candidate_cap
19
19
  from entrygraph.kinds import EdgeKind
20
20
 
21
21
  _SEP = ","
@@ -41,8 +41,9 @@ class CteEngine:
41
41
 
42
42
  def paths(
43
43
  self, sources: set[int], sinks: set[int], max_depth: int = 25, max_paths: int = 10
44
- ) -> list[list[tuple[int, Hop | None]]]:
44
+ ) -> PathList:
45
45
  results: list[list[tuple[int, Hop | None]]] = []
46
+ cap = _candidate_cap(max_paths)
46
47
  # a source that is itself a sink is a length-1 path (matches memory engine)
47
48
  for src in sorted(sources & sinks):
48
49
  results.append([(src, None)])
@@ -86,7 +87,7 @@ class CteEngine:
86
87
  SELECT nodes, lines, kinds, ids, confs, depth FROM walk
87
88
  WHERE node IN :sinks
88
89
  ORDER BY depth
89
- LIMIT :max_paths
90
+ LIMIT :cap
90
91
  """
91
92
  ).bindparams(
92
93
  bindparam("sources", expanding=True),
@@ -100,20 +101,22 @@ class CteEngine:
100
101
  "minconf": self.min_confidence,
101
102
  "include_cha": 1 if self.include_cha else 0,
102
103
  "max_depth": max_depth,
103
- "max_paths": max_paths,
104
+ "cap": cap,
104
105
  "sep": _SEP,
105
106
  },
106
107
  ).all()
107
108
 
108
109
  for nodes_str, lines_str, kinds_str, ids_str, confs_str, _depth in rows:
109
- if len(results) >= max_paths:
110
+ if len(results) >= cap:
110
111
  break
111
112
  path = self._decode(nodes_str, lines_str, kinds_str, ids_str, confs_str)
112
113
  if path is not None:
113
114
  results.append(path)
114
115
 
115
116
  results.sort(key=lambda p: (len(p), [n for n, _ in p]))
116
- return results[:max_paths]
117
+ # The CTE has no per-visit budget (SQLite bounds it), so it never
118
+ # silently under-returns the way the memory DFS could — never truncated.
119
+ return PathList.of(results, truncated=False)
117
120
 
118
121
  @staticmethod
119
122
  def _decode(