graphite-code 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. {graphite_code-0.3.0 → graphite_code-0.4.0}/CHANGELOG.md +78 -0
  2. {graphite_code-0.3.0 → graphite_code-0.4.0}/PKG-INFO +1 -1
  3. {graphite_code-0.3.0 → graphite_code-0.4.0}/RELEASING.md +46 -13
  4. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/__init__.py +1 -1
  5. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/doctor_probes.py +38 -4
  6. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/extract/ast.py +125 -1
  7. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/lifecycle_storage.py +22 -8
  8. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/storage.py +30 -12
  9. graphite_code-0.4.0/tests/data/sqloracle/blindspots.py.txt +206 -0
  10. graphite_code-0.4.0/tests/data/sqloracle/round69.py.txt +79 -0
  11. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_doctor.py +284 -7
  12. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_graph_provider_isolation.py +15 -7
  13. graphite_code-0.4.0/tests/test_method_dispatch_scope.py +290 -0
  14. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_probe_workspace.py +12 -1
  15. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_python_resolver.py +96 -1
  16. graphite_code-0.4.0/tests/test_sqloracle.py +284 -0
  17. graphite_code-0.4.0/tests/test_verify_published_release.py +409 -0
  18. {graphite_code-0.3.0 → graphite_code-0.4.0}/.gitignore +0 -0
  19. {graphite_code-0.3.0 → graphite_code-0.4.0}/ARCHITECTURE.md +0 -0
  20. {graphite_code-0.3.0 → graphite_code-0.4.0}/CONTRIBUTING.md +0 -0
  21. {graphite_code-0.3.0 → graphite_code-0.4.0}/LICENSE +0 -0
  22. {graphite_code-0.3.0 → graphite_code-0.4.0}/README.md +0 -0
  23. {graphite_code-0.3.0 → graphite_code-0.4.0}/pyproject.toml +0 -0
  24. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/__main__.py +0 -0
  25. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/_cleanup_worker.py +0 -0
  26. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/activation.py +0 -0
  27. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/agent_hooks.py +0 -0
  28. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/agent_settings.py +0 -0
  29. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/analyze.py +0 -0
  30. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/answer_contract.py +0 -0
  31. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/bootstrap.py +0 -0
  32. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/buildlock.py +0 -0
  33. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/cache.py +0 -0
  34. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/channel.py +0 -0
  35. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/cli.py +0 -0
  36. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/cluster.py +0 -0
  37. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/config.py +0 -0
  38. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/context.py +0 -0
  39. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/daemon.py +0 -0
  40. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/daemon_health.py +0 -0
  41. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/debt.py +0 -0
  42. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/dependency_install.py +0 -0
  43. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/detach.py +0 -0
  44. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/doctor.py +0 -0
  45. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/engine_identity.py +0 -0
  46. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/export/__init__.py +0 -0
  47. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/export/html.py +0 -0
  48. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/export/json.py +0 -0
  49. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/export/md.py +0 -0
  50. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/extract/__init__.py +0 -0
  51. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/freshness.py +0 -0
  52. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/git.py +0 -0
  53. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/graph.py +0 -0
  54. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/graph_io.py +0 -0
  55. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/health.py +0 -0
  56. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/hook_entry.py +0 -0
  57. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/hookinstall.py +0 -0
  58. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/hookshim.py +0 -0
  59. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/incident_ledger.py +0 -0
  60. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/ingest.py +0 -0
  61. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/init.py +0 -0
  62. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/io.py +0 -0
  63. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/listing.py +0 -0
  64. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/llm.py +0 -0
  65. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/llm_probe.py +0 -0
  66. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/mcp.py +0 -0
  67. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/mcp_server.py +0 -0
  68. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/natural_query.py +0 -0
  69. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/overlays.py +0 -0
  70. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/probe_process.py +0 -0
  71. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/probe_workspace.py +0 -0
  72. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/process_contracts.py +0 -0
  73. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/provider_observer.py +0 -0
  74. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/query.py +0 -0
  75. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/query_plan.py +0 -0
  76. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/replacement_audit.py +0 -0
  77. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/resolve.py +0 -0
  78. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/review.py +0 -0
  79. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/__init__.py +0 -0
  80. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/approval.py +0 -0
  81. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/classifier.py +0 -0
  82. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/claude_executor.py +0 -0
  83. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/claude_probe.py +0 -0
  84. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/cli_identity.py +0 -0
  85. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/codex_executor.py +0 -0
  86. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/codex_probe.py +0 -0
  87. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/context_builder.py +0 -0
  88. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/contracts.py +0 -0
  89. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/diff_policy.py +0 -0
  90. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/edit_apply.py +0 -0
  91. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/effort.py +0 -0
  92. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/lifecycle.py +0 -0
  93. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/lifecycle_operator.py +0 -0
  94. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/lifecycle_service.py +0 -0
  95. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/ollama_executor.py +0 -0
  96. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/ollama_probe.py +0 -0
  97. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/openrouter_executor.py +0 -0
  98. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/openrouter_probe.py +0 -0
  99. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/policy.py +0 -0
  100. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/probe_runner.py +0 -0
  101. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/process_runner.py +0 -0
  102. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/profiles.py +0 -0
  103. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/prompt.py +0 -0
  104. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/registry.py +0 -0
  105. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/route_pool.py +0 -0
  106. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/route_pool_execution.py +0 -0
  107. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/schema_validation.py +0 -0
  108. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/service.py +0 -0
  109. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/settings.py +0 -0
  110. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/shadow.py +0 -0
  111. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/telemetry.py +0 -0
  112. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/worktree.py +0 -0
  113. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/zai_edit.py +0 -0
  114. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/zai_executor.py +0 -0
  115. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/routing/zai_probe.py +0 -0
  116. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/savings.py +0 -0
  117. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/ts_bridge.py +0 -0
  118. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/ts_resolver.mjs +0 -0
  119. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/typescript_activation.py +0 -0
  120. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/usage_ledger.py +0 -0
  121. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/validation.py +0 -0
  122. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/watch.py +0 -0
  123. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/windows_job.py +0 -0
  124. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/windows_startup.py +0 -0
  125. {graphite_code-0.3.0 → graphite_code-0.4.0}/src/graphite/windows_task.py +0 -0
  126. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/conftest.py +0 -0
  127. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/fake_clis/fake_cli.py +0 -0
  128. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/fixtures/provider_lifecycle_schema_v1.sql +0 -0
  129. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/fixtures/routing_schema_v2_ca77600.sql +0 -0
  130. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/fixtures/routing_schema_v3_94eb333.sql +0 -0
  131. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/fixtures/routing_schema_v4_lifecycle_migration.sql +0 -0
  132. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_activation.py +0 -0
  133. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_activation_backstop.py +0 -0
  134. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_agent_hook_activation.py +0 -0
  135. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_agent_hooks.py +0 -0
  136. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_agent_settings.py +0 -0
  137. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_answer_contract.py +0 -0
  138. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_arrow_and_constructor_binding.py +0 -0
  139. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_bootstrap.py +0 -0
  140. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_build_detach.py +0 -0
  141. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_buildlock.py +0 -0
  142. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_cache_engine_identity.py +0 -0
  143. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_cache_file_set.py +0 -0
  144. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_cache_pruning.py +0 -0
  145. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_call_graph.py +0 -0
  146. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_channel_cli.py +0 -0
  147. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_channel_core.py +0 -0
  148. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_channel_lock.py +0 -0
  149. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_channel_mcp.py +0 -0
  150. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_channel_register.py +0 -0
  151. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_channel_report.py +0 -0
  152. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_channel_status.py +0 -0
  153. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_class_field_arrow_binding.py +0 -0
  154. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_cli_channel.py +0 -0
  155. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_cli_version.py +0 -0
  156. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_context.py +0 -0
  157. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_cwd_relative_config.py +0 -0
  158. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_daemon.py +0 -0
  159. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_daemon_activation.py +0 -0
  160. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_daemon_build_lock.py +0 -0
  161. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_daemon_engine_staleness.py +0 -0
  162. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_daemon_health.py +0 -0
  163. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_daemon_health_activation.py +0 -0
  164. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_debt.py +0 -0
  165. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_destructured_binding.py +0 -0
  166. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_determinism.py +0 -0
  167. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_doctor_agent_hooks.py +0 -0
  168. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_doctor_hooks.py +0 -0
  169. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_doctor_managed_docs.py +0 -0
  170. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_documentation.py +0 -0
  171. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_edit_apply.py +0 -0
  172. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_engine_identity.py +0 -0
  173. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_external_calls.py +0 -0
  174. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_git_security.py +0 -0
  175. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_go_rust.py +0 -0
  176. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_graph_io.py +0 -0
  177. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_hardening.py +0 -0
  178. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_health.py +0 -0
  179. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_hook_entry.py +0 -0
  180. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_hook_template.py +0 -0
  181. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_hookinstall.py +0 -0
  182. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_hookshim.py +0 -0
  183. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_html_security.py +0 -0
  184. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_incident_ledger.py +0 -0
  185. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_init.py +0 -0
  186. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_init_activation_doctrine.py +0 -0
  187. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_init_hooks.py +0 -0
  188. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_init_validation_exit.py +0 -0
  189. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_lifecycle_operator.py +0 -0
  190. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_listing.py +0 -0
  191. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_listing_surfaces.py +0 -0
  192. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_llm.py +0 -0
  193. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_mcp.py +0 -0
  194. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_monorepo.py +0 -0
  195. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_natural_query.py +0 -0
  196. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_overlays.py +0 -0
  197. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_path_leak_fixture.py +0 -0
  198. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_probe_diagnostics.py +0 -0
  199. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_claude_probe.py +0 -0
  200. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_codex_probe.py +0 -0
  201. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_lifecycle.py +0 -0
  202. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_lifecycle_service.py +0 -0
  203. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_lifecycle_storage.py +0 -0
  204. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_observer.py +0 -0
  205. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_ollama_probe.py +0 -0
  206. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_openrouter_probe.py +0 -0
  207. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_provider_probe_runner.py +0 -0
  208. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_public_surface.py +0 -0
  209. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_published_schemas.py +0 -0
  210. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_query_plan.py +0 -0
  211. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_reliability.py +0 -0
  212. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_replacement_audit.py +0 -0
  213. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_resolve.py +0 -0
  214. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_review.py +0 -0
  215. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_route_pool.py +0 -0
  216. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_approval.py +0 -0
  217. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_benchmark.py +0 -0
  218. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_classifier.py +0 -0
  219. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_claude_executor.py +0 -0
  220. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_cli.py +0 -0
  221. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_cli_contracts.py +0 -0
  222. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_cli_recovery.py +0 -0
  223. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_codex_executor.py +0 -0
  224. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_context.py +0 -0
  225. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_contracts.py +0 -0
  226. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_diff_policy.py +0 -0
  227. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_executor.py +0 -0
  228. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_openrouter_executor.py +0 -0
  229. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_policy.py +0 -0
  230. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_process_runner.py +0 -0
  231. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_profiles.py +0 -0
  232. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_registry.py +0 -0
  233. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_schema_validation.py +0 -0
  234. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_security.py +0 -0
  235. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_service.py +0 -0
  236. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_shadow.py +0 -0
  237. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_storage.py +0 -0
  238. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_telemetry.py +0 -0
  239. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_worktree.py +0 -0
  240. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_zai_executor.py +0 -0
  241. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_routing_zai_probe.py +0 -0
  242. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_savings.py +0 -0
  243. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_search.py +0 -0
  244. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_smoke.py +0 -0
  245. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_typescript_activation.py +0 -0
  246. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_typescript_resolver.py +0 -0
  247. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_usage_ledger.py +0 -0
  248. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_verify_artifact.py +0 -0
  249. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_watch.py +0 -0
  250. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_windows_startup.py +0 -0
  251. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_windows_task.py +0 -0
  252. {graphite_code-0.3.0 → graphite_code-0.4.0}/tests/test_zai_edit.py +0 -0
