cg-code-graph 0.19.1__tar.gz → 0.21.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 (376) hide show
  1. {cg_code_graph-0.19.1/cg_code_graph.egg-info → cg_code_graph-0.21.0}/PKG-INFO +11 -10
  2. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/README.md +10 -9
  3. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/__init__.py +1 -1
  4. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/blindspots.py +107 -0
  5. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/cli.py +52 -2
  6. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/config.py +42 -2
  7. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/core/model.py +1 -0
  8. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/core/plugin.py +3 -0
  9. cg_code_graph-0.21.0/cg_code_graph/core/redact.py +158 -0
  10. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/coverage.py +59 -8
  11. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/external.py +18 -4
  12. cg_code_graph-0.21.0/cg_code_graph/guard_facts.py +440 -0
  13. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/hooks.py +2 -1
  14. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/indexer.py +33 -2
  15. cg_code_graph-0.21.0/cg_code_graph/insecure_transport.py +513 -0
  16. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/link.py +96 -2
  17. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/local_sockets.py +4 -2
  18. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/mcp_server.py +42 -4
  19. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/payload.py +263 -38
  20. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/django/plugin.py +16 -1
  21. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/express/plugin.py +132 -17
  22. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/laravel/plugin.py +172 -3
  23. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/laravel/values.py +261 -3
  24. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/extractor/extract.php +28 -4
  25. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/gating.py +12 -3
  26. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/http_client.py +20 -4
  27. cg_code_graph-0.21.0/cg_code_graph/plugins/php/inline_guards.py +777 -0
  28. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/plugin.py +4 -1
  29. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/extractor/extract.mjs +356 -16
  30. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/extractor/fw.mjs +32 -2
  31. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/plugin.py +4 -2
  32. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/express.yaml +6 -0
  33. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/protocols/view.py +5 -3
  34. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/query.py +32 -20
  35. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/routes.py +126 -10
  36. cg_code_graph-0.21.0/cg_code_graph/sdk_systems.py +2701 -0
  37. cg_code_graph-0.21.0/cg_code_graph/ssrf_input.py +854 -0
  38. cg_code_graph-0.21.0/cg_code_graph/surface.py +990 -0
  39. cg_code_graph-0.21.0/cg_code_graph/webhook_subs.py +557 -0
  40. cg_code_graph-0.21.0/cg_code_graph/webhooks.py +1541 -0
  41. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0/cg_code_graph.egg-info}/PKG-INFO +11 -10
  42. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph.egg-info/SOURCES.txt +22 -0
  43. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_coverage.py +4 -4
  44. cg_code_graph-0.21.0/tests/test_echo_ref.py +48 -0
  45. cg_code_graph-0.21.0/tests/test_echo_wrapper.py +99 -0
  46. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_elysia.py +13 -0
  47. cg_code_graph-0.21.0/tests/test_fp_batch.py +293 -0
  48. cg_code_graph-0.21.0/tests/test_inline_guards.py +186 -0
  49. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_php_http.py +16 -1
  50. cg_code_graph-0.21.0/tests/test_redact_salt.py +97 -0
  51. cg_code_graph-0.21.0/tests/test_request_keys.py +229 -0
  52. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_routes_search_explain.py +12 -4
  53. cg_code_graph-0.21.0/tests/test_sdk_external_infra.py +279 -0
  54. cg_code_graph-0.21.0/tests/test_sdk_external_mail.py +167 -0
  55. cg_code_graph-0.21.0/tests/test_ssrf.py +424 -0
  56. cg_code_graph-0.21.0/tests/test_surface.py +703 -0
  57. cg_code_graph-0.21.0/tests/test_surface_transport.py +530 -0
  58. cg_code_graph-0.21.0/tests/test_ts_node_http.py +100 -0
  59. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_ts_nuxt.py +8 -3
  60. cg_code_graph-0.21.0/tests/test_unguarded.py +204 -0
  61. cg_code_graph-0.21.0/tests/test_webhooks_152.py +159 -0
  62. cg_code_graph-0.21.0/tests/test_webhooks_152b.py +194 -0
  63. cg_code_graph-0.19.1/cg_code_graph/sdk_systems.py +0 -963
  64. cg_code_graph-0.19.1/cg_code_graph/webhooks.py +0 -598
  65. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/LICENSE +0 -0
  66. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/MANIFEST.in +0 -0
  67. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/__main__.py +0 -0
  68. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/affected.py +0 -0
  69. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/agent_rules.py +0 -0
  70. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/agents.py +0 -0
  71. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/aitools.py +0 -0
  72. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/android_ipc.py +0 -0
  73. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/apple_ipc.py +0 -0
  74. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/apps.py +0 -0
  75. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/bridges.py +0 -0
  76. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/brokers.py +0 -0
  77. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/concepts.py +0 -0
  78. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/core/__init__.py +0 -0
  79. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/core/cache.py +0 -0
  80. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/core/detect.py +0 -0
  81. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/core/env.py +0 -0
  82. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/core/extractors.py +0 -0
  83. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/core/fsutil.py +0 -0
  84. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/core/generated.py +0 -0
  85. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/core/paths.py +0 -0
  86. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/core/store.py +0 -0
  87. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/core/syntax_errors.py +0 -0
  88. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/dart_isolates.py +0 -0
  89. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/doctor.py +0 -0
  90. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/explore.py +0 -0
  91. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/graphql.py +0 -0
  92. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/jobs.py +0 -0
  93. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/lint_async.py +0 -0
  94. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/local_ipc.py +0 -0
  95. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/mcp_install.py +0 -0
  96. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/parity.py +0 -0
  97. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/parity_structure.py +0 -0
  98. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plans.py +0 -0
  99. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/platform_scan.py +0 -0
  100. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/platforms.py +0 -0
  101. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/__init__.py +0 -0
  102. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/astro/__init__.py +0 -0
  103. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/astro/content.py +0 -0
  104. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/astro/plugin.py +0 -0
  105. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/cfamily/__init__.py +0 -0
  106. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/cfamily/plugin.py +0 -0
  107. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/cfamily/syntax.py +0 -0
  108. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/__init__.py +0 -0
  109. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/bridges.py +0 -0
  110. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/extractor/bin/extract.dart +0 -0
  111. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/extractor/pubspec.lock +0 -0
  112. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/extractor/pubspec.yaml +0 -0
  113. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/http.py +0 -0
  114. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/models.py +0 -0
  115. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/plugin.py +0 -0
  116. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/dart/program.py +0 -0
  117. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/django/__init__.py +0 -0
  118. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/django/extras.py +0 -0
  119. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/django/models.py +0 -0
  120. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/django/schemas.py +0 -0
  121. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/django/shapes.py +0 -0
  122. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/django/urls.py +0 -0
  123. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/express/__init__.py +0 -0
  124. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/flutter/__init__.py +0 -0
  125. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/flutter/plugin.py +0 -0
  126. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/java/__init__.py +0 -0
  127. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/java/exact.py +0 -0
  128. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/java/plugin.py +0 -0
  129. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/jvm/__init__.py +0 -0
  130. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/jvm/interop.py +0 -0
  131. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/jvm/spring.py +0 -0
  132. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/kotlin/__init__.py +0 -0
  133. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/kotlin/exact.py +0 -0
  134. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/kotlin/plugin.py +0 -0
  135. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/kotlin/reparse.py +0 -0
  136. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/laravel/__init__.py +0 -0
  137. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/laravel/broadcast.py +0 -0
  138. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/laravel/tests.py +0 -0
  139. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/native/__init__.py +0 -0
  140. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/native/gates.py +0 -0
  141. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/native/runner.py +0 -0
  142. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/native/scipread.py +0 -0
  143. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/native/ts.py +0 -0
  144. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/nest/__init__.py +0 -0
  145. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/nest/plugin.py +0 -0
  146. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/nextjs/__init__.py +0 -0
  147. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/nextjs/plugin.py +0 -0
  148. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/nuxt/__init__.py +0 -0
  149. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/nuxt/plugin.py +0 -0
  150. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/__init__.py +0 -0
  151. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/external.py +0 -0
  152. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/extractor/composer.json +0 -0
  153. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/extractor/composer.lock +0 -0
  154. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/php/strings.py +0 -0
  155. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/__init__.py +0 -0
  156. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/aitools.py +0 -0
  157. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/external.py +0 -0
  158. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/fields.py +0 -0
  159. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/plugin.py +0 -0
  160. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/refs.py +0 -0
  161. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/roots.py +0 -0
  162. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/socketio.py +0 -0
  163. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/subproc.py +0 -0
  164. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/tests.py +0 -0
  165. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/python/values.py +0 -0
  166. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/pyweb/__init__.py +0 -0
  167. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/pyweb/plugin.py +0 -0
  168. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/pyweb/values.py +0 -0
  169. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/rust/__init__.py +0 -0
  170. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/rust/cargo.py +0 -0
  171. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/rust/plugin.py +0 -0
  172. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/rust/syntax.py +0 -0
  173. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/scip/__init__.py +0 -0
  174. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/scip/importer.py +0 -0
  175. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/scip/scip.proto +0 -0
  176. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/scip/scip_pb2.py +0 -0
  177. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/stubs/__init__.py +0 -0
  178. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/stubs/plugins.py +0 -0
  179. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/swift/__init__.py +0 -0
  180. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/swift/baseurl.py +0 -0
  181. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/swift/exact.py +0 -0
  182. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/swift/indexstore.py +0 -0
  183. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/swift/packages.py +0 -0
  184. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/swift/plugin.py +0 -0
  185. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/__init__.py +0 -0
  186. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/baseurl.py +0 -0
  187. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/extractor/package-lock.json +0 -0
  188. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/extractor/package.json +0 -0
  189. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/extractor/rr.mjs +0 -0
  190. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/nav.py +0 -0
  191. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/ts/react_router.py +0 -0
  192. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/tsweb/__init__.py +0 -0
  193. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/tsweb/common.py +0 -0
  194. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/plugins/tsweb/data.py +0 -0
  195. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/__init__.py +0 -0
  196. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/c_cpp.yaml +0 -0
  197. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/common.yaml +0 -0
  198. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/dart.yaml +0 -0
  199. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/django-ninja.yaml +0 -0
  200. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/django.yaml +0 -0
  201. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/djangorestframework.yaml +0 -0
  202. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/java.yaml +0 -0
  203. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/kotlin.yaml +0 -0
  204. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/laravel.yaml +0 -0
  205. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/nest.yaml +0 -0
  206. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/nextjs.yaml +0 -0
  207. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/nuxt.yaml +0 -0
  208. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/php.yaml +0 -0
  209. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/python.yaml +0 -0
  210. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/react-router.yaml +0 -0
  211. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/rust.yaml +0 -0
  212. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/spring.yaml +0 -0
  213. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/swift.yaml +0 -0
  214. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/presets/typescript.yaml +0 -0
  215. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/process_runs.py +0 -0
  216. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/protocols/__init__.py +0 -0
  217. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/protocols/builtin.py +0 -0
  218. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/protocols/matchers.py +0 -0
  219. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/realtime.py +0 -0
  220. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/realtime_events.py +0 -0
  221. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/realtime_native.py +0 -0
  222. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/realtime_ws.py +0 -0
  223. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/roundtrip.py +0 -0
  224. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/rpc.py +0 -0
  225. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/sockets.py +0 -0
  226. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/starters.py +0 -0
  227. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/tests_index.py +0 -0
  228. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/udp_apps.py +0 -0
  229. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/viz/__init__.py +0 -0
  230. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/viz/graph.py +0 -0
  231. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/viz/server.py +0 -0
  232. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/app.css +0 -0
  233. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/app.js +0 -0
  234. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/index.html +0 -0
  235. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/layered.js +0 -0
  236. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/vendor/VERSIONS.txt +0 -0
  237. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/vendor/cose-base.js +0 -0
  238. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/vendor/cytoscape-fcose.js +0 -0
  239. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/vendor/cytoscape.min.js +0 -0
  240. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/viz/static/vendor/layout-base.js +0 -0
  241. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/viz/tools/package-lock.json +0 -0
  242. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/viz/tools/package.json +0 -0
  243. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/viz/tools/shoot.mjs +0 -0
  244. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/watch.py +0 -0
  245. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph/xcode.py +0 -0
  246. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph.egg-info/dependency_links.txt +0 -0
  247. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph.egg-info/entry_points.txt +0 -0
  248. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph.egg-info/requires.txt +0 -0
  249. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/cg_code_graph.egg-info/top_level.txt +0 -0
  250. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/pyproject.toml +0 -0
  251. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/setup.cfg +0 -0
  252. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_affected.py +0 -0
  253. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_agents_install.py +0 -0
  254. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_agents_md.py +0 -0
  255. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_ai_tools.py +0 -0
  256. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_android_ipc.py +0 -0
  257. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_apple_ipc.py +0 -0
  258. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_astro.py +0 -0
  259. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_astro_content.py +0 -0
  260. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_astro_server.py +0 -0
  261. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_bridges.py +0 -0
  262. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_bridges_pigeon_monorepo.py +0 -0
  263. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_brokers.py +0 -0
  264. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_cache_clean.py +0 -0
  265. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_cache_invalidation.py +0 -0
  266. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_cache_migration.py +0 -0
  267. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_channels_tests.py +0 -0
  268. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_cli_output.py +0 -0
  269. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_completeness.py +0 -0
  270. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_config_extras.py +0 -0
  271. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_coverage_ts_hint.py +0 -0
  272. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_dart_isolates.py +0 -0
  273. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_dispatch_interfaces.py +0 -0
  274. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_django_flutter.py +0 -0
  275. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_docs_languages.py +0 -0
  276. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_docs_links.py +0 -0
  277. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_enum_constants.py +0 -0
  278. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_env.py +0 -0
  279. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_explore.py +0 -0
  280. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_external.py +0 -0
  281. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_gating.py +0 -0
  282. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_generated.py +0 -0
  283. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_graphql.py +0 -0
  284. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_hooks.py +0 -0
  285. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_impact_overrides.py +0 -0
  286. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_index_isolation.py +0 -0
  287. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_inherited_overrides.py +0 -0
  288. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_java.py +0 -0
  289. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_java_exact.py +0 -0
  290. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_jobs.py +0 -0
  291. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_js_refs.py +0 -0
  292. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_js_runtime.py +0 -0
  293. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_kotlin.py +0 -0
  294. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_kotlin_binding.py +0 -0
  295. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_kotlin_exact.py +0 -0
  296. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_kotlin_fields.py +0 -0
  297. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_kotlin_mixed.py +0 -0
  298. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_kotlin_nav_consts.py +0 -0
  299. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_kotlin_props.py +0 -0
  300. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_laravel_determinism.py +0 -0
  301. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_laravel_resources.py +0 -0
  302. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_lint_async.py +0 -0
  303. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_local_ipc.py +0 -0
  304. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_local_sockets.py +0 -0
  305. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_mcp.py +0 -0
  306. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_mcp_index.py +0 -0
  307. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_mcp_install.py +0 -0
  308. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_native_cfamily.py +0 -0
  309. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_native_concurrency.py +0 -0
  310. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_native_rust.py +0 -0
  311. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_nuxt_import_dirs.py +0 -0
  312. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_nuxt_rootsrc.py +0 -0
  313. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_packaging.py +0 -0
  314. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_parity.py +0 -0
  315. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_parity_structure.py +0 -0
  316. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_php_props.py +0 -0
  317. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_plain_js.py +0 -0
  318. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_plans.py +0 -0
  319. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_platform_gaps.py +0 -0
  320. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_platform_type_variants.py +0 -0
  321. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_platforms.py +0 -0
  322. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_platforms_apple.py +0 -0
  323. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_presets_config.py +0 -0
  324. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_process_ipc.py +0 -0
  325. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_process_runs.py +0 -0
  326. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_protocols.py +0 -0
  327. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_python_app_tests_module.py +0 -0
  328. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_python_collection_chains.py +0 -0
  329. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_python_compat.py +0 -0
  330. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_python_entry_refs.py +0 -0
  331. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_python_fields.py +0 -0
  332. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_python_infer_cycles.py +0 -0
  333. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_python_roots.py +0 -0
  334. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_python_subprocess_tests.py +0 -0
  335. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_python_tests.py +0 -0
  336. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_python_web_hosts.py +0 -0
  337. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_python_web_routes.py +0 -0
  338. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_python_web_routes_more.py +0 -0
  339. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_react_router.py +0 -0
  340. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_realtime.py +0 -0
  341. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_realtime_native.py +0 -0
  342. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_roundtrip.py +0 -0
  343. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_route_tables.py +0 -0
  344. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_rpc.py +0 -0
  345. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_scip_health.py +0 -0
  346. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_sdk_external.py +0 -0
  347. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_snippet.py +0 -0
  348. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_sockets.py +0 -0
  349. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_spring.py +0 -0
  350. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_starters_budget.py +0 -0
  351. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_swift.py +0 -0
  352. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_swift_binding.py +0 -0
  353. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_swift_exact.py +0 -0
  354. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_swift_facts.py +0 -0
  355. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_swift_fields.py +0 -0
  356. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_swift_modules.py +0 -0
  357. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_swift_more.py +0 -0
  358. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_swift_nav_values.py +0 -0
  359. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_swift_parse_errors.py +0 -0
  360. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_swift_props.py +0 -0
  361. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_swift_sdk.py +0 -0
  362. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_swift_testing.py +0 -0
  363. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_swift_values.py +0 -0
  364. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_tests_scope.py +0 -0
  365. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_ts_fields.py +0 -0
  366. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_ts_fields_js_vue.py +0 -0
  367. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_ts_file_specs.py +0 -0
  368. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_ts_frameworks.py +0 -0
  369. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_ts_src_tsconfig.py +0 -0
  370. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_values.py +0 -0
  371. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_viz.py +0 -0
  372. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_vue_nav.py +0 -0
  373. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_watch.py +0 -0
  374. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_webhooks.py +0 -0
  375. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_websockets.py +0 -0
  376. {cg_code_graph-0.19.1 → cg_code_graph-0.21.0}/tests/test_workspace_link.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cg-code-graph