@@ -12,6 +12,84 @@ machine-checkable identity; the version is for humans.
12
12
 
13
13
  [Keep a Changelog]: https://keepachangelog.com/en/1.1.0/
14
14
 
15
+ ## [0.4.0] — 2026-08-14
16
+
17
+ **A minor bump because consumer graphs change, not because an API did.** Both fixes
18
+ below remove or add graph edges, so every consumer that rebuilds will see different
19
+ `callers`/`context` answers and a slightly different Python `calls` ratio. Nothing in
20
+ the CLI or the JSON schema changed. Under this project's pre-1.0 rule — minor versions
21
+ may break things — a patch number would have understated a release that silently
22
+ changes what the graph says.
23
+
24
+ ### Fixed
25
+
26
+ **Python method calls bound to any same-named definition in the repo, however
27
+ unreachable** (#54). `_resolve_method_dispatch` re-pointed `recv.method()` to any
28
+ `is_method` node sharing the name, justified in its own docstring by "method names
29
+ are almost always globally unique" — true of domain names, false of every name a
30
+ standard library already owns. On graphite's own graph `query "callers resolve"`
31
+ returned **232 callers graded `decision_grade`**, every one an ordinary
32
+ `pathlib.Path(...).resolve()` re-pointed to a test double's `resolve` in
33
+ `tests/test_git_security.py`; `src/graphite/io.py::atomic_write_text` was recorded
34
+ as calling a `write` defined in `tests/test_doctor.py`. The mis-bindings counted as
35
+ `bound`, so the health ratio rose as the defect got worse.
36
+
37
+ A Python call now dispatches only to a definition in the calling file or in a file
38
+ it directly imports. Filtering happens **before** the ambiguity cap, because
39
+ reachability is the disambiguation that cap was standing in for — so a common name
40
+ with one reachable definition now resolves instead of being abandoned as "too
41
+ generic to guess".
42
+
43
+ Measured on this repo: `callers resolve` 232 → 5, `callers get` 87 → 0,
44
+ `callers write` 30 → 0; `calls` edges 13498 → 12497 (−1428, +427); `calls.python`
45
+ 0.980 → 0.977. Every one of the 1428 dropped edges buckets as
46
+ `src|scripts|tests → tests` or `src → src`, and every sampled member was another
47
+ false binding (`sqlite3.connect`, `Thread.start`, a file handle's `write`).
48
+
49
+ **Expect consumer graphs to lose call edges and report a slightly lower Python
50
+ `calls` ratio.** That is honest accounting, not a regression — the same shape as the
51
+ constructor-edge change that moved some javascript cells down. Do not "fix" it back.
52
+
53
+ The gate resolves language through `LANGUAGE_BY_EXT`, the same table `collect_files`
54
+ uses to choose an extractor, so it cannot disagree with the walk that produced the
55
+ edge. Verified to reach a **warm** cache too: a graph built by the old code, rebuilt
56
+ in place by the new code, loses the false binding — the dispatch pass runs after
57
+ `cache.write`, so a stale partition cannot preserve it.
58
+
59
+ Scoped to **Python only, on evidence**. The gate's premise — "the caller does not
60
+ import the definer" standing in for "the caller cannot be holding one" — is never
61
+ exact in a duck-typed language. TypeScript/JavaScript stay ungated because
62
+ `test_member_call_ambiguous_small_set_links_to_all` pins the documented fan-out on
63
+ an `x: any` receiver with no import at all, a shape the gate would delete; Rust
64
+ because this repo has none to measure. Go could not be gated at all: its in-repo
65
+ imports target a synthesized package id that never equals any file's node id.
66
+ Residual: the same defect survives in ungated JS/TS.
67
+
68
+ **`import pkg.sub` emitted no import edge to `pkg/__init__.py`** (#55), though that
69
+ statement binds the name `pkg` and executes the package. Every dotted importer was
70
+ therefore invisible as a dependent of that `__init__.py` — the file where re-exports
71
+ and shared constants live, so a high-traffic edit target whose blast radius was
72
+ understated. Silently: a missing edge cannot lower the imports ratio, which is
73
+ computed over the edges that *were* emitted.
74
+
75
+ Found because the #54 gate refused `pkg.build()` and a test caught it. The gate
76
+ initially compensated with its own ancestor expansion; with the real edges emitted
77
+ that compensation became unfalsifiable — removing it changed no test — so it was
78
+ deleted rather than left looking like protection.
79
+ `test_a_dotted_import_reaches_the_package_it_binds` now guards the coupling.
80
+
81
+ Measured on this repo: import edges 1890 → 1920 (+30, −0), imports health 740/740 →
82
+ 770/770 with `ratio` 1.0 and `external` **unchanged** at 1148 — purely additive, no
83
+ reclassification. `impact src/graphite/routing/__init__.py` went from 1 likely test
84
+ to **8**.
85
+
86
+ Scoped to `import a.b.c`, not `from a.b import c`. Both execute the package, but only
87
+ the first binds `a`, so only the first can carry a `a.something()` call to attribute;
88
+ the from-import spelling has two existing specifications pinning one edge per module,
89
+ and expanding it would have added ~234 further edges here with no measured demand.
90
+ Ancestors are edged only when they resolve in-repo, so a PEP 420 namespace package
91
+ (no `__init__.py`) and an external `import os.path` both emit nothing extra.
92
+
15
93
  ## [0.3.0] — 2026-08-12
16
94
 
17
95
  **The first release consumers install as a built wheel.** Through 0.2.1 every
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: graphite-code
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Local-first, zero-LLM knowledge graph extraction for codebases.
5
5
  Project-URL: Homepage, https://github.com/jared0565/graphite
6
6
  Project-URL: Repository, https://github.com/jared0565/graphite
@@ -72,12 +72,12 @@ not a harmless duplicate: Hatchling refuses to build a field declared both stati
72
72
  and dynamically, so the next build aborts in `_get_version` rather than producing a
73
73
  mismatched artifact.
74
74
 
75
- The single source is deliberate. Every consumer here imports from one shared editable
76
- install, and `importlib.metadata` reports whatever was written into the installed
77
- metadata at install time so a version set only in `pyproject.toml` reaches a consumer
78
- only when someone reinstalls, which routine work never does. `graphite --version` reads
79
- `graphite.__version__` from the source tree for that reason, and reports a
80
- `stale-install` line when the installed metadata disagrees.
75
+ The single source is deliberate. `importlib.metadata` reports whatever was written into
76
+ the installed metadata at install time, so a version set only in `pyproject.toml` reaches
77
+ a consumer only when someone reinstalls. `graphite --version` reads
78
+ `graphite.__version__` for that reason, and reports a `stale-install` line when the
79
+ installed metadata disagrees which is how a development checkout and an installed
80
+ build are told apart at all.
81
81
 
82
82
  Review the complete diff and confirm no unrelated changes exist, and that
83
83
  `pyproject.toml` still declares the version dynamic:
@@ -275,8 +275,34 @@ git push origin vVERSION
275
275
 
276
276
  Never force push. Publication to PyPI or another index may use only a separately approved
277
277
  mechanism from a secure environment with scoped credentials. Never place tokens in
278
- command-line arguments, shell history, repository files, or logs. This repository and
279
- guide do not claim that any package index is configured.
278
+ command-line arguments, shell history, repository files, or logs.
279
+
280
+ **An index IS configured as of 2026-08-14: PyPI, distribution `graphite-code`.**
281
+ Publication runs from `.github/workflows/publish.yml` by Trusted Publishing (OIDC),
282
+ `workflow_dispatch` only. There is no API token in this repository, in any secret, or on
283
+ any maintainer machine — the credential is minted per run and expires in seconds, which
284
+ satisfies the paragraph above by having no long-lived secret to mishandle. The workflow
285
+ publishes the artifacts already attached to the GitHub Release and verifies both SHA256s
286
+ against literals pinned in the reviewed workflow; it never builds. Releasing a new version
287
+ therefore requires editing the approved version and both digests in that file and
288
+ committing the change, so digests pass through review rather than a dispatch form.
289
+
290
+ After publishing, verify from the INDEX rather than from this checkout:
291
+
292
+ ```text
293
+ python scripts/verify_published_release.py VERSION --wheel-sha256 <from EVIDENCE.md>
294
+ ```
295
+
296
+ Run it BEFORE publishing as well. Every arm must fail while the version does not exist —
297
+ that negative control is what makes the throwaway venv's isolation from this checkout a
298
+ measured fact instead of an assumption.
299
+
300
+ The digest is the load-bearing arm and the only one that may be omitted. Omitting it is
301
+ legal, and the run then reports `1 arm(s) never ran` and exits 0 having compared no bytes
302
+ — read the arm counts, not just `OVERALL`. An expected digest that is present but is not
303
+ 64 hex characters is a FAIL that says `malformed`, which is a different diagnosis from a
304
+ substituted wheel. `tests/test_verify_published_release.py` holds every arm to this,
305
+ including the ones that only fire on a bad release.
280
306
 
281
307
  ## Retention and rollback
282
308
 
@@ -289,11 +315,18 @@ when a rollback is wanted. The store lives at
289
315
  rather than as a literal: this file ships inside the sdist, so an absolute path
290
316
  here is the maintainer's layout published as if it were policy.
291
317
 
292
- This is not bookkeeping. Consumers import Graphite from a single editable
293
- install pointed at the development tree, so a saved file is live for all of them
294
- with no build and no boundary. Rollback means "reinstall the last known-good
295
- wheel", and that is possible only for a wheel somebody kept. A build directed at
296
- a scratch directory satisfies the gate above and leaves the gap open.
318
+ This is not bookkeeping. Rollback means "reinstall the last known-good wheel",
319
+ and that is possible only for a wheel somebody kept. A build directed at a
320
+ scratch directory satisfies the gate above and leaves the gap open.
321
+
322
+ Through 0.2.1 consumers imported Graphite from one shared editable install
323
+ pointed at the development tree, so a saved file was live for all of them with
324
+ no build and no boundary. **That stopped being true at 0.3.0**, which ships as a
325
+ built wheel: `graphite.__file__` resolves under `site-packages` and no editable
326
+ `.pth` survives. The retention rule did not weaken — it got sharper. A fix now
327
+ reaches nobody until a release is cut, so the store is the only record of what
328
+ consumers are actually running, and `index.md`'s "Currently deployed" section is
329
+ the only place that distinguishes it from what is on `main`.
297
330
 
298
331
  Rolling back, in an environment that already has the runtime dependencies:
299
332
 
@@ -38,4 +38,4 @@ __all__ = ["__version__"]
38
38
  #: Coarse and hand-maintained by construction. It is a release label, not
39
39
  #: evidence that a given fix is present -- to answer that, survey for the marker
40
40
  #: the fix introduced, or compare `graphite --version` fingerprints.
41
- __version__ = "0.3.0"
41
+ __version__ = "0.4.0"
@@ -10,7 +10,7 @@ import sys
10
10
  import tempfile
11
11
  import threading
12
12
  import time
13
- from collections.abc import Callable
13
+ from collections.abc import Callable, Mapping
14
14
  from concurrent.futures import ThreadPoolExecutor
15
15
  from functools import lru_cache
16
16
  from importlib import machinery, metadata
@@ -490,13 +490,36 @@ _TYPESCRIPT_REMEDIATION = (
490
490
  )
491
491
 
492
492
 
493
- def _blocked(error_type: str, code: str) -> DoctorCheck:
493
+ def _blocked(error_type: str, code: str, *, masked: Mapping[str, object] | None = None) -> DoctorCheck:
494
+ """Report one failure, optionally carrying the cause it displaced.
495
+
496
+ `masked` is for the one place a result legitimately overrides another: a
497
+ cleanup failure outranks the primary error, because leaked state is more
498
+ urgent than a reproducible one. Overriding it must not ERASE it -- reporting
499
+ only `cleanup_timeout` for a pipeline that exited nonzero sends an operator
500
+ to temp directories instead of to the build (#53).
501
+
502
+ Both carried values come from a fixed vocabulary produced by
503
+ `_process_error_type` and this module's own literals, so neither can carry a
504
+ path. That is why they are copied by NAME rather than by merging the
505
+ displaced details wholesale -- a future detail key holding a path would
506
+ otherwise be promoted into this report for free.
507
+ """
508
+ details: dict[str, object] = {"error_type": error_type, "code": code}
509
+ if masked is not None:
510
+ masked_type = masked.get("error_type")
511
+ masked_code = masked.get("code")
512
+ # Absent, never null: `masked_error_type: null` would invite a reader to
513
+ # treat "nothing was displaced" as "something was, and it was nothing".
514
+ if isinstance(masked_type, str) and isinstance(masked_code, str):
515
+ details["masked_error_type"] = masked_type
516
+ details["masked_code"] = masked_code
494
517
  return DoctorCheck(
495
518
  "deep_core",
496
519
  "Deterministic pipeline",
497
520
  "blocked",
498
521
  "The isolated deterministic pipeline probe failed safely.",
499
- {"error_type": error_type, "code": code},
522
+ details,
500
523
  ("Run the Graphite build, validate, and query commands locally and inspect their diagnostics.",),
501
524
  )
502
525
 
@@ -735,7 +758,18 @@ def probe_core_pipeline(
735
758
  if lease is not None:
736
759
  cleanup_error = _cleanup_workspace_lease(lease, deadline, _clock)
737
760
  if cleanup_error is not None:
738
- candidate = _blocked("cleanup", cleanup_error)
761
+ # Passed unconditionally: `_blocked` carries a cause only when
762
+ # one is actually present. A probe that SUCCEEDED and then failed
763
+ # to tear down has details with no `error_type`, so nothing is
764
+ # carried -- which is the outcome we want.
765
+ #
766
+ # An earlier version also tested `candidate.status == "blocked"`
767
+ # here. It was removed because it could not be shown to guard: it
768
+ # made `masked` unreachable for a ready candidate, which in turn
769
+ # made the type check inside `_blocked` unreachable, so each
770
+ # guard's only effect was to hide the other from every test.
771
+ displaced = candidate.details if candidate is not None else None
772
+ candidate = _blocked("cleanup", cleanup_error, masked=displaced)
739
773
  else:
740
774
  _release_core_probe_slot()
741
775
 
@@ -11,7 +11,7 @@ from typing import Any, Collection, Final
11
11
 
12
12
  from ..cache import Cache
13
13
  from ..config import Config
14
- from ..ingest import FileEntry
14
+ from ..ingest import LANGUAGE_BY_EXT, FileEntry
15
15
  from ..resolve import SourceIndex, should_keep_call_target
16
16
 
17
17
 
@@ -1096,6 +1096,37 @@ def _python_import_modules(node: Any) -> list[tuple[str, int]]:
1096
1096
  return out
1097
1097
 
1098
1098
 
1099
+ def _python_ancestor_packages(
1100
+ module: str, dots: int, rel_path: str, source_index: SourceIndex | None
1101
+ ) -> list[str]:
1102
+ """Resolved paths of the in-repo packages `import module` also executes (#55).
1103
+
1104
+ `import pkg.sub` imports `pkg` as well: Python executes `pkg/__init__.py` and
1105
+ binds the ROOT name in the importer, so `pkg.build()` afterwards is a call into
1106
+ the package. Recording only the deepest module made every dotted importer
1107
+ invisible as a dependent of that `__init__.py` -- and a package `__init__.py` is
1108
+ where re-exports and shared constants live, so `impact` understated the blast
1109
+ radius of a high-traffic edit target. Silently: a missing edge cannot lower the
1110
+ imports ratio, which is computed over the edges that WERE emitted.
1111
+
1112
+ Scoped to ancestors that resolve IN-REPO. `import os.path` gains nothing from a
1113
+ second edge to `os`: it would inflate the external count and move the imports
1114
+ ratio for a package that can never be a blast-radius target.
1115
+
1116
+ The counterpart for the other spelling is `_python_from_import_submodules`,
1117
+ added for the mirror-image defect in issue #7.
1118
+ """
1119
+ if source_index is None or "." not in module:
1120
+ return []
1121
+ segments = module.split(".")
1122
+ out: list[str] = []
1123
+ for depth in range(1, len(segments)):
1124
+ resolved = source_index.resolve_python_module(rel_path, ".".join(segments[:depth]), dots)
1125
+ if resolved:
1126
+ out.append(resolved)
1127
+ return out
1128
+
1129
+
1099
1130
  def _python_from_import_submodules(
1100
1131
  node: Any, rel_path: str, source_index: SourceIndex | None
1101
1132
  ) -> list[str]:
@@ -1326,6 +1357,19 @@ def _extract_python(file_id: str, rel_path: str, _source: bytes, tree: Any, sour
1326
1357
  file_id, _file_node_id(resolved), "imports", rel_path,
1327
1358
  _line(node), confidence="EXACT_IMPORT",
1328
1359
  ))
1360
+ # `import pkg.sub` also imports `pkg` (#55). Deliberately NOT
1361
+ # applied to `from pkg.sub import x`: that executes the package
1362
+ # too, but binds only `x`, so there is no `pkg.` call to
1363
+ # attribute -- and `test_from_import_symbol_only_edge_unchanged`
1364
+ # pins one edge per module for that spelling.
1365
+ for ancestor in (
1366
+ _python_ancestor_packages(module, dots, rel_path, source_index)
1367
+ if node.type == "import_statement" else ()
1368
+ ):
1369
+ result.edges.append(_edge(
1370
+ file_id, _file_node_id(ancestor), "imports", rel_path,
1371
+ _line(node), confidence="EXACT_IMPORT",
1372
+ ))
1329
1373
  else:
1330
1374
  result.edges.append(_edge(
1331
1375
  file_id, _make_id(module) if module else _make_id("package"),
@@ -1838,6 +1882,49 @@ def extract_all(entries: list[FileEntry], cfg: Config, cache: Cache | None = Non
1838
1882
  # edge is left untouched (pre-fix behavior).
1839
1883
  _MAX_METHOD_DISPATCH_CANDIDATES = 3
1840
1884
 
1885
+ # Languages where the reachability gate has been MEASURED. Deliberately just
1886
+ # Python -- see `_dispatch_is_gated` for why this is not simply "every language
1887
+ # whose imports bind to a file node".
1888
+ _DISPATCH_GATED_LANGUAGES = frozenset({"python"})
1889
+
1890
+
1891
+ def _dispatch_is_gated(source_file: str | None) -> bool:
1892
+ """Whether method dispatch from this file is restricted to reachable definitions.
1893
+
1894
+ Resolved through `LANGUAGE_BY_EXT` -- the same table `collect_files` uses to pick
1895
+ an extractor -- so the gate cannot disagree with the walk that produced the edge.
1896
+ A private suffix list here would agree with that table only by coincidence: `.py`
1897
+ happens to be the sole extension mapping to `python` today, and a `.pyw` or `.pyi`
1898
+ entry added there would silently escape a hardcoded list while still being
1899
+ extracted as Python.
1900
+
1901
+ FAILS OPEN, and the allowlist is narrower than the mechanism can support.
1902
+
1903
+ The gate rests on a proxy: "the caller does not import the definer" standing in
1904
+ for "the caller cannot be holding one of those". That proxy is never exact --
1905
+ every language here is duck-typed or structurally typed, so a function can be
1906
+ handed an instance of a class it never mentions. It is admitted for Python
1907
+ because it was MEASURED there (#54): on this repo it removed 1400+ edges whose
1908
+ every sampled member was a false binding, and added back ~350 real ones that the
1909
+ ambiguity cap had been abandoning.
1910
+
1911
+ TypeScript/JavaScript are NOT gated, on evidence rather than caution.
1912
+ `test_member_call_ambiguous_small_set_links_to_all` pins the documented
1913
+ small-ambiguous-set fan-out using `x: any` and no import at all -- a shape that
1914
+ is idiomatic in TS and that this gate would delete. Rust is ungated for the
1915
+ duller reason that this repo contains none, so nothing here could measure it.
1916
+ Extending the allowlist is a per-language measurement, not a one-line edit.
1917
+
1918
+ Go could not be gated even with evidence: its in-repo imports target a
1919
+ synthesized PACKAGE id (`example_com_repo_store`) that never equals the file
1920
+ node id of any file in that package, and files within one package see each
1921
+ other with no import statement at all.
1922
+ """
1923
+ if not source_file:
1924
+ return False
1925
+ language = LANGUAGE_BY_EXT.get(Path(source_file).suffix.casefold())
1926
+ return language in _DISPATCH_GATED_LANGUAGES
1927
+
1841
1928
 
1842
1929
  def _resolve_method_dispatch(
1843
1930
  nodes: list[dict[str, Any]], edges: list[dict[str, Any]]
@@ -1870,6 +1957,26 @@ def _resolve_method_dispatch(
1870
1957
  methods_by_name.setdefault(n["name"].casefold(), set()).add(n["id"])
1871
1958
  known_ids = {n["id"] for n in nodes}
1872
1959
 
1960
+ # Which file each definition lives in, and which files each file imports
1961
+ # DIRECTLY. Both are needed to answer "could this caller have reached that
1962
+ # definition at all" -- see the reachability filter below (#54).
1963
+ file_of_node: dict[str, str] = {
1964
+ n["id"]: _file_node_id(n["source_file"]) for n in nodes if n.get("source_file")
1965
+ }
1966
+ imports_by_file: dict[str, set[str]] = {}
1967
+ for e in edges:
1968
+ if e.get("relation") == "imports" and e.get("source") and e.get("target"):
1969
+ imports_by_file.setdefault(e["source"], set()).add(e["target"])
1970
+
1971
+ # This gate reads import edges literally, which is only sound while they are
1972
+ # complete. It once carried its own ancestor-package expansion, because
1973
+ # `import pkg.sub` emitted no edge to `pkg/__init__.py` and the gate would
1974
+ # otherwise refuse `pkg.build()` -- a call on the very name that statement
1975
+ # binds. #55 emits that edge, so the compensation became unfalsifiable (removing
1976
+ # it changed no test) and was deleted rather than left looking like protection.
1977
+ # `test_a_dotted_import_reaches_the_package_it_binds` is what now guards the
1978
+ # coupling: it fails if those edges ever stop being emitted.
1979
+
1873
1980
  out: list[dict[str, Any]] = []
1874
1981
  for e in edges:
1875
1982
  method = e.pop("_member", None) # strip from every edge, resolved or not
@@ -1877,6 +1984,23 @@ def _resolve_method_dispatch(
1877
1984
  out.append(e)
1878
1985
  continue
1879
1986
  candidates = methods_by_name.get(method.casefold())
1987
+ # Reachability gate (#54). A name-only match let `Path(...).resolve()` bind
1988
+ # to a test double's `resolve`, and `f.write(text)` to a `write` defined in
1989
+ # a test file -- 232 and 30 false callers respectively, graded
1990
+ # decision_grade, with the mis-bindings counted as `bound` so the health
1991
+ # ratio rose as the defect got worse. A definition the caller neither
1992
+ # declares nor imports cannot be the receiver's method, whatever its name.
1993
+ # Filtering BEFORE the cap is deliberate: reachability is exactly the
1994
+ # disambiguation the cap was standing in for, so a common name with one
1995
+ # reachable definition now resolves instead of being abandoned as
1996
+ # "too generic to guess".
1997
+ if candidates and _dispatch_is_gated(e.get("source_file")):
1998
+ caller_file = _file_node_id(e["source_file"])
1999
+ reachable = imports_by_file.get(caller_file, frozenset())
2000
+ candidates = {
2001
+ c for c in candidates
2002
+ if file_of_node.get(c) == caller_file or file_of_node.get(c) in reachable
2003
+ }
1880
2004
  if not candidates or len(candidates) > _MAX_METHOD_DISPATCH_CANDIDATES:
1881
2005
  # Member call that resolves to no known definition. Keep it when
1882
2006
  # the target is a real node, or when the call was already
@@ -266,7 +266,11 @@ class LifecycleStore:
266
266
  isolation_level=None,
267
267
  )
268
268
  connection.row_factory = sqlite3.Row
269
- connection.execute(f"PRAGMA busy_timeout = {self.busy_timeout_ms}")
269
+ # PRAGMA accepts no bind parameters, so this cannot be parameterized.
270
+ # busy_timeout_ms is an int validated to 100..30_000 at construction.
271
+ # Bare marker, not `nosemgrep: <id>`: semgrep namespaces rule ids by
272
+ # CONFIG PATH, so the real id embeds this machine's install location.
273
+ connection.execute(f"PRAGMA busy_timeout = {self.busy_timeout_ms}") # nosemgrep
270
274
  connection.execute("PRAGMA foreign_keys = ON")
271
275
  connection.execute("PRAGMA journal_mode = WAL")
272
276
  return connection
@@ -342,7 +346,11 @@ class LifecycleStore:
342
346
  isolation_level=None,
343
347
  )
344
348
  connection.row_factory = sqlite3.Row
345
- connection.execute(f"PRAGMA busy_timeout = {self.busy_timeout_ms}")
349
+ # PRAGMA accepts no bind parameters, so this cannot be parameterized.
350
+ # busy_timeout_ms is an int validated to 100..30_000 at construction.
351
+ # Bare marker, not `nosemgrep: <id>`: semgrep namespaces rule ids by
352
+ # CONFIG PATH, so the real id embeds this machine's install location.
353
+ connection.execute(f"PRAGMA busy_timeout = {self.busy_timeout_ms}") # nosemgrep
346
354
  connection.execute("PRAGMA foreign_keys = ON")
347
355
  connection.execute("PRAGMA query_only = ON")
348
356
  version = connection.execute(
@@ -440,6 +448,10 @@ class LifecycleStore:
440
448
  ),
441
449
  )
442
450
  try:
451
+ # The `# nosemgrep` markers below cover table IDENTIFIER interpolation.
452
+ # SQL binds values, never identifiers, so a migration that renames and
453
+ # rebuilds tables cannot use parameters. `table` is not caller input:
454
+ # `rebuilds` is the literal tuple defined immediately above.
443
455
  for table, create_sql, columns in rebuilds:
444
456
  rebuilt = create_sql.replace(
445
457
  f"CREATE TABLE IF NOT EXISTS {table} ",
@@ -449,17 +461,17 @@ class LifecycleStore:
449
461
  if rebuilt == create_sql:
450
462
  raise LifecycleStorageError("lifecycle_migration_failed")
451
463
  connection.execute(rebuilt)
452
- connection.execute(
464
+ connection.execute( # nosemgrep
453
465
  f"INSERT INTO {table}__v2 ({columns}) SELECT {columns} FROM {table}"
454
466
  )
455
- before = connection.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
456
- after = connection.execute(
467
+ before = connection.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0] # nosemgrep
468
+ after = connection.execute( # nosemgrep
457
469
  f"SELECT COUNT(*) FROM {table}__v2"
458
470
  ).fetchone()[0]
459
471
  if before != after:
460
472
  raise LifecycleStorageError("lifecycle_migration_quarantined")
461
- connection.execute(f"DROP TABLE {table}")
462
- connection.execute(f"ALTER TABLE {table}__v2 RENAME TO {table}")
473
+ connection.execute(f"DROP TABLE {table}") # nosemgrep
474
+ connection.execute(f"ALTER TABLE {table}__v2 RENAME TO {table}") # nosemgrep
463
475
  if connection.execute("PRAGMA foreign_key_check").fetchall():
464
476
  raise LifecycleStorageError("lifecycle_migration_quarantined")
465
477
  integrity = connection.execute("PRAGMA integrity_check").fetchone()
@@ -501,9 +513,11 @@ class LifecycleStore:
501
513
  }
502
514
  if not set(expected) <= actual:
503
515
  raise LifecycleStorageError("lifecycle_rollback_required")
516
+ # Identifier interpolation again, and `table` comes from the `expected`
517
+ # literal above -- never from a caller. PRAGMA takes no bind parameters.
504
518
  for table, required_columns in expected.items():
505
519
  columns = {
506
- str(row[1]) for row in connection.execute(f"PRAGMA table_info({table})")
520
+ str(row[1]) for row in connection.execute(f"PRAGMA table_info({table})") # nosemgrep
507
521
  }
508
522
  if not required_columns <= columns:
509
523
  raise LifecycleStorageError("lifecycle_rollback_required")
@@ -902,7 +902,11 @@ class RepositoryStore:
902
902
  isolation_level=None,
903
903
  )
904
904
  connection.row_factory = sqlite3.Row
905
- connection.execute(f"PRAGMA busy_timeout = {self.busy_timeout_ms}")
905
+ # PRAGMA accepts no bind parameters, so this cannot be parameterized.
906
+ # busy_timeout_ms is an int validated to 100..30_000 at construction.
907
+ # Bare marker, not `nosemgrep: <id>`: semgrep namespaces rule ids by
908
+ # CONFIG PATH, so the real id embeds this machine's install location.
909
+ connection.execute(f"PRAGMA busy_timeout = {self.busy_timeout_ms}") # nosemgrep
906
910
  connection.execute("PRAGMA foreign_keys = ON")
907
911
  connection.execute("PRAGMA journal_mode = WAL")
908
912
  return connection
@@ -1245,21 +1249,26 @@ class RepositoryStore:
1245
1249
  )
1246
1250
  connection.execute("PRAGMA foreign_keys=OFF")
1247
1251
  connection.execute("BEGIN IMMEDIATE")
1252
+ # The `# nosemgrep` markers below cover table IDENTIFIER interpolation.
1253
+ # SQL binds values, never identifiers, so a migration that copies and
1254
+ # rebuilds tables cannot use parameters. `table` is not caller input:
1255
+ # `_PRE_V6_WIDENED_TABLES` is a module-level `Final` mapping of two
1256
+ # literal table names.
1248
1257
  for table, (ddl, columns) in _PRE_V6_WIDENED_TABLES.items():
1249
- before = connection.execute(
1258
+ before = connection.execute( # nosemgrep
1250
1259
  f"SELECT COUNT(*) FROM {table}"
1251
1260
  ).fetchone()[0]
1252
- connection.execute(
1261
+ connection.execute( # nosemgrep
1253
1262
  f"CREATE TABLE {table}_pre_v6_copy AS SELECT * FROM {table}"
1254
1263
  )
1255
- connection.execute(f"DROP TABLE {table}")
1264
+ connection.execute(f"DROP TABLE {table}") # nosemgrep
1256
1265
  connection.execute(ddl)
1257
1266
  connection.execute(
1258
1267
  f"INSERT INTO {table} ({columns}) "
1259
1268
  f"SELECT {columns} FROM {table}_pre_v6_copy"
1260
1269
  )
1261
- connection.execute(f"DROP TABLE {table}_pre_v6_copy")
1262
- after = connection.execute(
1270
+ connection.execute(f"DROP TABLE {table}_pre_v6_copy") # nosemgrep
1271
+ after = connection.execute( # nosemgrep
1263
1272
  f"SELECT COUNT(*) FROM {table}"
1264
1273
  ).fetchone()[0]
1265
1274
  if before != after:
@@ -1318,21 +1327,26 @@ class RepositoryStore:
1318
1327
  )
1319
1328
  connection.execute("PRAGMA foreign_keys=OFF")
1320
1329
  connection.execute("BEGIN IMMEDIATE")
1330
+ # The `# nosemgrep` markers below cover table IDENTIFIER interpolation.
1331
+ # SQL binds values, never identifiers, so a migration that copies and
1332
+ # rebuilds tables cannot use parameters. `table` is not caller input:
1333
+ # `_PRE_V6_WIDENED_TABLES` is a module-level `Final` mapping of two
1334
+ # literal table names.
1321
1335
  for table, (ddl, columns) in _PRE_V6_WIDENED_TABLES.items():
1322
- before = connection.execute(
1336
+ before = connection.execute( # nosemgrep
1323
1337
  f"SELECT COUNT(*) FROM {table}"
1324
1338
  ).fetchone()[0]
1325
- connection.execute(
1339
+ connection.execute( # nosemgrep
1326
1340
  f"CREATE TABLE {table}_pre_v7_copy AS SELECT * FROM {table}"
1327
1341
  )
1328
- connection.execute(f"DROP TABLE {table}")
1342
+ connection.execute(f"DROP TABLE {table}") # nosemgrep
1329
1343
  connection.execute(ddl)
1330
1344
  connection.execute(
1331
1345
  f"INSERT INTO {table} ({columns}) "
1332
1346
  f"SELECT {columns} FROM {table}_pre_v7_copy"
1333
1347
  )
1334
- connection.execute(f"DROP TABLE {table}_pre_v7_copy")
1335
- after = connection.execute(
1348
+ connection.execute(f"DROP TABLE {table}_pre_v7_copy") # nosemgrep
1349
+ after = connection.execute( # nosemgrep
1336
1350
  f"SELECT COUNT(*) FROM {table}"
1337
1351
  ).fetchone()[0]
1338
1352
  if before != after:
@@ -3263,10 +3277,14 @@ class RepositoryStore:
3263
3277
  return tuple(dict(row) for row in rows)
3264
3278
 
3265
3279
  def row_count(self, table: str) -> int:
3280
+ # The one site here that DOES take a caller-supplied name, which is why the
3281
+ # allowlist below is load-bearing rather than decorative: `table` is
3282
+ # rejected unless it is one of the known table names, and only then reaches
3283
+ # the query. A table identifier cannot be passed as a bind parameter.
3266
3284
  if table not in _TABLES:
3267
3285
  raise ValueError("table_invalid")
3268
3286
  with self._connection() as connection:
3269
- return int(connection.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0])
3287
+ return int(connection.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]) # nosemgrep
3270
3288
 
3271
3289
  def save_capability_snapshot_record(self, snapshot: CapabilitySnapshot) -> bool:
3272
3290
  if not isinstance(snapshot, CapabilitySnapshot):