3
- Version: 0.19.1
3
+ Version: 0.21.0
4
4
  Summary: cg: a code graph of routes, calls, data access and platform boundaries for agents and humans (CLI + MCP server)
5
5
  Author: cyberchronos00
6
6
  License-Expression: MIT
@@ -33,11 +33,11 @@ Dynamic: license-file
33
33
 
34
34
  For you and your AI agent (CLI + MCP). No LLM in the graph.
35
35
 
36
- [![PyPI](https://img.shields.io/pypi/v/cg-code-graph)](https://pypi.org/project/cg-code-graph/) [![Downloads](https://img.shields.io/pepy/dt/cg-code-graph)](https://pepy.tech/project/cg-code-graph) [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-3776AB)](https://pypi.org/project/cg-code-graph/) [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE) [![Docs](https://img.shields.io/badge/docs-code--graph-93b600)](https://code-graph.cyberchronos00.workers.dev/)
36
+ [![PyPI](https://img.shields.io/pypi/v/cg-code-graph)](https://pypi.org/project/cg-code-graph/) [![Downloads](https://img.shields.io/pepy/dt/cg-code-graph)](https://pepy.tech/project/cg-code-graph) [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-3776AB)](https://pypi.org/project/cg-code-graph/) [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/cyberchronos00/code-graph/blob/main/LICENSE) [![Docs](https://img.shields.io/badge/docs-code--graph-93b600)](https://code-graph.cyberchronos00.workers.dev/)
37
37
 
38
- [Docs](https://code-graph.cyberchronos00.workers.dev/) · [Quick start](https://code-graph.cyberchronos00.workers.dev/docs/quickstart) · [CLI](https://code-graph.cyberchronos00.workers.dev/docs/cli) · [MCP](https://code-graph.cyberchronos00.workers.dev/docs/mcp) · [Changelog](CHANGELOG.md)
38
+ [Docs](https://code-graph.cyberchronos00.workers.dev/) · [Quick start](https://code-graph.cyberchronos00.workers.dev/docs/quickstart) · [CLI](https://code-graph.cyberchronos00.workers.dev/docs/cli) · [MCP](https://code-graph.cyberchronos00.workers.dev/docs/mcp) · [Changelog](https://github.com/cyberchronos00/code-graph/blob/main/CHANGELOG.md)
39
39
 
40
- [![cg visual view: the graph around the warehouse connection, a selected node with its evidence paths and source, then the planned-change overlay for the preorders plan](docs/media/cg-view-preview.gif)](docs/media/cg-view-demo.mp4)
40
+ [![cg visual view: the graph around the warehouse connection, a selected node with its evidence paths and source, then the planned-change overlay for the preorders plan](https://raw.githubusercontent.com/cyberchronos00/code-graph/main/docs/media/cg-view-preview.gif)](https://github.com/cyberchronos00/code-graph/blob/main/docs/media/cg-view-demo.mp4)
41
41
 
42
42
  </div>
43
43
 
@@ -67,7 +67,7 @@ dependents: 18 nodes (code: 6, entry points: 9, other: 3)
67
67
  -READS_TABLE[resolved @ catalog/tasks.py:14]-> table:store_books
68
68
  ```
69
69
 
70
- [![Terminal demo: cg indexes the bundled Laravel and Nuxt sample apps, links them, then runs reaches, path, impact and plan check](docs/media/cg-terminal-demo.gif)](docs/media/cg-terminal-demo.mp4)
70
+ [![Terminal demo: cg indexes the bundled Laravel and Nuxt sample apps, links them, then runs reaches, path, impact and plan check](https://raw.githubusercontent.com/cyberchronos00/code-graph/main/docs/media/cg-terminal-demo.gif)](https://github.com/cyberchronos00/code-graph/blob/main/docs/media/cg-terminal-demo.mp4)
71
71
 
72
72
  ## Supported languages
73
73
 
@@ -75,7 +75,7 @@ dependents: 18 nodes (code: 6, entry points: 9, other: 3)
75
75
  |---|---|---|---|
76
76
  | ![Python](https://img.shields.io/badge/Python-3776AB?logo=python&logoColor=white) | Django (+ DRF, django-ninja), FastAPI / Starlette, Flask, Celery | exact (stdlib `ast`) | [Python](https://code-graph.cyberchronos00.workers.dev/docs/python) |
77
77
  | ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white) ![JavaScript](https://img.shields.io/badge/JavaScript-F7DF1E?logo=javascript&logoColor=black) | Nuxt / Vue, NestJS, Next.js, Astro, Express, Fastify, Koa, Hono, Elysia, React Router / Remix | exact (TypeScript compiler API) | [TS frameworks](https://code-graph.cyberchronos00.workers.dev/docs/ts-frameworks) |
78
- | ![PHP](https://img.shields.io/badge/PHP-777BB4?logo=php&logoColor=white) | Laravel (routes, middleware, queues, broadcasting, outbound HTTP) | exact (nikic/php-parser; needs PHP 8.2+) | [PHP](https://code-graph.cyberchronos00.workers.dev/docs/php) |
78
+ | ![PHP](https://img.shields.io/badge/PHP-777BB4?logo=php&logoColor=white) | Laravel (routes, route attributes, middleware, queues, broadcasting, outbound HTTP) | exact (nikic/php-parser; needs PHP 8.2+) | [PHP](https://code-graph.cyberchronos00.workers.dev/docs/php) |
79
79
  | ![Dart](https://img.shields.io/badge/Dart-0175C2?logo=dart&logoColor=white) | Flutter | exact (Dart analyzer; needs the Dart SDK) | [Platforms](https://code-graph.cyberchronos00.workers.dev/docs/platforms) |
80
80
  | ![Kotlin](https://img.shields.io/badge/Kotlin-7F52FF?logo=kotlin&logoColor=white) | Android, Kotlin Multiplatform, Spring | heuristic; exact with scip-java (opt-in) | [Kotlin](https://code-graph.cyberchronos00.workers.dev/docs/kotlin) |
81
81
  | ![Java](https://img.shields.io/badge/Java-ED8B00?logo=openjdk&logoColor=white) | Spring (routes, guards, beans, tables, clients) | heuristic; exact with scip-java (opt-in) | [Java](https://code-graph.cyberchronos00.workers.dev/docs/java) |
@@ -88,8 +88,9 @@ Go and others: import a SCIP index with `cg index --scip FILE`. Run `cg doctor`
88
88
 
89
89
  - `reaches`, `impact`, `path`: every caller, route, job and page that reaches a table, config key or method.
90
90
  - `cg affected --base main`: the tests and entry points a change reaches (`--quiet` feeds your test runner).
91
- - Across repos: `cg link` matches client HTTP calls (frontends and Laravel / Guzzle backends) to server routes.
92
- - Route guards: `cg routes --writes --unguarded` lists routes that write with no auth guard.
91
+ - Across repos: `cg link` matches client HTTP calls (frontends and Laravel / Guzzle backends) to server routes, and checks request keys (Laravel FormRequest, Elysia schemas, Nuxt / TypeScript bodies).
92
+ - Route guards: `cg routes --writes --unguarded` lists routes that write with no auth guard. Laravel checks inside the action count as inline guards unless `--strict`.
93
+ - Security views: `cg surface` lists the inbound / outbound attack surface and findings (hardcoded credentials, plaintext protocols, unverified webhooks, unguarded handlers, listeners on all interfaces, disabled TLS / SSH verification, plaintext gRPC, exposed IPC, outbound URLs built from request input) as text, JSON or SARIF, with `--fail-on` for CI.
93
94
  - MCP server (`cg-mcp`) so agents query the graph instead of grepping.
94
95
  - Visual view (`cg serve`) and plan checks (`cg plan check`) for planned changes.
95
96
 
@@ -97,6 +98,6 @@ Go and others: import a SCIP index with `cg index --scip FILE`. Run `cg doctor`
97
98
 
98
99
  [Install](https://code-graph.cyberchronos00.workers.dev/docs/install) · [Quick start](https://code-graph.cyberchronos00.workers.dev/docs/quickstart) · [CLI](https://code-graph.cyberchronos00.workers.dev/docs/cli) · [MCP](https://code-graph.cyberchronos00.workers.dev/docs/mcp) · [Configuration](https://code-graph.cyberchronos00.workers.dev/docs/configuration) · [Architecture](https://code-graph.cyberchronos00.workers.dev/docs/architecture) · [Limitations](https://code-graph.cyberchronos00.workers.dev/docs/limitations)
99
100
 
100
- [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md) · MIT [License](LICENSE)
101
+ [Changelog](https://github.com/cyberchronos00/code-graph/blob/main/CHANGELOG.md) · [Contributing](https://github.com/cyberchronos00/code-graph/blob/main/CONTRIBUTING.md) · [Security](https://github.com/cyberchronos00/code-graph/blob/main/SECURITY.md) · MIT [License](https://github.com/cyberchronos00/code-graph/blob/main/LICENSE)
101
102
 
102
- More recordings: [docs/media](docs/media).
103
+ More recordings: [docs/media](https://github.com/cyberchronos00/code-graph/tree/main/docs/media).
@@ -6,11 +6,11 @@
6
6
 
7
7
  For you and your AI agent (CLI + MCP). No LLM in the graph.
8
8
 
9
- [![PyPI](https://img.shields.io/pypi/v/cg-code-graph)](https://pypi.org/project/cg-code-graph/) [![Downloads](https://img.shields.io/pepy/dt/cg-code-graph)](https://pepy.tech/project/cg-code-graph) [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-3776AB)](https://pypi.org/project/cg-code-graph/) [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE) [![Docs](https://img.shields.io/badge/docs-code--graph-93b600)](https://code-graph.cyberchronos00.workers.dev/)
9
+ [![PyPI](https://img.shields.io/pypi/v/cg-code-graph)](https://pypi.org/project/cg-code-graph/) [![Downloads](https://img.shields.io/pepy/dt/cg-code-graph)](https://pepy.tech/project/cg-code-graph) [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-3776AB)](https://pypi.org/project/cg-code-graph/) [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/cyberchronos00/code-graph/blob/main/LICENSE) [![Docs](https://img.shields.io/badge/docs-code--graph-93b600)](https://code-graph.cyberchronos00.workers.dev/)
10
10
 
11
- [Docs](https://code-graph.cyberchronos00.workers.dev/) · [Quick start](https://code-graph.cyberchronos00.workers.dev/docs/quickstart) · [CLI](https://code-graph.cyberchronos00.workers.dev/docs/cli) · [MCP](https://code-graph.cyberchronos00.workers.dev/docs/mcp) · [Changelog](CHANGELOG.md)
11
+ [Docs](https://code-graph.cyberchronos00.workers.dev/) · [Quick start](https://code-graph.cyberchronos00.workers.dev/docs/quickstart) · [CLI](https://code-graph.cyberchronos00.workers.dev/docs/cli) · [MCP](https://code-graph.cyberchronos00.workers.dev/docs/mcp) · [Changelog](https://github.com/cyberchronos00/code-graph/blob/main/CHANGELOG.md)
12
12
 
13
- [![cg visual view: the graph around the warehouse connection, a selected node with its evidence paths and source, then the planned-change overlay for the preorders plan](docs/media/cg-view-preview.gif)](docs/media/cg-view-demo.mp4)
13
+ [![cg visual view: the graph around the warehouse connection, a selected node with its evidence paths and source, then the planned-change overlay for the preorders plan](https://raw.githubusercontent.com/cyberchronos00/code-graph/main/docs/media/cg-view-preview.gif)](https://github.com/cyberchronos00/code-graph/blob/main/docs/media/cg-view-demo.mp4)
14
14
 
15
15
  </div>
16
16
 
@@ -40,7 +40,7 @@ dependents: 18 nodes (code: 6, entry points: 9, other: 3)
40
40
  -READS_TABLE[resolved @ catalog/tasks.py:14]-> table:store_books
41
41
  ```
42
42
 
43
- [![Terminal demo: cg indexes the bundled Laravel and Nuxt sample apps, links them, then runs reaches, path, impact and plan check](docs/media/cg-terminal-demo.gif)](docs/media/cg-terminal-demo.mp4)
43
+ [![Terminal demo: cg indexes the bundled Laravel and Nuxt sample apps, links them, then runs reaches, path, impact and plan check](https://raw.githubusercontent.com/cyberchronos00/code-graph/main/docs/media/cg-terminal-demo.gif)](https://github.com/cyberchronos00/code-graph/blob/main/docs/media/cg-terminal-demo.mp4)
44
44
 
45
45
  ## Supported languages
46
46
 
@@ -48,7 +48,7 @@ dependents: 18 nodes (code: 6, entry points: 9, other: 3)
48
48
  |---|---|---|---|
49
49
  | ![Python](https://img.shields.io/badge/Python-3776AB?logo=python&logoColor=white) | Django (+ DRF, django-ninja), FastAPI / Starlette, Flask, Celery | exact (stdlib `ast`) | [Python](https://code-graph.cyberchronos00.workers.dev/docs/python) |
50
50
  | ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white) ![JavaScript](https://img.shields.io/badge/JavaScript-F7DF1E?logo=javascript&logoColor=black) | Nuxt / Vue, NestJS, Next.js, Astro, Express, Fastify, Koa, Hono, Elysia, React Router / Remix | exact (TypeScript compiler API) | [TS frameworks](https://code-graph.cyberchronos00.workers.dev/docs/ts-frameworks) |
51
- | ![PHP](https://img.shields.io/badge/PHP-777BB4?logo=php&logoColor=white) | Laravel (routes, middleware, queues, broadcasting, outbound HTTP) | exact (nikic/php-parser; needs PHP 8.2+) | [PHP](https://code-graph.cyberchronos00.workers.dev/docs/php) |
51
+ | ![PHP](https://img.shields.io/badge/PHP-777BB4?logo=php&logoColor=white) | Laravel (routes, route attributes, middleware, queues, broadcasting, outbound HTTP) | exact (nikic/php-parser; needs PHP 8.2+) | [PHP](https://code-graph.cyberchronos00.workers.dev/docs/php) |
52
52
  | ![Dart](https://img.shields.io/badge/Dart-0175C2?logo=dart&logoColor=white) | Flutter | exact (Dart analyzer; needs the Dart SDK) | [Platforms](https://code-graph.cyberchronos00.workers.dev/docs/platforms) |
53
53
  | ![Kotlin](https://img.shields.io/badge/Kotlin-7F52FF?logo=kotlin&logoColor=white) | Android, Kotlin Multiplatform, Spring | heuristic; exact with scip-java (opt-in) | [Kotlin](https://code-graph.cyberchronos00.workers.dev/docs/kotlin) |
54
54
  | ![Java](https://img.shields.io/badge/Java-ED8B00?logo=openjdk&logoColor=white) | Spring (routes, guards, beans, tables, clients) | heuristic; exact with scip-java (opt-in) | [Java](https://code-graph.cyberchronos00.workers.dev/docs/java) |
@@ -61,8 +61,9 @@ Go and others: import a SCIP index with `cg index --scip FILE`. Run `cg doctor`
61
61
 
62
62
  - `reaches`, `impact`, `path`: every caller, route, job and page that reaches a table, config key or method.
63
63
  - `cg affected --base main`: the tests and entry points a change reaches (`--quiet` feeds your test runner).
64
- - Across repos: `cg link` matches client HTTP calls (frontends and Laravel / Guzzle backends) to server routes.
65
- - Route guards: `cg routes --writes --unguarded` lists routes that write with no auth guard.
64
+ - Across repos: `cg link` matches client HTTP calls (frontends and Laravel / Guzzle backends) to server routes, and checks request keys (Laravel FormRequest, Elysia schemas, Nuxt / TypeScript bodies).
65
+ - Route guards: `cg routes --writes --unguarded` lists routes that write with no auth guard. Laravel checks inside the action count as inline guards unless `--strict`.
66
+ - Security views: `cg surface` lists the inbound / outbound attack surface and findings (hardcoded credentials, plaintext protocols, unverified webhooks, unguarded handlers, listeners on all interfaces, disabled TLS / SSH verification, plaintext gRPC, exposed IPC, outbound URLs built from request input) as text, JSON or SARIF, with `--fail-on` for CI.
66
67
  - MCP server (`cg-mcp`) so agents query the graph instead of grepping.
67
68
  - Visual view (`cg serve`) and plan checks (`cg plan check`) for planned changes.
68
69
 
@@ -70,6 +71,6 @@ Go and others: import a SCIP index with `cg index --scip FILE`. Run `cg doctor`
70
71
 
71
72
  [Install](https://code-graph.cyberchronos00.workers.dev/docs/install) · [Quick start](https://code-graph.cyberchronos00.workers.dev/docs/quickstart) · [CLI](https://code-graph.cyberchronos00.workers.dev/docs/cli) · [MCP](https://code-graph.cyberchronos00.workers.dev/docs/mcp) · [Configuration](https://code-graph.cyberchronos00.workers.dev/docs/configuration) · [Architecture](https://code-graph.cyberchronos00.workers.dev/docs/architecture) · [Limitations](https://code-graph.cyberchronos00.workers.dev/docs/limitations)
72
73
 
73
- [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md) · MIT [License](LICENSE)
74
+ [Changelog](https://github.com/cyberchronos00/code-graph/blob/main/CHANGELOG.md) · [Contributing](https://github.com/cyberchronos00/code-graph/blob/main/CONTRIBUTING.md) · [Security](https://github.com/cyberchronos00/code-graph/blob/main/SECURITY.md) · MIT [License](https://github.com/cyberchronos00/code-graph/blob/main/LICENSE)
74
75
 
75
- More recordings: [docs/media](docs/media).
76
+ More recordings: [docs/media](https://github.com/cyberchronos00/code-graph/tree/main/docs/media).
@@ -1,2 +1,2 @@
1
1
  """code-graph: deterministic code knowledge graph (beta)."""
2
- __version__ = "0.19.1"
2
+ __version__ = "0.21.0"
@@ -35,6 +35,8 @@ KINDS = {
35
35
  "Nuxt imports.dirs entry that is not a literal path or glob"),
36
36
  "vue_unresolved_navigation": ("route", "typescript",
37
37
  "Nuxt / Vue / Astro navigation target that matches no page or several pages"),
38
+ "ts_unrecognised_http_client": ("endpoint", "typescript",
39
+ "outbound HTTP call through an imported client (http, https, undici, got, node-fetch, superagent, ...) that made no endpoint"),
38
40
  }
39
41
 
40
42
 
@@ -415,6 +417,110 @@ def nuxt_unevaluable_import_dirs(root: Path) -> dict | None:
415
417
  return _finding("nuxt_unevaluable_import_dirs", hits)
416
418
 
417
419
 
420
+ # ------------------------------------------------------------------------------------------- TS HTTP clients
421
+
422
+ HTTP_CLIENT_MODS = ("http", "https", "undici", "got", "node-fetch", "superagent", "needle", "phin", "request")
423
+ _HTTP_VERBS = {"request", "get", "post", "put", "patch", "delete", "del", "head", "options", "fetch", "stream"}
424
+ _HTTP_ONLY = {"request", "get"} # node:http / node:https: createServer, Agent, ... are not client calls
425
+ _HTTP_MOD_ALT = "|".join(re.escape(m) for m in HTTP_CLIENT_MODS)
426
+ _IMPORT_DEFAULT = re.compile(r"""import\s+(?:type\s+)?(\w+)\s*(?:,\s*\{([^}]*)\}\s*)?from\s*['"](?:node:)?(%s)['"]""" % _HTTP_MOD_ALT)
427
+ _IMPORT_NS = re.compile(r"""import\s+\*\s+as\s+(\w+)\s+from\s*['"](?:node:)?(%s)['"]""" % _HTTP_MOD_ALT)
428
+ _IMPORT_NAMED = re.compile(r"""import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"](?:node:)?(%s)['"]""" % _HTTP_MOD_ALT)
429
+ _REQ_BIND = re.compile(r"""(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?require\(\s*['"](?:node:)?(%s)['"]\s*\)""" % _HTTP_MOD_ALT)
430
+ _REQ_DESTR = re.compile(r"""(?:const|let|var)\s+\{([^}]*)\}\s*=\s*require\(\s*['"](?:node:)?(%s)['"]\s*\)""" % _HTTP_MOD_ALT)
431
+ _TEST_FILE = re.compile(r"(^|/)(tests?|__tests__|e2e|spec|specs|fixtures?)/|\.(test|spec)\.[cm]?[jt]sx?$")
432
+
433
+
434
+ def _named(spec: str) -> list[tuple[str, str]]:
435
+ out = []
436
+ for part in spec.split(","):
437
+ bits = [b for b in re.split(r"\s+as\s+|\s+", part.replace("type ", "").strip()) if b]
438
+ if bits:
439
+ out.append((bits[0], bits[-1]))
440
+ return out
441
+
442
+
443
+ def _http_client_calls(src: str) -> int | None:
444
+ """Offset of the first call through an imported HTTP client module in `src`, else None."""
445
+ binds: dict[str, str] = {} # name -> module (the module object itself)
446
+ named: dict[str, str] = {} # local name -> module (an imported client function)
447
+ for m in _IMPORT_DEFAULT.finditer(src):
448
+ binds[m.group(1)] = m.group(3)
449
+ for imp, loc in _named(m.group(2) or ""):
450
+ named[loc] = f"{m.group(3)}:{imp}"
451
+ for m in _IMPORT_NS.finditer(src):
452
+ binds[m.group(1)] = m.group(2)
453
+ for m in _IMPORT_NAMED.finditer(src):
454
+ for imp, loc in _named(m.group(1)):
455
+ named[loc] = f"{m.group(2)}:{imp}"
456
+ for m in _REQ_BIND.finditer(src):
457
+ binds[m.group(1)] = m.group(2)
458
+ for m in _REQ_DESTR.finditer(src):
459
+ for imp, loc in _named(m.group(1).replace(":", " as ")):
460
+ named[loc] = f"{m.group(2)}:{imp}"
461
+ best = None
462
+
463
+ def hit(pos):
464
+ nonlocal best
465
+ if best is None or pos < best:
466
+ best = pos
467
+
468
+ for name, mod in binds.items():
469
+ verbs = _HTTP_ONLY if mod in ("http", "https") else _HTTP_VERBS
470
+ if mod not in ("http", "https"):
471
+ m = re.search(r"(?<![\w.$])%s\s*\(" % re.escape(name), src) # got(url), request(opts), needle(...)
472
+ if m:
473
+ hit(m.start())
474
+ m = re.search(r"(?<![\w.$])%s\s*\.\s*(%s)\s*\(" % (re.escape(name), "|".join(sorted(verbs))), src)
475
+ if m:
476
+ hit(m.start())
477
+ for name, spec in named.items():
478
+ mod, _, imp = spec.partition(":")
479
+ ok = imp in (_HTTP_ONLY if mod in ("http", "https") else _HTTP_VERBS | {"Client", "Pool"})
480
+ if not ok:
481
+ continue
482
+ m = re.search(r"(?<![\w.$])(?:new\s+)?%s\s*\(" % re.escape(name), src)
483
+ if m:
484
+ hit(m.start())
485
+ return best
486
+
487
+
488
+ def _opaque_origin(origin: str | None) -> bool:
489
+ """An origin no host can be read from: a bare placeholder that is not a configured / env value, or a templated host."""
490
+ o = origin or ""
491
+ if not o:
492
+ return False
493
+ if o.startswith("{"):
494
+ return not o.startswith(("{env.", "{runtimeConfig."))
495
+ return "://" in o and "{" in o.split("://", 1)[1].split("/", 1)[0]
496
+
497
+
498
+ def ts_unrecognised_http_client(root: Path, files: list[str], builder) -> dict | None:
499
+ """Files that import an HTTP client module (http, https, undici, got, node-fetch, superagent, needle, phin, request)
500
+ and call it, with no client endpoint made there (a call cg could not follow, or only an origin it could not read)."""
501
+ if builder is None:
502
+ return None
503
+ made: dict[str, bool] = {}
504
+ for e in builder.edges.values():
505
+ if e.kind != "HTTP_CALLS" or not e.file:
506
+ continue
507
+ n = builder.nodes.get(e.dst)
508
+ if n is not None and not _opaque_origin((n.attrs or {}).get("origin")):
509
+ made[e.file] = True
510
+ hits = []
511
+ for f in files:
512
+ if made.get(f) or _TEST_FILE.search(f):
513
+ continue
514
+ src = _read(root, f)
515
+ if not src or not re.search(r"(?:from|require\s*\()\s*['\"](?:node:)?(?:%s)['\"]" % _HTTP_MOD_ALT, src):
516
+ continue
517
+ src = _strip_comments(src)
518
+ pos = _http_client_calls(src)
519
+ if pos is not None:
520
+ hits.append((f, _line(src, pos)))
521
+ return _finding("ts_unrecognised_http_client", hits)
522
+
523
+
418
524
  # ------------------------------------------------------------------------------------------- driver
419
525
 
420
526
  def detect(root: str | Path, files_by_ext: dict[str, list[str]], builder=None, programs: dict | None = None) -> list[dict]:
@@ -442,6 +548,7 @@ def detect(root: str | Path, files_by_ext: dict[str, list[str]], builder=None, p
442
548
  run(express_loop_routes, root, js)
443
549
  run(nuxt_unevaluable_import_dirs, root)
444
550
  run(vue_unresolved_navigation, builder)
551
+ run(ts_unrecognised_http_client, root, js, builder)
445
552
  run(laravel_loop_routes, root, php)
446
553
  py = programs.get("python")
447
554
  if py is not None:
@@ -171,7 +171,8 @@ def main(argv=None):
171
171
  p.add_argument("--writes", nargs="?", const="*", metavar="TABLE", help="routes reaching a DB write (any table, or TABLE)")
172
172
  p.add_argument("--reaches", nargs="+", metavar="SPEC", help="routes reaching any of these nodes (table, column, connection:, env:, Class.method)")
173
173
  p.add_argument("--missing", metavar="NAME", help="keep routes with no guard whose name contains NAME (e.g. auth:api, ApiKeyGuard)")
174
- p.add_argument("--unguarded", action="store_true", help="keep routes with no auth guard (framework presets, the auth name pattern, .cg.yaml auth.extra_patterns and --auth-pattern)")
174
+ p.add_argument("--unguarded", action="store_true", help="keep routes with no auth guard (framework presets, the auth name pattern, .cg.yaml auth.extra_patterns and --auth-pattern). An unconditional inline check (Laravel authorize / permission / shared secret) counts; --strict ignores it")
175
+ p.add_argument("--strict", action="store_true", help="with --unguarded, count only route-level guards (inline checks are still listed)")
175
176
  p.add_argument("--auth-pattern", help="extra regex for guard names that count as auth")
176
177
  p.add_argument("--min-confidence", default="heuristic", choices=["heuristic", "resolved", "exact"])
177
178
  p.add_argument("--max-items", type=int, default=200); p.add_argument("--no-paths", action="store_true"); p.add_argument("--json", action="store_true")
@@ -206,6 +207,23 @@ def main(argv=None):
206
207
  p.add_argument("--source", help="address or credential source: literal, env, env-example, compose, config")
207
208
  p.add_argument("--tls-off", action="store_true", help="only systems known to be reached without TLS")
208
209
  p.add_argument("--max-items", type=int, default=60)
210
+ p = sub.add_parser("surface", help="security views over recorded facts: inbound / outbound attack surface and findings "
211
+ "(hardcoded credentials, plaintext protocols, disabled TLS / SSH verification, unverified "
212
+ "webhooks, unguarded handlers, listeners on all interfaces, exposed IPC); text, JSON or "
213
+ "SARIF; --fail-on for CI gates")
214
+ p.add_argument("--db", required=True)
215
+ p.add_argument("--inbound", action="store_true", help="list the inbound surface (routes, webhooks, sockets, consumers, IPC)")
216
+ p.add_argument("--outbound", action="store_true", help="list the outbound surface (every external system)")
217
+ p.add_argument("--protocol", help="only this protocol (http, ws, grpc, tcp, redis, postgres, saas, ...)")
218
+ from .surface import finding_names
219
+ p.add_argument("--finding", help="only this finding: " + ", ".join(finding_names()))
220
+ p.add_argument("--min-confidence", choices=["heuristic", "resolved", "exact"], default="heuristic",
221
+ help="drop findings and items below this confidence (default: heuristic, keep all)")
222
+ p.add_argument("--format", choices=["text", "json", "sarif"], default="text")
223
+ p.add_argument("--fail-on", help="comma-separated finding types; exit 1 when any remains after ignores")
224
+ p.add_argument("--max-items", type=int, default=200)
225
+ p.add_argument("--strict", action="store_true", help="unguarded: count route guards only (an inline check does not protect)")
226
+ p.add_argument("--show-ignored", action="store_true", help="also list findings accepted by .cg.yaml surface.ignore")
209
227
  p = sub.add_parser("tools", help="LLM tools and MCP tools / resources / prompts: handler, tables it reaches, agents "
210
228
  "offering it, callers, checks; agents, dynamic dispatch, model calls")
211
229
  p.add_argument("pattern", nargs="?", help="tool name, substring or glob")
@@ -548,7 +566,7 @@ def main(argv=None):
548
566
  if a.cmd == "routes":
549
567
  from . import routes as R
550
568
  res = R.routes_report(st, writes=a.writes, reaches=a.reaches, missing=a.missing, unguarded=a.unguarded,
551
- auth_pattern=a.auth_pattern, min_conf=a.min_confidence, platform=a.platform)
569
+ strict=a.strict, auth_pattern=a.auth_pattern, min_conf=a.min_confidence, platform=a.platform)
552
570
  if a.json:
553
571
  res["completeness"] = R.route_completeness(st)
554
572
  print(json.dumps(res, indent=1, default=str) if a.json else R.render_routes(res, st, max_items=a.max_items, paths=not a.no_paths))
@@ -568,7 +586,14 @@ def main(argv=None):
568
586
  res = external(st, a.pattern, protocol=a.protocol, source=a.source, tls_off=a.tls_off,
569
587
  max_items=max(a.max_items, 200) if a.json else a.max_items)
570
588
  print(json.dumps(res, indent=1, default=str) if a.json else render_external(res, max_items=a.max_items))
589
+ if not a.json:
590
+ from .coverage import endpoint_note
591
+ n = endpoint_note(st)
592
+ if n:
593
+ print(n)
571
594
  return
595
+ if a.cmd == "surface":
596
+ return surface_cmd(a, st)
572
597
  if a.cmd == "tools":
573
598
  from .aitools import render_tools, tools
574
599
  res = tools(st, a.pattern, framework=a.framework, unmatched=a.unmatched, agent=a.agent,
@@ -720,6 +745,11 @@ def main(argv=None):
720
745
  elif a.cmd == "api-calls":
721
746
  rows = Q.api_calls(st, a.spec)
722
747
  print(json.dumps(rows, indent=1, default=str) if a.json else Q.render_api_calls(rows))
748
+ if not a.json:
749
+ from .coverage import endpoint_note
750
+ n = endpoint_note(st)
751
+ if n:
752
+ print(n)
723
753
  elif a.cmd == "stats":
724
754
  doc = stats_doc(st)
725
755
  print(json.dumps(doc, indent=1, default=str) if a.json else render_stats(st, doc))
@@ -823,6 +853,26 @@ def _note(comp: dict, as_json: bool) -> None:
823
853
  print(n)
824
854
 
825
855
 
856
+ def surface_cmd(a, st) -> int:
857
+ """`cg surface`: exit 0, or 1 when a --fail-on finding remains after ignores; 2 for a bad finding name."""
858
+ from . import surface as S
859
+ fail_on = [x.strip() for x in a.fail_on.split(",") if x.strip()] if a.fail_on else []
860
+ try:
861
+ res = S.surface(st, inbound=a.inbound, outbound=a.outbound, protocol=a.protocol, finding=a.finding,
862
+ min_confidence=a.min_confidence, fail_on=fail_on, max_items=a.max_items, strict=a.strict,
863
+ show_ignored=a.show_ignored)
864
+ except ValueError as ex:
865
+ print(f"cg surface: {ex}", file=sys.stderr)
866
+ return 2
867
+ if a.format == "json":
868
+ print(S.to_json(res))
869
+ elif a.format == "sarif":
870
+ print(json.dumps(S.to_sarif(res), indent=1))
871
+ else:
872
+ print(S.render_surface(res, max_items=a.max_items))
873
+ return 1 if res["failed"] else 0
874
+
875
+
826
876
  def config_cmd(a) -> int:
827
877
  from .config import ConfigError, effective, find, load_file, render_effective, unknown_key_warnings
828
878
  if a.action == "validate":
@@ -26,6 +26,11 @@ Records project-specific knowledge once, for the CLI, the MCP server and the vis
26
26
  protocols:
27
27
  external: ["kafka:audit.*", "http:GET /status"] # <protocol>:<name glob> handled outside the analysed repos
28
28
  # (not reported as no_receiver / no_sender by cg protocols)
29
+ surface:
30
+ ignore: # accepted risks of `cg surface` (reason is required)
31
+ - {finding: unguarded, path: "app/Http/Controllers/HealthController.php", reason: public health probe}
32
+ - {finding: plaintext, id: "external:redis:cache:6379", reason: compose-internal network}
33
+ - {finding: hardcoded, fingerprint: 3f9a1c0b7d2e4a55, reason: rotated test key}
29
34
  gates: config/gates.json # gate scenarios file (cg index --gates)
30
35
  plans:
31
36
  dir: docs/plans # plans directory (--plans-dir)
@@ -53,7 +58,7 @@ SCHEMA: dict[str, set | None] = { # top-level key -> allowed sub-keys (None:
53
58
  "frameworks": {"add", "remove"}, "auth": {"extra_patterns"}, "secret": {"extra_patterns"}, "gates": None,
54
59
  "plans": {"dir", "text_mention_dirs"}, "viz": {"presets"}, "generated": {"paths", "vendored", "keep", "include"},
55
60
  "platforms": {"targets", "paths", "file_suffixes", "path_conventions"}, "include": None, "apps": None,
56
- "rust": {"targets"}, "protocols": {"external"}, "lossy": None,
61
+ "rust": {"targets"}, "protocols": {"external"}, "lossy": None, "surface": {"ignore"},
57
62
  }
58
63
  APP_KEYS = ("name", "root", "role", "links")
59
64
  APP_ROLES = ("backend", "frontend")
@@ -298,6 +303,9 @@ def parse(data: Any, fname: str = ".cg.yaml") -> dict:
298
303
  if bad:
299
304
  raise ConfigError(f"{fname}: protocols.external: {bad[0]!r} is not <protocol>:<name glob> (e.g. kafka:audit.*)")
300
305
  out["protocols"] = {"external": ext}
306
+ sf = _section(data, "surface", fname)
307
+ if sf is not None and sf.get("ignore") is not None:
308
+ out["surface"] = {"ignore": _surface_ignores(sf["ignore"], f"{fname}: surface.ignore")}
301
309
  if data.get("gates") is not None:
302
310
  out["gates"] = norm_root(data["gates"], f"{fname}: gates")
303
311
  pl = _section(data, "plans", fname)
@@ -319,6 +327,38 @@ def parse(data: Any, fname: str = ".cg.yaml") -> dict:
319
327
  return out
320
328
 
321
329
 
330
+ SURFACE_IGNORE_KEYS = ("finding", "path", "id", "fingerprint", "reason")
331
+
332
+
333
+ def _surface_ignores(v: Any, where: str) -> list[dict]:
334
+ """`surface.ignore`: [{finding, path | id | fingerprint, reason}]; the reason is required."""
335
+ from .surface import FINDINGS
336
+ if not isinstance(v, list):
337
+ raise ConfigError(f"{where}: expected a list of {{finding, path | id | fingerprint, reason}} entries")
338
+ out = []
339
+ for i, e in enumerate(v):
340
+ w = f"{where}[{i}]"
341
+ if not isinstance(e, dict):
342
+ raise ConfigError(f"{w}: expected a mapping with finding, path | id | fingerprint and reason")
343
+ bad = sorted(str(k) for k in set(e) - set(SURFACE_IGNORE_KEYS))
344
+ if bad:
345
+ raise ConfigError(f"{w}: unknown key {bad[0]!r} (allowed: {', '.join(SURFACE_IGNORE_KEYS)})")
346
+ f = e.get("finding")
347
+ if f not in FINDINGS:
348
+ raise ConfigError(f"{w}: finding {f!r} is not one of {', '.join(FINDINGS)}")
349
+ reason = e.get("reason")
350
+ if not isinstance(reason, str) or not reason.strip():
351
+ raise ConfigError(f"{w}: reason is required (say why the risk is accepted)")
352
+ sel = {k: e[k] for k in ("path", "id", "fingerprint") if e.get(k) is not None}
353
+ if not sel:
354
+ raise ConfigError(f"{w}: give at least one of path, id or fingerprint")
355
+ for k, x in sel.items():
356
+ if not isinstance(x, str) or not x.strip():
357
+ raise ConfigError(f"{w}: {k} must be a non-empty string")
358
+ out.append({"finding": f, **{k: x.strip() for k, x in sel.items()}, "reason": reason.strip()})
359
+ return out
360
+
361
+
322
362
  def _apps(v: Any, fname: str) -> list[dict]:
323
363
  """apps as a list of {name, root, role, links} (or a mapping name -> {root, role, links}), validated: unique names,
324
364
  role backend / frontend, links naming backend apps. `root` may be relative, `../other-repo` or absolute.
@@ -516,7 +556,7 @@ def effective(root: str | Path, python_roots: list[str] | None = None, gates: st
516
556
  row(f"platforms.{k}", pc.get(k, True), f"{fname} platforms.{k}" if k in pc else f"built-in ({what})")
517
557
  row("auth.token_pattern", PR.values("common", "auth", "token_pattern"), "preset common")
518
558
  for section in ("auth", "secret"):
519
- for key in ("guards", "not_auth") if section == "auth" else ("guards",):
559
+ for key in ("guards", "not_auth", "session_only") if section == "auth" else ("guards",):
520
560
  for p in plan["presets"]:
521
561
  g = PR.values(p, section, key, default=None)
522
562
  if g:
@@ -72,6 +72,7 @@ EDGE_KINDS: dict[str, tuple[bool, str]] = {
72
72
  "READS_PROP": (True, "code reads a stored property (field node: Swift `var x`, `@State`, `@Published`)"),
73
73
  "WRITES_PROP": (True, "code writes a stored property (assignment, compound assignment)"),
74
74
  "USES_VALUE": (True, "code refers to an enum case, a constant, a static/global variable or an object-like macro"),
75
+ "REGISTERS_CALLBACK": (False, "function builds a callback URL for one of the app's own routes and registers it with an external party (attrs.body_key, endpoint)"),
75
76
  "REFERENCES_FN": (True, "code/data takes a function as a value (callback, dispatch table, handler registration)"),
76
77
  "USES_UNSAFE": (True, "code contains an unsafe block or is an unsafe fn (sink node unsafe:<crate>)"),
77
78
  "GATED_BY": (True, "code compiled only under a condition: Cargo feature, cfg predicate or preprocessor macro"),
@@ -59,6 +59,9 @@ class GraphBuilder:
59
59
  # Pigeon API definitions found by the Dart plugin ({Api: {kind: host | flutter, methods, file}}): bridges.py
60
60
  # links their native implementations / native callers
61
61
  self.pigeon_apis: dict = {}
62
+ # keyed-hash salt for the markers that stand in for secret values (cg_code_graph/core/redact.py); the indexer
63
+ # sets it from the graph DB before any plugin runs
64
+ self.redact_salt: str | None = None
62
65
 
63
66
  def add_node(self, kind: str, key: str, name: str | None = None, **kw) -> str:
64
67
  nid = node_id(kind, key)
@@ -0,0 +1,158 @@
1
+ """Secret values never reach the graph database (#47).
2
+
3
+ A literal that belongs to a credential-looking key (password, secret, token, key, api_key, auth, private, credential,
4
+ passphrase, dsn, ...) is stored as `redacted:hmac:<first 8 hex of HMAC-SHA-256(graph salt, value)>`: enough to see that
5
+ a literal exists and to tell two values apart within one graph, not enough to recover one, and not recognisable by
6
+ hashing a common password. The salt is `secrets.token_hex(16)`, created on the first index of a graph DB, kept in its
7
+ `meta` table (`redact_salt`) and carried over by a re-index or refresh of that DB; a new DB gets a new salt, so markers
8
+ of different graphs never compare. Graphs written before part 2d hold `redacted:sha256:<8 hex>` markers, which
9
+ `is_redacted` still accepts. A password inside a URL (`scheme://user:pw@host`) gets the same marker in place of the
10
+ password so the host stays readable. Placeholders (`${X}`, `%(X)s`, `<x>`, `null`) and
11
+ values that are already markers are left alone. Applied at index time by the plugins that keep literal config values
12
+ (Laravel config/*.php `value` and `env()` defaults) and as a last pass over every node / edge attr before the write.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import hmac
18
+ import re
19
+ import secrets
20
+
21
+ MARK = "redacted:hmac:"
22
+ LEGACY_MARK = "redacted:sha256:"
23
+ SECRET_WORDS = ("PASSWORD", "PASS", "PASSWD", "SECRET", "TOKEN", "SECRET_KEY", "SECRET_ACCESS_KEY", "KEY", "AUTH", "PWD")
24
+ _EXTRA_WORDS = {"PASSWORD", "PASSWD", "PWD", "PASSPHRASE", "SECRET", "SECRETS", "TOKEN", "TOKENS", "AUTH", "AUTHORIZATION",
25
+ "CREDENTIAL", "CREDENTIALS", "PRIVATE", "APIKEY", "DSN", "BEARER", "SALT", "SIGNATURE"}
26
+ _NOT_SECRET_TAIL = ("_KEY_ID", "_PUBLIC_KEY", "_KEY_PATH")
27
+ _URL_PW = re.compile(r"(?P<head>\b[A-Za-z][\w+.-]*://[^/\s:@'\"]*:)(?P<pw>[^@/\s'\"]+)(?P<tail>@)")
28
+ _PLACEHOLDER = ("${", "{{", "%(", "<", "$")
29
+
30
+
31
+ def secret_key(key: str) -> bool:
32
+ """The `cg surface` rule for a config key / env name that holds a credential."""
33
+ k = re.sub(r"[^A-Za-z0-9]+", "_", key).upper().strip("_")
34
+ return any(k.endswith("_" + s) or k == s for s in SECRET_WORDS) and not k.endswith(_NOT_SECRET_TAIL)
35
+
36
+
37
+ def looks_secret(key: str) -> bool:
38
+ """Wider than `secret_key`: any word of the name says credential (private_key_pem, db_dsn, apiKey, auth_header)."""
39
+ if secret_key(key):
40
+ return True
41
+ spaced = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", str(key))
42
+ words = [w for w in re.split(r"[^A-Za-z0-9]+", spaced.upper()) if w]
43
+ if any(w in _EXTRA_WORDS for w in words):
44
+ return True
45
+ return "KEY" in words and not set(words) & {"PUBLIC", "ID", "PATH", "FILE", "NAME", "PREFIX", "TYPE", "SIZE", "LENGTH"}
46
+
47
+
48
+ def is_redacted(v) -> bool:
49
+ return isinstance(v, str) and v.startswith((MARK, LEGACY_MARK))
50
+
51
+
52
+ def new_salt() -> str:
53
+ return secrets.token_hex(16)
54
+
55
+
56
+ def marker(value: str, salt: str) -> str:
57
+ """The stored stand-in for `value` in the graph that owns `salt`. There is no default salt on purpose: an unsalted
58
+ marker would be a plain hash of the value."""
59
+ if not salt:
60
+ raise ValueError("marker() needs the graph's redact salt")
61
+ return MARK + hmac.new(salt.encode(), value.encode("utf-8", "replace"), hashlib.sha256).hexdigest()[:8]
62
+
63
+
64
+ def placeholder(v) -> bool:
65
+ if not isinstance(v, str):
66
+ return True
67
+ t = v.strip()
68
+ return not t or t.startswith(_PLACEHOLDER) or t.lower() in ("null", "none", "false", "true", "changeme?") or is_redacted(t)
69
+
70
+
71
+ def redact_url(v: str, salt: str) -> str:
72
+ if "://" not in v or "@" not in v:
73
+ return v
74
+ return _URL_PW.sub(lambda m: m["head"] + (m["pw"] if placeholder(m["pw"]) else marker(m["pw"], salt)) + m["tail"], v)
75
+
76
+
77
+ def redact(value, key: str | None, salt: str):
78
+ """`value` as it may be stored: a marker for a literal under a credential-looking `key`, a URL password replaced
79
+ by one, anything else unchanged. Non-strings and placeholders pass through."""
80
+ if not isinstance(value, str):
81
+ return value
82
+ if key is not None and looks_secret(key) and not placeholder(value):
83
+ return marker(value, salt)
84
+ return redact_url(value, salt)
85
+
86
+
87
+ _VALUE_KEYS = {"value", "literal", "default", "env_default", "default_value"}
88
+ _NAME_KEYS = ("key", "name", "env", "env_key", "setting")
89
+ # Names that only ever carry a literal credential. `token`, `secret`, `credential(s)` are left out on purpose: plugins
90
+ # use them for identifiers (the Nest dependency-injection token on INJECTS edges, a secret's name, a credential source).
91
+ _DIRECT = {"password", "passwd", "pwd", "passphrase", "api_key", "apikey", "access_key", "secret_key", "client_secret",
92
+ "private_key", "auth_token"}
93
+ _REFERENCE = re.compile(r"^(?:env|config|file|const|literal)[: ]|^\$\{|^[\w./-]+:\d+$")
94
+ STRUCTURAL_EDGES = {"INJECTS", "BOUND_TO", "CREDENTIAL_FROM"}
95
+ # Attr maps keyed by source identifiers: an exported class called `ApiKey` is a name, not a literal under a credential key.
96
+ _IDENTIFIER_MAPS = {"reexports"}
97
+
98
+
99
+ def _holds_secret(d: dict, k: str) -> bool:
100
+ if k.lower() in _DIRECT:
101
+ return not _REFERENCE.match(str(d[k]))
102
+ if k in _VALUE_KEYS:
103
+ return any(isinstance(d.get(nk), str) and looks_secret(d[nk]) for nk in _NAME_KEYS)
104
+ return False
105
+
106
+
107
+ def sweep(builder, salt: str | None = None) -> int:
108
+ """Last pass over node / edge attrs; returns the count of replaced values. Replaces the password of a
109
+ `scheme://user:password@host` string, a literal under a credential-named attr (`password`, `token`, ...), and the
110
+ `value` / `default` of a `{key|name: <credential-looking>, value: ...}` record (TypeScript / Python config readers,
111
+ Laravel config). Booleans such as `credential_literal`, hosts and every other value stay as they are. The salt is
112
+ `salt`, else `builder.redact_salt` (set by the indexer from the graph DB)."""
113
+ salt = salt or getattr(builder, "redact_salt", None)
114
+ if not salt:
115
+ raise ValueError("sweep() needs the graph's redact salt")
116
+ n = 0
117
+
118
+ def walk(x, name=None):
119
+ nonlocal n
120
+ if name in _IDENTIFIER_MAPS:
121
+ return x
122
+ if isinstance(x, str):
123
+ if "@" in x and "://" in x:
124
+ y = redact_url(x, salt)
125
+ if y != x:
126
+ n += 1
127
+ return y
128
+ return x
129
+ if isinstance(x, dict):
130
+ for k, v in x.items():
131
+ if isinstance(v, str) and _holds_secret(x, k) and not placeholder(v):
132
+ x[k] = marker(v, salt)
133
+ n += 1
134
+ continue
135
+ nv = walk(v, k)
136
+ if nv is not v:
137
+ x[k] = nv
138
+ return x
139
+ if isinstance(x, list):
140
+ for i, v in enumerate(x):
141
+ nv = walk(v)
142
+ if nv is not v:
143
+ x[i] = nv
144
+ return x
145
+ return x
146
+ for node in builder.nodes.values():
147
+ doc = getattr(node, "doc", None)
148
+ if isinstance(doc, str) and "@" in doc and "://" in doc:
149
+ y = redact_url(doc, salt)
150
+ if y != doc:
151
+ node.doc = y
152
+ n += 1
153
+ if node.attrs:
154
+ walk(node.attrs)
155
+ for e in builder.edges.values():
156
+ if e.attrs and e.kind not in STRUCTURAL_EDGES:
157
+ walk(e.attrs)
158
+ return n