graphite-code 0.4.0__tar.gz → 0.5.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 (254) hide show
  1. {graphite_code-0.4.0 → graphite_code-0.5.0}/CHANGELOG.md +770 -664
  2. {graphite_code-0.4.0 → graphite_code-0.5.0}/PKG-INFO +1 -1
  3. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/__init__.py +41 -41
  4. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/extract/ast.py +206 -29
  5. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_arrow_and_constructor_binding.py +172 -172
  6. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_cache_file_set.py +110 -110
  7. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_call_graph.py +932 -932
  8. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_class_field_arrow_binding.py +130 -130
  9. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_destructured_binding.py +183 -183
  10. graphite_code-0.5.0/tests/test_dispatch_evidence.py +277 -0
  11. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_external_calls.py +548 -539
  12. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_go_rust.py +73 -32
  13. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_hardening.py +12 -12
  14. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_method_dispatch_scope.py +294 -290
  15. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_monorepo.py +199 -199
  16. graphite_code-0.5.0/tests/test_node_identity.py +139 -0
  17. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_python_resolver.py +22 -22
  18. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_typescript_resolver.py +274 -274
  19. {graphite_code-0.4.0 → graphite_code-0.5.0}/.gitignore +0 -0
  20. {graphite_code-0.4.0 → graphite_code-0.5.0}/ARCHITECTURE.md +0 -0
  21. {graphite_code-0.4.0 → graphite_code-0.5.0}/CONTRIBUTING.md +0 -0
  22. {graphite_code-0.4.0 → graphite_code-0.5.0}/LICENSE +0 -0
  23. {graphite_code-0.4.0 → graphite_code-0.5.0}/README.md +0 -0
  24. {graphite_code-0.4.0 → graphite_code-0.5.0}/RELEASING.md +0 -0
  25. {graphite_code-0.4.0 → graphite_code-0.5.0}/pyproject.toml +0 -0
  26. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/__main__.py +0 -0
  27. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/_cleanup_worker.py +0 -0
  28. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/activation.py +0 -0
  29. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/agent_hooks.py +0 -0
  30. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/agent_settings.py +0 -0
  31. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/analyze.py +0 -0
  32. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/answer_contract.py +0 -0
  33. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/bootstrap.py +0 -0
  34. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/buildlock.py +0 -0
  35. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/cache.py +0 -0
  36. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/channel.py +0 -0
  37. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/cli.py +0 -0
  38. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/cluster.py +0 -0
  39. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/config.py +0 -0
  40. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/context.py +0 -0
  41. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/daemon.py +0 -0
  42. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/daemon_health.py +0 -0
  43. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/debt.py +0 -0
  44. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/dependency_install.py +0 -0
  45. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/detach.py +0 -0
  46. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/doctor.py +0 -0
  47. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/doctor_probes.py +0 -0
  48. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/engine_identity.py +0 -0
  49. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/export/__init__.py +0 -0
  50. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/export/html.py +0 -0
  51. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/export/json.py +0 -0
  52. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/export/md.py +0 -0
  53. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/extract/__init__.py +0 -0
  54. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/freshness.py +0 -0
  55. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/git.py +0 -0
  56. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/graph.py +0 -0
  57. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/graph_io.py +0 -0
  58. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/health.py +0 -0
  59. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/hook_entry.py +0 -0
  60. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/hookinstall.py +0 -0
  61. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/hookshim.py +0 -0
  62. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/incident_ledger.py +0 -0
  63. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/ingest.py +0 -0
  64. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/init.py +0 -0
  65. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/io.py +0 -0
  66. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/listing.py +0 -0
  67. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/llm.py +0 -0
  68. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/llm_probe.py +0 -0
  69. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/mcp.py +0 -0
  70. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/mcp_server.py +0 -0
  71. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/natural_query.py +0 -0
  72. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/overlays.py +0 -0
  73. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/probe_process.py +0 -0
  74. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/probe_workspace.py +0 -0
  75. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/process_contracts.py +0 -0
  76. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/provider_observer.py +0 -0
  77. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/query.py +0 -0
  78. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/query_plan.py +0 -0
  79. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/replacement_audit.py +0 -0
  80. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/resolve.py +0 -0
  81. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/review.py +0 -0
  82. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/__init__.py +0 -0
  83. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/approval.py +0 -0
  84. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/classifier.py +0 -0
  85. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/claude_executor.py +0 -0
  86. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/claude_probe.py +0 -0
  87. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/cli_identity.py +0 -0
  88. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/codex_executor.py +0 -0
  89. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/codex_probe.py +0 -0
  90. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/context_builder.py +0 -0
  91. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/contracts.py +0 -0
  92. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/diff_policy.py +0 -0
  93. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/edit_apply.py +0 -0
  94. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/effort.py +0 -0
  95. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/lifecycle.py +0 -0
  96. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/lifecycle_operator.py +0 -0
  97. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/lifecycle_service.py +0 -0
  98. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/lifecycle_storage.py +0 -0
  99. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/ollama_executor.py +0 -0
  100. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/ollama_probe.py +0 -0
  101. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/openrouter_executor.py +0 -0
  102. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/openrouter_probe.py +0 -0
  103. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/policy.py +0 -0
  104. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/probe_runner.py +0 -0
  105. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/process_runner.py +0 -0
  106. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/profiles.py +0 -0
  107. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/prompt.py +0 -0
  108. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/registry.py +0 -0
  109. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/route_pool.py +0 -0
  110. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/route_pool_execution.py +0 -0
  111. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/schema_validation.py +0 -0
  112. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/service.py +0 -0
  113. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/settings.py +0 -0
  114. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/shadow.py +0 -0
  115. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/storage.py +0 -0
  116. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/telemetry.py +0 -0
  117. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/worktree.py +0 -0
  118. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/zai_edit.py +0 -0
  119. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/zai_executor.py +0 -0
  120. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/routing/zai_probe.py +0 -0
  121. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/savings.py +0 -0
  122. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/ts_bridge.py +0 -0
  123. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/ts_resolver.mjs +0 -0
  124. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/typescript_activation.py +0 -0
  125. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/usage_ledger.py +0 -0
  126. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/validation.py +0 -0
  127. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/watch.py +0 -0
  128. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/windows_job.py +0 -0
  129. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/windows_startup.py +0 -0
  130. {graphite_code-0.4.0 → graphite_code-0.5.0}/src/graphite/windows_task.py +0 -0
  131. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/conftest.py +0 -0
  132. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/data/sqloracle/blindspots.py.txt +0 -0
  133. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/data/sqloracle/round69.py.txt +0 -0
  134. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/fake_clis/fake_cli.py +0 -0
  135. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/fixtures/provider_lifecycle_schema_v1.sql +0 -0
  136. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/fixtures/routing_schema_v2_ca77600.sql +0 -0
  137. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/fixtures/routing_schema_v3_94eb333.sql +0 -0
  138. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/fixtures/routing_schema_v4_lifecycle_migration.sql +0 -0
  139. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_activation.py +0 -0
  140. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_activation_backstop.py +0 -0
  141. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_agent_hook_activation.py +0 -0
  142. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_agent_hooks.py +0 -0
  143. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_agent_settings.py +0 -0
  144. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_answer_contract.py +0 -0
  145. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_bootstrap.py +0 -0
  146. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_build_detach.py +0 -0
  147. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_buildlock.py +0 -0
  148. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_cache_engine_identity.py +0 -0
  149. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_cache_pruning.py +0 -0
  150. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_channel_cli.py +0 -0
  151. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_channel_core.py +0 -0
  152. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_channel_lock.py +0 -0
  153. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_channel_mcp.py +0 -0
  154. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_channel_register.py +0 -0
  155. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_channel_report.py +0 -0
  156. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_channel_status.py +0 -0
  157. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_cli_channel.py +0 -0
  158. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_cli_version.py +0 -0
  159. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_context.py +0 -0
  160. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_cwd_relative_config.py +0 -0
  161. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_daemon.py +0 -0
  162. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_daemon_activation.py +0 -0
  163. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_daemon_build_lock.py +0 -0
  164. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_daemon_engine_staleness.py +0 -0
  165. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_daemon_health.py +0 -0
  166. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_daemon_health_activation.py +0 -0
  167. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_debt.py +0 -0
  168. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_determinism.py +0 -0
  169. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_doctor.py +0 -0
  170. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_doctor_agent_hooks.py +0 -0
  171. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_doctor_hooks.py +0 -0
  172. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_doctor_managed_docs.py +0 -0
  173. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_documentation.py +0 -0
  174. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_edit_apply.py +0 -0
  175. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_engine_identity.py +0 -0
  176. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_git_security.py +0 -0
  177. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_graph_io.py +0 -0
  178. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_graph_provider_isolation.py +0 -0
  179. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_health.py +0 -0
  180. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_hook_entry.py +0 -0
  181. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_hook_template.py +0 -0
  182. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_hookinstall.py +0 -0
  183. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_hookshim.py +0 -0
  184. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_html_security.py +0 -0
  185. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_incident_ledger.py +0 -0
  186. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_init.py +0 -0
  187. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_init_activation_doctrine.py +0 -0
  188. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_init_hooks.py +0 -0
  189. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_init_validation_exit.py +0 -0
  190. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_lifecycle_operator.py +0 -0
  191. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_listing.py +0 -0
  192. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_listing_surfaces.py +0 -0
  193. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_llm.py +0 -0
  194. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_mcp.py +0 -0
  195. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_natural_query.py +0 -0
  196. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_overlays.py +0 -0
  197. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_path_leak_fixture.py +0 -0
  198. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_probe_diagnostics.py +0 -0
  199. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_probe_workspace.py +0 -0
  200. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_provider_claude_probe.py +0 -0
  201. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_provider_codex_probe.py +0 -0
  202. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_provider_lifecycle.py +0 -0
  203. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_provider_lifecycle_service.py +0 -0
  204. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_provider_lifecycle_storage.py +0 -0
  205. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_provider_observer.py +0 -0
  206. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_provider_ollama_probe.py +0 -0
  207. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_provider_openrouter_probe.py +0 -0
  208. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_provider_probe_runner.py +0 -0
  209. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_public_surface.py +0 -0
  210. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_published_schemas.py +0 -0
  211. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_query_plan.py +0 -0
  212. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_reliability.py +0 -0
  213. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_replacement_audit.py +0 -0
  214. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_resolve.py +0 -0
  215. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_review.py +0 -0
  216. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_route_pool.py +0 -0
  217. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_approval.py +0 -0
  218. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_benchmark.py +0 -0
  219. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_classifier.py +0 -0
  220. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_claude_executor.py +0 -0
  221. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_cli.py +0 -0
  222. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_cli_contracts.py +0 -0
  223. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_cli_recovery.py +0 -0
  224. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_codex_executor.py +0 -0
  225. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_context.py +0 -0
  226. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_contracts.py +0 -0
  227. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_diff_policy.py +0 -0
  228. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_executor.py +0 -0
  229. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_openrouter_executor.py +0 -0
  230. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_policy.py +0 -0
  231. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_process_runner.py +0 -0
  232. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_profiles.py +0 -0
  233. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_registry.py +0 -0
  234. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_schema_validation.py +0 -0
  235. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_security.py +0 -0
  236. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_service.py +0 -0
  237. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_shadow.py +0 -0
  238. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_storage.py +0 -0
  239. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_telemetry.py +0 -0
  240. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_worktree.py +0 -0
  241. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_zai_executor.py +0 -0
  242. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_routing_zai_probe.py +0 -0
  243. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_savings.py +0 -0
  244. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_search.py +0 -0
  245. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_smoke.py +0 -0
  246. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_sqloracle.py +0 -0
  247. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_typescript_activation.py +0 -0
  248. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_usage_ledger.py +0 -0
  249. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_verify_artifact.py +0 -0
  250. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_verify_published_release.py +0 -0
  251. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_watch.py +0 -0
  252. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_windows_startup.py +0 -0
  253. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_windows_task.py +0 -0
  254. {graphite_code-0.4.0 → graphite_code-0.5.0}/tests/test_zai_edit.py +0 -0
@@ -1,664 +1,770 @@
1
- # Changelog
2
-
3
- Notable changes to graphite. Format follows [Keep a Changelog]; versioning is
4
- semver, pre-1.0 (minor versions may break things).
5
-
6
- **A version number here is a coarse release label, not a fix marker.** Several
7
- behavioural fixes have shipped with no version change at all — the `-P`
8
- agent-hook fix went out under an unchanged `DOC_VERSION`. To answer "does this
9
- install have fix X", survey for the marker X introduced, or compare
10
- `graphite --version` fingerprints between installs. The fingerprint is the
11
- machine-checkable identity; the version is for humans.
12
-
13
- [Keep a Changelog]: https://keepachangelog.com/en/1.1.0/
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
-
93
- ## [0.3.0]2026-08-12
94
-
95
- **The first release consumers install as a built wheel.** Through 0.2.1 every
96
- consumer on the development machine imported graphite from one shared editable
97
- install pointed at the source tree, so a saved file was live everywhere with no
98
- build, no review and no version boundary. That is why `__version__` could not
99
- distinguish two states of the tree, and why "which fix does this install have"
100
- was answerable only by surveying for markers. A wheel install writes source and
101
- metadata together, so from 0.3.0 the version means something again.
102
-
103
- ### Fixed
104
-
105
- **Daemon discovery only worked on one machine, and the wheel shipped that path.**
106
- `_default_daemon_base` compared each parent directory against a single hardcoded
107
- absolute path the maintainer's own layout, present since the initial commit.
108
- Two separate defects in one line. Functionally, discovery succeeded on exactly
109
- one machine and silently fell back to the project root everywhere else, so
110
- `daemon-health` and `bootstrap` reported no daemon for every other user.
111
- Distribution-wise, the wheel carried an absolute developer path, which
112
- `RELEASING.md` forbids in a release archive.
113
-
114
- The value is only ever used to reach `base/.graphite-daemon/status.json`, so it
115
- now searches upward for that marker instead of matching a name. Behaviour is
116
- unchanged where the old literal applied, and correct everywhere else.
117
- `GRAPHITE_PROJECTS_ROOT` still takes precedence, and the directory name is now a
118
- single named constant shared with the reader of that file.
119
-
120
- Worth recording *why this survived two releases*: 0.2.0 and 0.2.1 both ran a
121
- disclosure scan and both recorded zero developer-path hits, because both searched
122
- the **compressed** archive bytes, where a plaintext pattern cannot match. A clean
123
- tree and a leaking one were indistinguishable the scan could not fail. The
124
- 0.2.1 wheel in fact shipped this path in three modules and twelve times in
125
- `METADATA`. `tests/test_hardening.py` now scans the packaged sources directly,
126
- docstrings included, and is mutation-proven against the original line.
127
-
128
- **The engine fingerprint could not see the parsers (#52).** `engine_identity`
129
- digested `cache_version`, `schema_version`, `graphite.__version__` and the files
130
- under `src/graphite/` — everything except the tree-sitter grammars that actually
131
- produce the ASTs. Upgrading `tree-sitter-python` therefore changed every
132
- extracted graph while the fingerprint stayed byte-identical.
133
-
134
- Three mechanisms this repo deliberately built were defeated by that. The
135
- extraction cache partitions on `{cache_version}-{engine[:16]}` (#21), so a
136
- grammar upgrade served parses produced by the OLD parser precisely the
137
- staleness partitioning was introduced to prevent. The daemon queues a rebuild
138
- when a project's recorded engine identity differs from the current one (#18), and
139
- a grammar upgrade produced no difference, so supervised repos kept graphs that
140
- disagreed with their source. And `metadata.engine.fingerprint` is documented as
141
- the engine that produced the content, which across a parser change it was not.
142
-
143
- Not hypothetical: every runtime dependency is lower-bound only, and the installed
144
- `tree-sitter` was already 0.25.2 against a declared floor of `>=0.23`.
145
-
146
- `engine_identity` now folds a `parsers` record into the digest, and
147
- `metadata.engine` reports it, so a reader can see *which* grammars produced a
148
- graph rather than only that something moved:
149
-
150
- ```
151
- "parsers": "tree-sitter=0.25.2,tree-sitter-go=0.25.0,..."
152
- ```
153
-
154
- An absent grammar is recorded as `absent` rather than omitted — "go was
155
- installed" and "go was not" are different engines and must not share a
156
- fingerprint and a missing grammar degrades that one language without making
157
- identity itself fail. `ENGINE_SCHEMA_VERSION` is now `2`: a fingerprint computed
158
- under schema 1 covered graphite's own files only, so the two are not comparable.
159
-
160
- **Expect a one-time rebuild everywhere.** Every fingerprint changes, so the first
161
- run after upgrading re-extracts and re-partitions its cache. That is the correct
162
- signal, not a side effect.
163
-
164
- **A transport failure now says what it observed (#51).** The deep MCP probe's
165
- diagnostic read every field off a `ProbeProcessResult`, and a transport failure
166
- never produces one `run_bounded_process` raises instead of returning. So on a
167
- timeout, the one failure the diagnostic exists to explain, it printed `<none>`
168
- for every field. A real CI sighting was consequently uninterpretable.
169
-
170
- `ProbeProcessError` now carries `elapsed_seconds`, `budget_seconds`,
171
- `stdout_bytes` and `stderr_bytes`. Counts and timings only: the error type is
172
- contractually free of process data, and tests pin that a child's output cannot
173
- reach `str(exc)`. Numbers are safe to carry there for the same reason
174
- `os_error` already was.
175
-
176
- Read `elapsed_s` against `budget_s`. At or below budget, the deadline fired on
177
- time and the child did not answer within it note a normal timeout lands
178
- *below* budget, because the runner reserves up to 40% of it for cleanup and
179
- enforces the earlier execution deadline. **Above** budget means our own deadline
180
- was late, i.e. the process was starved of CPU — the load hypothesis, which
181
- nothing in the log could previously express. `stdout_bytes` splits the first
182
- case: zero means the child never produced a byte, non-zero means it was alive
183
- and progressing.
184
-
185
- Live output, same probe, two failures:
186
-
187
- deep_mcp output_limit: elapsed_s=0.170 | budget_s=0.35 | stdout_bytes=1048577
188
- deep_mcp timeout: elapsed_s=0.325 | budget_s=0.35 | stdout_bytes=0
189
-
190
- ### Added
191
-
192
- **`graphite debt` reports declared blind spots and their age.** A blind spot that
193
- is DECLARED is working as designed; an undeclared one is the failure. The command
194
- prints open and retired entries with time-to-retire, so the count is auditable
195
- rather than a claim. Retirement is recorded against the fix that earned it.
196
-
197
- **The public API surface is declared.** `docs/` now states what consumers may
198
- depend on and what they may not. Six repos import this package from one install,
199
- so "it happened to work" was the only contract they had; anything not listed is
200
- explicitly not a promise.
201
-
202
- **Release verification targets the BUILT DISTRIBUTION, not the source tree.**
203
- Checks that passed against the working tree could not see what the wheel actually
204
- contained the two differ precisely where packaging bugs live.
205
-
206
- **CommonJS is modelled (#49).** `require('<literal>')` now emits a real
207
- `imports` edge resolved in-repo, or `EXTERNAL_IMPORT` for a bare package,
208
- exactly as the ESM equivalent does. Four call shapes that previously landed on
209
- a same-file phantom now bind to their definition:
210
-
211
- | shape | before | after |
212
- |---|---|---|
213
- | `const { f } = require('./x')` `f()` | phantom | binds |
214
- | `const m = require('./x')` → `m.f()` | phantom | binds |
215
- | `module.exports.f = f` `m.f()` | phantom | binds |
216
- | `import * as ns from './x'` `ns.f()` | phantom | binds |
217
-
218
- `_ImportBindings` gained `namespaces`, mapping a whole-module local name to the
219
- file it stands for, and `_resolve_call` turns `m.f()` into that file's `f`.
220
- This is a mirror of what Python's `alias_map` has always done for `import x` +
221
- `x.attr()`, not a new design. Detection of the `require()` shape lives in one
222
- predicate used by both the binding collector and the edge-emitting walk, so the
223
- two cannot drift.
224
-
225
- Measured on a four-file fixture, JavaScript and TypeScript both: calls
226
- **1/2 5/5**, imports **1/1 2/2**, placeholder share **0.143 0.077** as the
227
- `m.f` phantoms stopped being invented, and the graph moved unhealthy → healthy.
228
- Note the calls denominator *grew* while the ratio rose newly bound sites are
229
- sites that were previously never counted. `imported-by src/mod.js` now answers
230
- `consumer.js` at `decision_grade` instead of answering nothing at that grade.
231
-
232
- One interaction worth recording: `_resolve_method_dispatch` re-points any call
233
- edge carrying `_member` by method NAME alone, so a namespace-resolved `m.f()`
234
- would have been stolen back by any same-named class method elsewhere. The walk
235
- now omits `_member` when the namespace map resolved the target — the post-pass
236
- exists for edges that are "only a file-scoped phantom", which these no longer
237
- are.
238
-
239
- Guarded against a false positive the fix itself introduced. The binding maps are
240
- file-level while calls are walked per scope, so an inner `const m = ...`, a
241
- parameter named `m`, or a second destructure of the same name is
242
- indistinguishable from the module binding at resolution timeand `m.real()`
243
- would have claimed the module's definition, putting a caller in `callers real`
244
- that does not exist. A wrong edge is worse than a missing one.
245
- `_rebound_local_names` distrusts any require-bound name that is bound more than
246
- once anywhere in the file: deliberately blunt rather than modelling JavaScript
247
- scope, because it **fails closed**, giving up an edge instead of inventing one.
248
- Measured cost — in a file that rebinds the name, every `m.x()` in it loses
249
- binding, which is exactly the pre-#49 behaviour for that file and no worse.
250
- Applied only to names CommonJS introduced; ESM binding forms are
251
- statement-level and were never re-derived from a declarator.
252
-
253
- Also measured and correct without change: a module-object call to a member the
254
- module does not export (`m.notExported()`) produces an unbound placeholder and
255
- LOWERS the ratio rather than fabricating a binding.
256
-
257
- Three caveats retired on re-measured evidence — `ts-destructured-locals-unbound`
258
- (declared 2026-07-27), `js-require-emits-no-import-edge` and
259
- `js-module-object-calls-unbound` (both declared that morning). One added:
260
- `js-dynamic-module-load-unmodelled`, because `require(expr)` and `import()`
261
- expressions still emit nothing, measured the same day note the second has a
262
- string literal, so "non-literal argument" is not the test; `import()` is an
263
- expression rather than an import statement. And one more,
264
- `js-shadowed-module-local-unbound`, for the shadowing subset above. **Both
265
- non-detection classes are narrowed, not gone**, so `imports` stays in
266
- `NON_DETECTION_RELATIONS` for JavaScript and TypeScript. Retiring either
267
- predecessor without its successor would have removed the honest grade from a
268
- class of absence that is still not proof the tidy-registry mistake, made once
269
- and caught, then nearly made again one entry over.
270
-
271
- ### Fixed
272
-
273
- **`imported-by` reported a confident false absence for CommonJS.** A
274
- `require('./mod')` is a call expression, not an import statement, so the import
275
- extractor never sees it and no candidate edge is emitted. A missing *site*
276
- cannot lower a ratio computed over sites, so the metric graded its own blind
277
- spot healthy. Measured on a two-file fixture where `consumer.js` requires
278
- `./mod` **twice**: the graph held exactly one import edge (the unrelated ESM
279
- one), the imports cell read `total 1, bound 1, ratio 1.0`, and `imported-by
280
- src/mod.js` answered nothing at **`decision_grade`** — the grade whose contract
281
- is "an empty result is a trustworthy absence".
282
-
283
- This is round 55's defect in the relation that had been excused from it. That
284
- round added `NON_DETECTION_RELATIONS` for `calls`, because a callback-registered
285
- caller emits no edge, and recorded `imports` as exempt: "an import is a
286
- syntactic construct that extraction either sees or does not… add a relation only
287
- with a measured non-detection case, not on suspicion." CommonJS is that measured
288
- case. **A resolution metric cannot underwrite a coverage claim**, in any
289
- relation.
290
-
291
- `imports` now joins the non-detection set, **scoped by language** — Rust `use`
292
- and Go imports have no dynamic form graphite models, so their absences are still
293
- evidence and are not downgraded to buy a fix for JavaScript. The empty-listing
294
- line also names the construct that went undetected, since the reader's next
295
- action is a grep and which one depends on whether the missing edge is a callback
296
- registration or a `require()`.
297
-
298
- Two blind spots declared the day they were measured, per the caveat process:
299
- `js-require-emits-no-import-edge` (imports) and `js-module-object-calls-unbound`
300
- (calls `const m = require('./x'); m.f()` and `import * as ns; ns.f()`, neither
301
- covered by the existing destructuring entry). Python already binds this shape
302
- via `alias_map`; JavaScript has no equivalent. Extraction is unchanged — these
303
- are declarations, not fixes.
304
-
305
- **A failed Git version probe told the operator to upgrade a working Git.**
306
- `GitUnsupportedVersionError` carries three unrelated conditions, and the one it
307
- is named for is the rarest: the other two are a `--version` probe that timed out
308
- or could not be launched. All three raised the literal "Git 2.38 or newer is
309
- required" a sanitized message, and a false statement in the two cases where no
310
- version was ever read. It does not merely fail to help; it names a specific
311
- remedy, and that remedy is wrong. `review` repeated the same literal one layer
312
- up, `from None`, so the line a user actually sees on their terminal carried it
313
- too.
314
-
315
- Messages now come from a per-`reason` table of module constants
316
- (`git_version_failure_message`). "Git 2.38 or newer is required" survives for
317
- `too_old`, where it is exactly right; a timeout says it timed out; an
318
- unrecognised reason says the version could not be verified rather than
319
- inheriting a remedy. `review` looks up the same table instead of hardcoding a
320
- literal and deliberately does not pass `str(exc)` through, because that
321
- message can carry Git's own output and keeping it off a terminal is what the
322
- hardcoded literal was protecting. Mutation-proven: passing the exception's text
323
- through fails three tests.
324
-
325
- This is diagnosis, not a fix for #37 the flake it makes readable has not
326
- recurred in 40 CI runs since `b3ae61a`, and absence of a sighting is not
327
- evidence of a fix.
328
-
329
- **A package manager that printed anything was reported as not installed.**
330
- `run_manager_version` gave `<manager> --version` a 64-byte output budget, and
331
- `run_bounded_process` applies its budget **per stream** so whatever the child
332
- wrote to stderr competed with a limit sized for the version string on stdout.
333
- Overflow raises `output_limit`, which the probe reported as
334
- `manager_unavailable`: the same answer it gives when the manager is absent, so
335
- TypeScript activation declined to proceed and named the wrong cause.
336
- `_minimal_node_environment` forwards only locale variables and a PATH, so
337
- nothing silences npm notices or Node deprecation warnings, and either clears 64
338
- bytes on its own. The budget is now `MANAGER_VERSION_OUTPUT_LIMIT` (8 KiB)
339
- still a hard flood bound, three orders of magnitude under the install budget,
340
- and the only pathological one: a sweep of every `max_output_bytes` call site
341
- found the next smallest at 4 KiB.
342
-
343
- Found from the other side, as #48: Python 3.14 added a `site.py` check that
344
- warns when `sys.prefix` disagrees with the `pyvenv.cfg` layout, and the POSIX
345
- activation fixture wrote that landmark beside the copied interpreter rather
346
- than one level above its directory. 287 bytes on stderr, two red legs on ubuntu
347
- and macOS, reported as a missing package manager. Reproduced locally under WSL
348
- on CPython 3.14.7 a different distribution from the runner's — and attributed
349
- with a 2×2: **either fix alone clears it**, the budget because the probe stops
350
- caring what the child says, the fixture because the child stops saying it. Both
351
- shipped; the fixture was wrong on its own terms, and the budget defect was
352
- never about 3.14.
353
-
354
- **A version probe would not say what it had refused.** `run_manager_version`
355
- flattened the provenance revalidation result into `manager_unavailable`, while
356
- `run_install` had always returned it as-is one test asserted both, side by
357
- side, on a single command. "Your toolchain changed under us and graphite
358
- refused to launch it" and "there is no package manager here" are opposite
359
- operator situations, and they arrived as one string from a check whose whole
360
- job is to be believed. The reason is now returned unflattened
361
- (`executable_changed` / `command_changed`). The user-visible activation reason
362
- is unchanged: the mapping in `typescript_activation` already defaults unknown
363
- reasons to `manager_version_unavailable`.
364
-
365
- **Every bounded subprocess reported a failed containment on macOS.**
366
- `run_bounded_process` holds an exited child as an unreaped zombie on purpose, so
367
- its pgid cannot be recycled under the signals that follow. On darwin that makes
368
- the process group unsignalable and `killpg` answers **EPERM** where Linux
369
- answers success for the identical state so cleanup called every successful
370
- probe a failure. That is 46 of the 62 macOS test failures in #46, and in
371
- ordinary use it made `doctor` and every routing probe unusable on macOS.
372
-
373
- Measured on macos-latest 3.12.10 with ubuntu-latest as the control, four process
374
- states each. A live descendant in the same group makes darwin answer OK, which
375
- is what licenses reading EPERM as "nothing left to signal" rather than "not
376
- allowed to signal"; the transport also creates the group itself via `setsid()`
377
- from its own uid, so a member it may not signal is not reachable. The reading is
378
- gated on the leader having exited — on the timeout path the leader is alive and
379
- EPERM stays a failure. Linux behaviour is untouched.
380
-
381
- **A failed cleanup overwrote the diagnosis it was called to follow.** Every
382
- recheck after cleanup was guarded by "only if nothing failed yet"; the cleanup
383
- assignment itself was not, so a run that had already determined `timeout`,
384
- `output_limit` or `input_failed` reported `cleanup_failed` instead. Precedence
385
- is now explicit a transport failure, then the child's own non-zero exit, then
386
- `cleanup_failed` only when there is nothing else to report and a failed
387
- containment rides on `ProbeProcessError.cleanup_failed` rather than replacing
388
- the code. It is still raised, never returned as success.
389
-
390
- **The generated daemon launcher ran a wrapper instead of the interpreter.**
391
- `daemon_task_command` built its command from `resolve_graphite_executable()`
392
- whatever `graphite` resolved to on PATH, or `~/.local/bin/graphite.cmd` and
393
- launched it with the supervised projects root as the working directory, hidden,
394
- at every login. It now emits `<interpreter> -P -m graphite daemon …`, and an
395
- explicit `--graphite-executable` naming a console script is refused rather than
396
- silently accepted.
397
-
398
- ⚠️ **The commit subject for that change (`ff34b4f`) states the mechanism
399
- incorrectly**, and a published subject cannot be amended. `3c5304f` corrects the
400
- source; this entry is the version a `git log --oneline` reader should trust.
401
-
402
- A console script is **not** cwd-shadowable: running a script puts the script's
403
- own directory on `sys.path[0]`, and only `-m` puts the CWD there. The hazard is
404
- an `-m` **inside a wrapper** which a generator can neither see into nor add
405
- `-P` to. Measured from a directory holding a hostile `graphite.py`:
406
-
407
- | launch | result |
408
- |---|---|
409
- | `python -m graphite` | shadow ran |
410
- | `python -P -m graphite` | real graphite |
411
- | `.cmd` wrapper `python -B -m graphite` | shadow ran |
412
- | `.cmd` wrapper → `python -B -P -m graphite` | real graphite |
413
-
414
- So scope a shadowing sweep by *"does anything in this chain reach `-m` with a
415
- repo root as its working directory"* not by artifact kind, and not by whether
416
- the head of the command looks like an interpreter.
417
-
418
- **Fixing the generator does not fix the launcher it already wrote.** An existing
419
- install keeps the old command until `graphite daemon-install-startup-windows`
420
- (or `daemon-install-windows`) is re-run — the same marker-not-version rule this
421
- file opens with.
422
-
423
- **The distribution is now named `graphite-code`, and the name it is looked up by
424
- is pinned.** PyPI's `graphite` belongs to another project. The import package is
425
- still `graphite`; only the distribution name changed. A test pins the lookup name
426
- because `importlib.metadata` fails silently on a mismatch which is exactly how
427
- the CLI came to report no version at all.
428
-
429
- **The CLI reported nothing instead of an unresolvable distribution.** A failed
430
- metadata lookup fell through to silence, so a broken install and a working one
431
- were indistinguishable at the one command a consumer would use to tell them
432
- apart. It now says which distribution it could not resolve.
433
-
434
- **One machine's drive layout was welded into a published tool.** Default paths
435
- and documentation carried this checkout's own absolute paths, which would have
436
- shipped to every user of a release and to PyPI's rendered README. Fixed in the
437
- config defaults and the docs, with a check that keeps them gone.
438
-
439
- **Six sqlite connections were left to the garbage collector.** `_connect`
440
- orphaned its handle when configuration failed; the routing and lifecycle stores
441
- left connections unclosed on several paths; and the test fixtures leaked the same
442
- handle the source did, which hid the defect from the suite that should have
443
- caught it. On Windows an unclosed handle blocks the file, so recovery depended on
444
- GC timing. `conftest` now fails the suite when a handle is never closed the
445
- instrument that found the rest.
446
-
447
- **A running interpreter is trusted by identity, not by its path.** Path-based
448
- comparison misidentified the active interpreter when the same binary was reachable
449
- by more than one path, which is normal under virtualenvs and symlinks.
450
-
451
- **A test could pass while the thing it checked never happened (#50).** Sixteen
452
- wall-clock assertions were demoted to named hang guards: a stopwatch bound beside
453
- a structural assertion adds flake surface and no correctness. Two orphan checks
454
- were worse than flaky their reveal window was too short, so a surviving orphan
455
- could go unseen and the test would pass. Each fix is mutation-proven.
456
-
457
- **A failed Git step did not say which step failed (refs #37).** Git errors are now
458
- attributed to the operation and carry the OS error text, so a CI sighting is
459
- interpretable without a local repro.
460
-
461
- ## [0.2.1] 2026-08-09
462
-
463
- A portability release. `0.2.0` listed Linux and macOS as unverified; the suite
464
- now runs green end to end on Linux. Two of the defects hiding behind that gap
465
- were real, and the rest were tests that had never executed on a POSIX machine at
466
- all.
467
-
468
- ### Added
469
-
470
- - MIT license, declared with PEP 639 (`license = "MIT"` plus `license-files`)
471
- and shipped in both the wheel and the sdist. The `v0.2.0` tag predates the
472
- license commit, so that tagged tree carries no LICENSE file.
473
- - `resolve_trusted_file(..., follow_launcher=True)`: POSIX launcher-aware
474
- resolution, which keeps trust anchored in the resolved target while preserving
475
- the caller's own spelling for execution.
476
-
477
- ### Fixed
478
-
479
- **Virtual environments on POSIX.** Two call sites canonicalised `sys.executable`
480
- before launching it. `.venv/bin/python` is a symlink there, and Python locates a
481
- virtual environment from the executable it was *invoked as*, so the resolved path
482
- started the base installation instead which cannot `import graphite` at all.
483
- Both now judge the resolved target and launch the path they were given.
484
- Containment is unchanged: the rejection still tests the resolved path, so a
485
- symlink outside the workspace pointing at a workspace-controlled binary is still
486
- refused. Windows never saw this, because its virtual-environment interpreters are
487
- copies rather than symlinks.
488
-
489
- **A test froze the global clock and hung every POSIX CI leg** (#45).
490
- `monkeypatch.setattr(probes.time, "monotonic", ...)` reads as module-scoped and
491
- is not`probes.time` *is* the stdlib `time` module so the patch reached
492
- `probe_process`'s POSIX grace loop, whose exit condition became unreachable and
493
- which then spun on a real `time.sleep`. Every POSIX leg was killed at the
494
- 45-minute timeout. Fixed in the test by offsetting a real clock rather than
495
- freezing one: an advancing `time.monotonic()` is the function's contract, so a
496
- production guard would defend a condition that cannot occur.
497
-
498
- ### Changed
499
-
500
- - The residual TOCTOU in `_canonical_executable` is now named where it lives.
501
- Judging the resolved target while executing the given path moves the race from
502
- "swap the file" to "re-point the symlink". It stays accepted — no path check
503
- closes it, only an fd-based exec does — and the obvious narrowing, refusing
504
- group- or world-writable launcher directories, would reject Homebrew's
505
- `/usr/local/bin` on exactly the platforms the fix exists to support.
506
-
507
- ### Known limitations
508
-
509
- As in `0.2.0`, except:
510
-
511
- - **Linux is now verified**: 2835 passed, zero failures, zero timeouts, on
512
- Ubuntu under WSL2 with CPython 3.12.13. That is one machine, one distribution
513
- and one interpreter build — it means "no longer failing here", not "portable".
514
- - **macOS remains unverified** (#46). Linux evidence is evidence about Linux.
515
- - **CI has not started a job since 2026-08-05.** Every push since is refused
516
- with a GitHub billing/spending-limit error before the job begins, so the local
517
- gate and the WSL runs are currently the only signal.
518
-
519
- ## [0.2.0] — 2026-08-07
520
-
521
- The first tagged release, covering everything since the initial import on
522
- 2026-07-10 662 commits in total. `0.1.0` was the version the repo was created
523
- at; it was never tagged or released, so there is no earlier baseline to diff
524
- against. Grouped by theme rather than enumerated; `git log` has the detail.
525
-
526
- ### Added
527
-
528
- **Answers that grade themselves.** Every query result carries an `answer` block
529
- scoped to the relations and languages that answer actually used, graded
530
- `decision_grade` / `advisory` / `inconclusive`, with a registry of named
531
- caveats. On a `decision_grade` answer an empty result is a trustworthy absence.
532
- Human output prints `answer health:` and `known limits:` lines only when the
533
- answer is empty or degraded. Published JSON schemas under `docs/schemas/` with
534
- compatibility tests.
535
-
536
- **Resolution health as a first-class signal**, now at schema 3: `calls` and
537
- `imports` cells each carry an `external` count, and `total` already excludes it,
538
- so `ratio == bound / total` with no further adjustment.
539
-
540
- **Language binding.** Python cross-module call binding via symbol/alias import
541
- maps, plus method dispatch (`is_method` tagging and member flow-through).
542
- TypeScript/JavaScript arrow-assigned definitions, arrow-valued class fields, and
543
- `new X()` construction edges. Rust `use` and `mod` resolution against indexed
544
- Cargo manifests, attributed per crate.
545
-
546
- **A daemon that supervises only repos open in a coding agent**, with an
547
- activation registry, a cross-process build lock with TTL staleness, `build
548
- --detach`, and rebuilds queued when the engine identity changes rather than only
549
- at restart.
550
-
551
- **An append-only incident ledger** with event-sourced triage
552
- (`incidents list/ack/resolve`), fed by build cycles, observer cycles, extraction
553
- errors, artifact and graph-load failures, and inconclusive queries; surfaced in
554
- `doctor` and `daemon-health`.
555
-
556
- **Agent integration.** `graphite init` writes per-agent instruction files and
557
- git hooks (`--no-hooks` opts out; `--adopt` brings legacy unversioned docs under
558
- management by appending, never overwriting). A `graphite-first` PreToolUse hook
559
- in remind or strict mode, where strict denial is gated on proven resolution
560
- health. An MCP server, and a shared agent channel with a broker, identity
561
- derivation, create-only rounds and an append-only status log.
562
-
563
- **CLI.** `query` (with `callers`, `calls`, `reaches`, `path`, `depends-on`,
564
- `imported-by`, `community-of`, `stats`), `search`, `capabilities`, `context`,
565
- `impact`, `review-changes`, `doctor`, `incidents`, `channel`, `savings`, and
566
- `--version`.
567
-
568
- `graphite --version` reports the engine fingerprint — a digest over the engine's
569
- own source files alongside the cache and schema versions. It is byte-exact, so
570
- the implication runs one way: two installs agreeing on the fingerprint are
571
- running identical code, but two that differ are not necessarily running
572
- different code. Line endings are bytes, and `.gitattributes` normalizes to LF on
573
- commit, so a working tree holding CRLF fingerprints differently from a fresh
574
- checkout of the same commit. Equality proves sameness; inequality does not prove
575
- difference.
576
-
577
- ### Changed
578
-
579
- - The reported version now comes from `graphite.__version__` in the source tree
580
- rather than from `importlib.metadata`. Under an editable install the metadata
581
- is written once, at install time, and never moves again — so a release bump
582
- reached nobody until every consumer reinstalled. `pyproject.toml` reads the
583
- version from the source (`[tool.hatch.version]`) rather than declaring it.
584
- - `graphite --version` names a stale install when the source version and the
585
- installed distribution metadata disagree, instead of silently preferring one.
586
- That disagreement is routine for an editable install between reinstalls, but
587
- it is also what a shadowing `graphite` on `sys.path` looks like.
588
- - `requires-python = ">=3.11"` is now backed by measurement rather than
589
- inherited: 3.11, 3.12, 3.13 and 3.14 each run the full suite clean.
590
- - Extraction cache partitions on engine identity as well as cache version, so an
591
- extraction change invalidates its own cached extraction and `cache_version` is
592
- back to being a coarse manual override. Unreachable partitions are reclaimed
593
- on build.
594
-
595
- ### Fixed
596
-
597
- **Module shadowing (security).** `python -m graphite` puts the current directory
598
- at `sys.path[0]`, so a `graphite.py` or a `graphite/` directory at a repo
599
- root beats the installed package, and a module-shaped shadow *runs* before it
600
- errors. Every launch graphite generates or performs now passes `-P`, across six
601
- surfaces: agent hooks, git-hook trampolines, graphite's own repo, `.mcp.json`,
602
- `.vscode/tasks.json` (which fires on folder open, with no invocation), and
603
- graphite's own source, where seven launches carried `-B` — a flag that
604
- suppresses bytecode and does nothing to `sys.path`. A bare `graphite …` console
605
- script is shadowable identically and cannot express the fix. `-P` rather than
606
- `-I`, because `-I` implies `-E` and would strip the `GRAPHITE_*` config the CLI
607
- reads. `doctor` reports foreign hooks in a shadowable form, and never rewrites
608
- what it did not author.
609
-
610
- **Channel registry gate.** Emptying, deleting or renaming the committed agent
611
- registry disarmed the commit-message audit gate as thoroughly as removing it;
612
- authorisation is now derived from committed state, so a commit can no longer
613
- register itself, and a corrupt committed registry wedges rather than opening.
614
-
615
- **Subprocess decode.** `text=True` without `encoding=` decodes with the locale
616
- codec, and the failure lands on subprocess's reader thread: `stdout` comes back
617
- `None` while `returncode` stays 0, so the crash surfaces frames from its cause.
618
- Observed live. Fixed across seven modules and guarded mechanically via `ast`.
619
-
620
- **Graph-first bypass.** The PreToolUse matcher named tools rather than
621
- behaviour, so searches issued through the Bash and PowerShell tools never
622
- reached the hook. Shell commands are now parsed and routed through the same
623
- denial path. Four subsequent parser defects fixed: redirection operands,
624
- separated flag values and out-of-repo targets were falsely denied, and a
625
- case-folded `-E`/`-e` collision failed open.
626
-
627
- **Git enumeration hardening.** A trusted-path runner, isolated environment,
628
- containment checks against symlink and absolute-path escapes, and fail-closed
629
- behaviour no filesystem fallback when a repository cannot be enumerated
630
- safely.
631
-
632
- **Portability.** Zero-argument `super()` inside a `@dataclass(slots=True)`
633
- raises on Python 3.11 and 3.12 because the decorator rebuilds the class; the
634
- explicit two-argument form is now an AST invariant, since the interpreter the
635
- gate runs on cannot catch a reintroduction. `build_graph` no longer drops an
636
- edge that shares a node pair but differs by relation. Redirected CLI output is
637
- forced to UTF-8. An MCP probe no longer closes the child's stdin with a reply in
638
- flight: the server read that EOF as end-of-session and tore down mid-reply, so
639
- `initialize` was answered it is handled inline while `tools/list`, which is
640
- dispatched to a task racing the same teardown, was dropped.
641
-
642
- ### Known limitations
643
-
644
- - **Dynamic dispatch, decorator rebinding and `getattr` calls stay unbound.**
645
- They are counted honestly in the resolution ratio rather than hidden.
646
- - **Go and Rust imports ratios are honestly lower** than Python's and
647
- TypeScript's: neither emits `EXTERNAL_IMPORT`, so external imports are not
648
- excluded from their denominators. Cross-schema ratio comparison is invalid —
649
- branch on `schema`.
650
- - **Linux and macOS are unverified** (#45, #46). A POSIX routing defect is fixed
651
- — `_canonical_executable` rejected symlinks, and every POSIX interpreter is
652
- one but the suite has not been run green on either. CI cannot currently
653
- confirm it.
654
- - **The Windows Store Python distribution is not supported.** Its app-execution
655
- alias injects `PYTHONUSERBASE`, redirects AppData and changes process
656
- identity, which breaks environment-sanitization and process-cleanup
657
- behaviour. Use a regular CPython install.
658
- - **One flaky test remains** (#37).
659
- - Aggregate `resolution_health.healthy` can report true while the language your
660
- question actually used is degraded. Gate on `answer.grade`, not the aggregate.
661
-
662
- ## [0.1.0] 2026-07-10
663
-
664
- Initial import as a standalone repository. Never tagged or released.
1
+ # Changelog
2
+
3
+ Notable changes to graphite. Format follows [Keep a Changelog]; versioning is
4
+ semver, pre-1.0 (minor versions may break things).
5
+
6
+ **A version number here is a coarse release label, not a fix marker.** Several
7
+ behavioural fixes have shipped with no version change at all — the `-P`
8
+ agent-hook fix went out under an unchanged `DOC_VERSION`. To answer "does this
9
+ install have fix X", survey for the marker X introduced, or compare
10
+ `graphite --version` fingerprints between installs. The fingerprint is the
11
+ machine-checkable identity; the version is for humans.
12
+
13
+ [Keep a Changelog]: https://keepachangelog.com/en/1.1.0/
14
+
15
+ ## [0.5.0] — 2026-08-26
16
+
17
+ ### Fixed
18
+
19
+ **Distinct definitions collapsed into one node id** (#57, #58). Two independent
20
+ mechanisms, one silent symptom: the losing definition vanished from the graph
21
+ entirely, every edge naming either landed on whichever survived, and nothing in
22
+ the answer signalled it — a contaminated `callers` list grades exactly like a
23
+ clean one, and an empty one for the vanished name reads as a trustworthy absence.
24
+
25
+ `_make_id` had **five** lossy operations — `strip("_.")` per part, `[^\w]+ -> _`,
26
+ `_+ -> _`, `casefold()` and truncation so within one file `Path`, `path` and
27
+ `_path` were one id. Measured on this repo: **5 collisions, 6 definitions absent**.
28
+ `routing/storage.py` defines `initialize` (L950) and `_initialize` (L3965); the
29
+ graph kept `initialize`, and `self._initialize()` was recorded as a call to it.
30
+
31
+ Removing a normalisation does not fix this, which is worth knowing before trying:
32
+ drop the strip and `path`/`_path` are *still* merged, because the `_+` collapse
33
+ puts them back; drop both and `path`/`Path` remain merged by the casefold. An
34
+ ambiguous input now gets a short **hex** discriminator the one alphabet that
35
+ survives both. A canonical input keeps its plain, readable id.
36
+
37
+ `_file_node_id` built its id from `path.stem`, **discarding the extension**, so
38
+ `index.ts` and `index.js`, or a component and its stylesheet, were one node — the
39
+ shape that most affects the TypeScript consumers. It also inherited the
40
+ sanitisation collisions: `src/graphite/init.py` (791 lines) and
41
+ `src/graphite/__init__.py` (41) were one node, so the larger module had no file
42
+ node at all and its 21 symbols hung off the smaller one. Both are fixed; file ids
43
+ now carry the full filename.
44
+
45
+ **Every node id in the graph changes.** Consumers re-extract on the engine
46
+ fingerprint anyway, so the rebuild costs nothing extra but any node id written
47
+ down in a document or a script must be re-read from the new graph. Both
48
+ docstrings on `_file_node_id` claimed the old scheme kept every file distinct;
49
+ both were false and are rewritten rather than appended to.
50
+
51
+ Known residual, measured at zero occurrences here: a path separator and a literal
52
+ underscore are still indistinguishable, so `a/b.py` and `a_b.py` would collide.
53
+ Closing it would mean discriminating every path containing an underscore.
54
+
55
+
56
+ **Method dispatch overruled the call classifier, in every language** (#56).
57
+ `_call_confidence` tags an edge `EXTERNAL_CALL` only when the call *provably*
58
+ leaves the repo — an attributable receiver root bound by an import that did not
59
+ resolve in-repo, or an `_EXTERNAL_GLOBALS` name no in-repo binding shadows.
60
+ `_resolve_method_dispatch` then re-pointed that same edge to an in-repo definition
61
+ by bare method name alone, producing an edge whose `confidence` said the call left
62
+ the repo and whose `target` was a function inside it.
63
+
64
+ The #54 reachability gate cannot catch these: the import that makes the definition
65
+ "reachable" is real and irrelevant, because the receiver is `os`. Externality is
66
+ evidence that gate never consults.
67
+
68
+ Measured on this repo, two builds through one interpreter differing only in this
69
+ pass: **42 `calls` edges removed, every one `EXTERNAL_CALL`, none `LOCAL_CALL`.**
70
+ Every removed edge read at its source line is a standard-library call
71
+ `os.close`/`os.open`/`os.write`/`os.kill` (24), `subprocess.run` (8),
72
+ `threading.Event` (4), Node's `path.resolve` (2), `sys.stdin.read` (1).
73
+ `os.kill(pid, 0)` in one test was bound to **three** different in-repo `kill`s.
74
+
75
+ callers resolve 5 decision_grade 3 decision_grade
76
+ callers kill 4 decision_grade 1 decision_grade
77
+ callers event 6 decision_grade 2 decision_grade
78
+ callers run 3 decision_grade 0 ADVISORY
79
+
80
+ The last line is the honest-answer contract working: the answer is now empty *and
81
+ says so*, rather than confidently naming three wrong callers.
82
+
83
+ It is a **refusal to re-point, not a deletion**. The edge stays where it already
84
+ pointed, so `health.py` can go on excluding it from the resolution denominator —
85
+ it can only exclude what is present. **The ratio therefore does not move**
86
+ (`calls.python` 0.977 0.977, total 9427 9387, external 3044 → 3076): under
87
+ schema 3 these edges were already excluded as external, so the fix shifts them
88
+ from `total` to `external` in the same step. Grading this on health would have
89
+ read as "no effect" the trap #54's first acceptance criterion fell into,
90
+ arriving from the other direction.
91
+
92
+ **Dispatch could cross a language boundary** (#56). No FFI is modelled, so a
93
+ JavaScript call site cannot reach a Python `def` but the name match ran across
94
+ the whole node set with no notion of language, and did:
95
+ `path.resolve(input.root)` in `src/graphite/ts_resolver.mjs` bound to a `resolve`
96
+ defined in `tests/test_git_security.py`, at `decision_grade`. Dispatch is now
97
+ confined to one interop **family** (`javascript`/`typescript`/`jsx`/`tsx` together;
98
+ every other language its own), never to the raw `LANGUAGE_BY_EXT` label — `.ts`
99
+ and `.tsx` are different labels and one ecosystem. Unlike the #54 gate this is an
100
+ exact invariant rather than a proxy, so it needs no per-language census: it removes
101
+ only bindings that are impossible. **Zero marginal effect on this repo** — both
102
+ crossings here were also `EXTERNAL_CALL` and already gone — which is why its
103
+ fixture makes the crossing a `LOCAL_CALL`.
104
+
105
+ **A Rust call on an unnameable receiver was a false external** (#56).
106
+ `_simple_rust_value` returns `None` for anything but an identifier or `self`, so
107
+ `build().format()` reached `_call_confidence` as the bare name `format`, which is
108
+ in `_EXTERNAL_GLOBALS`. The Rust site passed no `attributable=`, so the edge came
109
+ out `EXTERNAL_CALL` excused from the health denominator without ever being shown
110
+ to leave the repo. That is #14 mechanism A, which Python and TS/JS were threaded
111
+ for and Rust was not. Go is deliberately unchanged: its `selector_expression`
112
+ branch builds the call name from the operand's raw source text, so nothing here
113
+ reaches its bare-name fallback, and a guard nothing can trip is not a guard.
114
+
115
+ ### Still open
116
+
117
+ The #54 reachability gate remains Python-only. That is a separate, weaker
118
+ question bindings that are *possible but unlikely* in duck-typed code — and it
119
+ still owes the per-language measurement `_dispatch_is_gated` describes.
120
+
121
+ ## [0.4.0] 2026-08-14
122
+
123
+ **A minor bump because consumer graphs change, not because an API did.** Both fixes
124
+ below remove or add graph edges, so every consumer that rebuilds will see different
125
+ `callers`/`context` answers and a slightly different Python `calls` ratio. Nothing in
126
+ the CLI or the JSON schema changed. Under this project's pre-1.0 rule minor versions
127
+ may break things — a patch number would have understated a release that silently
128
+ changes what the graph says.
129
+
130
+ ### Fixed
131
+
132
+ **Python method calls bound to any same-named definition in the repo, however
133
+ unreachable** (#54). `_resolve_method_dispatch` re-pointed `recv.method()` to any
134
+ `is_method` node sharing the name, justified in its own docstring by "method names
135
+ are almost always globally unique" — true of domain names, false of every name a
136
+ standard library already owns. On graphite's own graph `query "callers resolve"`
137
+ returned **232 callers graded `decision_grade`**, every one an ordinary
138
+ `pathlib.Path(...).resolve()` re-pointed to a test double's `resolve` in
139
+ `tests/test_git_security.py`; `src/graphite/io.py::atomic_write_text` was recorded
140
+ as calling a `write` defined in `tests/test_doctor.py`. The mis-bindings counted as
141
+ `bound`, so the health ratio rose as the defect got worse.
142
+
143
+ A Python call now dispatches only to a definition in the calling file or in a file
144
+ it directly imports. Filtering happens **before** the ambiguity cap, because
145
+ reachability is the disambiguation that cap was standing in for — so a common name
146
+ with one reachable definition now resolves instead of being abandoned as "too
147
+ generic to guess".
148
+
149
+ Measured on this repo: `callers resolve` 232 → 5, `callers get` 87 → 0,
150
+ `callers write` 30 → 0; `calls` edges 13498 → 12497 (−1428, +427); `calls.python`
151
+ 0.980 → 0.977. Every one of the 1428 dropped edges buckets as
152
+ `src|scripts|tests → tests` or `src → src`, and every sampled member was another
153
+ false binding (`sqlite3.connect`, `Thread.start`, a file handle's `write`).
154
+
155
+ **Expect consumer graphs to lose call edges and report a slightly lower Python
156
+ `calls` ratio.** That is honest accounting, not a regression the same shape as the
157
+ constructor-edge change that moved some javascript cells down. Do not "fix" it back.
158
+
159
+ The gate resolves language through `LANGUAGE_BY_EXT`, the same table `collect_files`
160
+ uses to choose an extractor, so it cannot disagree with the walk that produced the
161
+ edge. Verified to reach a **warm** cache too: a graph built by the old code, rebuilt
162
+ in place by the new code, loses the false binding — the dispatch pass runs after
163
+ `cache.write`, so a stale partition cannot preserve it.
164
+
165
+ Scoped to **Python only, on evidence**. The gate's premise "the caller does not
166
+ import the definer" standing in for "the caller cannot be holding one" — is never
167
+ exact in a duck-typed language. TypeScript/JavaScript stay ungated because
168
+ `test_member_call_ambiguous_small_set_links_to_all` pins the documented fan-out on
169
+ an `x: any` receiver with no import at all, a shape the gate would delete; Rust
170
+ because this repo has none to measure. Go could not be gated at all: its in-repo
171
+ imports target a synthesized package id that never equals any file's node id.
172
+ Residual: the same defect survives in ungated JS/TS.
173
+
174
+ **`import pkg.sub` emitted no import edge to `pkg/__init__.py`** (#55), though that
175
+ statement binds the name `pkg` and executes the package. Every dotted importer was
176
+ therefore invisible as a dependent of that `__init__.py` the file where re-exports
177
+ and shared constants live, so a high-traffic edit target whose blast radius was
178
+ understated. Silently: a missing edge cannot lower the imports ratio, which is
179
+ computed over the edges that *were* emitted.
180
+
181
+ Found because the #54 gate refused `pkg.build()` and a test caught it. The gate
182
+ initially compensated with its own ancestor expansion; with the real edges emitted
183
+ that compensation became unfalsifiable — removing it changed no test — so it was
184
+ deleted rather than left looking like protection.
185
+ `test_a_dotted_import_reaches_the_package_it_binds` now guards the coupling.
186
+
187
+ Measured on this repo: import edges 1890 → 1920 (+30, −0), imports health 740/740 →
188
+ 770/770 with `ratio` 1.0 and `external` **unchanged** at 1148 — purely additive, no
189
+ reclassification. `impact src/graphite/routing/__init__.py` went from 1 likely test
190
+ to **8**.
191
+
192
+ Scoped to `import a.b.c`, not `from a.b import c`. Both execute the package, but only
193
+ the first binds `a`, so only the first can carry a `a.something()` call to attribute;
194
+ the from-import spelling has two existing specifications pinning one edge per module,
195
+ and expanding it would have added ~234 further edges here with no measured demand.
196
+ Ancestors are edged only when they resolve in-repo, so a PEP 420 namespace package
197
+ (no `__init__.py`) and an external `import os.path` both emit nothing extra.
198
+
199
+ ## [0.3.0] 2026-08-12
200
+
201
+ **The first release consumers install as a built wheel.** Through 0.2.1 every
202
+ consumer on the development machine imported graphite from one shared editable
203
+ install pointed at the source tree, so a saved file was live everywhere with no
204
+ build, no review and no version boundary. That is why `__version__` could not
205
+ distinguish two states of the tree, and why "which fix does this install have"
206
+ was answerable only by surveying for markers. A wheel install writes source and
207
+ metadata together, so from 0.3.0 the version means something again.
208
+
209
+ ### Fixed
210
+
211
+ **Daemon discovery only worked on one machine, and the wheel shipped that path.**
212
+ `_default_daemon_base` compared each parent directory against a single hardcoded
213
+ absolute path the maintainer's own layout, present since the initial commit.
214
+ Two separate defects in one line. Functionally, discovery succeeded on exactly
215
+ one machine and silently fell back to the project root everywhere else, so
216
+ `daemon-health` and `bootstrap` reported no daemon for every other user.
217
+ Distribution-wise, the wheel carried an absolute developer path, which
218
+ `RELEASING.md` forbids in a release archive.
219
+
220
+ The value is only ever used to reach `base/.graphite-daemon/status.json`, so it
221
+ now searches upward for that marker instead of matching a name. Behaviour is
222
+ unchanged where the old literal applied, and correct everywhere else.
223
+ `GRAPHITE_PROJECTS_ROOT` still takes precedence, and the directory name is now a
224
+ single named constant shared with the reader of that file.
225
+
226
+ Worth recording *why this survived two releases*: 0.2.0 and 0.2.1 both ran a
227
+ disclosure scan and both recorded zero developer-path hits, because both searched
228
+ the **compressed** archive bytes, where a plaintext pattern cannot match. A clean
229
+ tree and a leaking one were indistinguishable the scan could not fail. The
230
+ 0.2.1 wheel in fact shipped this path in three modules and twelve times in
231
+ `METADATA`. `tests/test_hardening.py` now scans the packaged sources directly,
232
+ docstrings included, and is mutation-proven against the original line.
233
+
234
+ **The engine fingerprint could not see the parsers (#52).** `engine_identity`
235
+ digested `cache_version`, `schema_version`, `graphite.__version__` and the files
236
+ under `src/graphite/` everything except the tree-sitter grammars that actually
237
+ produce the ASTs. Upgrading `tree-sitter-python` therefore changed every
238
+ extracted graph while the fingerprint stayed byte-identical.
239
+
240
+ Three mechanisms this repo deliberately built were defeated by that. The
241
+ extraction cache partitions on `{cache_version}-{engine[:16]}` (#21), so a
242
+ grammar upgrade served parses produced by the OLD parser precisely the
243
+ staleness partitioning was introduced to prevent. The daemon queues a rebuild
244
+ when a project's recorded engine identity differs from the current one (#18), and
245
+ a grammar upgrade produced no difference, so supervised repos kept graphs that
246
+ disagreed with their source. And `metadata.engine.fingerprint` is documented as
247
+ the engine that produced the content, which across a parser change it was not.
248
+
249
+ Not hypothetical: every runtime dependency is lower-bound only, and the installed
250
+ `tree-sitter` was already 0.25.2 against a declared floor of `>=0.23`.
251
+
252
+ `engine_identity` now folds a `parsers` record into the digest, and
253
+ `metadata.engine` reports it, so a reader can see *which* grammars produced a
254
+ graph rather than only that something moved:
255
+
256
+ ```
257
+ "parsers": "tree-sitter=0.25.2,tree-sitter-go=0.25.0,..."
258
+ ```
259
+
260
+ An absent grammar is recorded as `absent` rather than omitted — "go was
261
+ installed" and "go was not" are different engines and must not share a
262
+ fingerprint and a missing grammar degrades that one language without making
263
+ identity itself fail. `ENGINE_SCHEMA_VERSION` is now `2`: a fingerprint computed
264
+ under schema 1 covered graphite's own files only, so the two are not comparable.
265
+
266
+ **Expect a one-time rebuild everywhere.** Every fingerprint changes, so the first
267
+ run after upgrading re-extracts and re-partitions its cache. That is the correct
268
+ signal, not a side effect.
269
+
270
+ **A transport failure now says what it observed (#51).** The deep MCP probe's
271
+ diagnostic read every field off a `ProbeProcessResult`, and a transport failure
272
+ never produces one — `run_bounded_process` raises instead of returning. So on a
273
+ timeout, the one failure the diagnostic exists to explain, it printed `<none>`
274
+ for every field. A real CI sighting was consequently uninterpretable.
275
+
276
+ `ProbeProcessError` now carries `elapsed_seconds`, `budget_seconds`,
277
+ `stdout_bytes` and `stderr_bytes`. Counts and timings only: the error type is
278
+ contractually free of process data, and tests pin that a child's output cannot
279
+ reach `str(exc)`. Numbers are safe to carry there for the same reason
280
+ `os_error` already was.
281
+
282
+ Read `elapsed_s` against `budget_s`. At or below budget, the deadline fired on
283
+ time and the child did not answer within it note a normal timeout lands
284
+ *below* budget, because the runner reserves up to 40% of it for cleanup and
285
+ enforces the earlier execution deadline. **Above** budget means our own deadline
286
+ was late, i.e. the process was starved of CPU the load hypothesis, which
287
+ nothing in the log could previously express. `stdout_bytes` splits the first
288
+ case: zero means the child never produced a byte, non-zero means it was alive
289
+ and progressing.
290
+
291
+ Live output, same probe, two failures:
292
+
293
+ deep_mcp output_limit: elapsed_s=0.170 | budget_s=0.35 | stdout_bytes=1048577
294
+ deep_mcp timeout: elapsed_s=0.325 | budget_s=0.35 | stdout_bytes=0
295
+
296
+ ### Added
297
+
298
+ **`graphite debt` reports declared blind spots and their age.** A blind spot that
299
+ is DECLARED is working as designed; an undeclared one is the failure. The command
300
+ prints open and retired entries with time-to-retire, so the count is auditable
301
+ rather than a claim. Retirement is recorded against the fix that earned it.
302
+
303
+ **The public API surface is declared.** `docs/` now states what consumers may
304
+ depend on and what they may not. Six repos import this package from one install,
305
+ so "it happened to work" was the only contract they had; anything not listed is
306
+ explicitly not a promise.
307
+
308
+ **Release verification targets the BUILT DISTRIBUTION, not the source tree.**
309
+ Checks that passed against the working tree could not see what the wheel actually
310
+ contained the two differ precisely where packaging bugs live.
311
+
312
+ **CommonJS is modelled (#49).** `require('<literal>')` now emits a real
313
+ `imports` edge — resolved in-repo, or `EXTERNAL_IMPORT` for a bare package,
314
+ exactly as the ESM equivalent does. Four call shapes that previously landed on
315
+ a same-file phantom now bind to their definition:
316
+
317
+ | shape | before | after |
318
+ |---|---|---|
319
+ | `const { f } = require('./x')` `f()` | phantom | binds |
320
+ | `const m = require('./x')` `m.f()` | phantom | binds |
321
+ | `module.exports.f = f` `m.f()` | phantom | binds |
322
+ | `import * as ns from './x'` → `ns.f()` | phantom | binds |
323
+
324
+ `_ImportBindings` gained `namespaces`, mapping a whole-module local name to the
325
+ file it stands for, and `_resolve_call` turns `m.f()` into that file's `f`.
326
+ This is a mirror of what Python's `alias_map` has always done for `import x` +
327
+ `x.attr()`, not a new design. Detection of the `require()` shape lives in one
328
+ predicate used by both the binding collector and the edge-emitting walk, so the
329
+ two cannot drift.
330
+
331
+ Measured on a four-file fixture, JavaScript and TypeScript both: calls
332
+ **1/2 5/5**, imports **1/1 2/2**, placeholder share **0.143 0.077** as the
333
+ `m.f` phantoms stopped being invented, and the graph moved unhealthy → healthy.
334
+ Note the calls denominator *grew* while the ratio rose newly bound sites are
335
+ sites that were previously never counted. `imported-by src/mod.js` now answers
336
+ `consumer.js` at `decision_grade` instead of answering nothing at that grade.
337
+
338
+ One interaction worth recording: `_resolve_method_dispatch` re-points any call
339
+ edge carrying `_member` by method NAME alone, so a namespace-resolved `m.f()`
340
+ would have been stolen back by any same-named class method elsewhere. The walk
341
+ now omits `_member` when the namespace map resolved the target — the post-pass
342
+ exists for edges that are "only a file-scoped phantom", which these no longer
343
+ are.
344
+
345
+ Guarded against a false positive the fix itself introduced. The binding maps are
346
+ file-level while calls are walked per scope, so an inner `const m = ...`, a
347
+ parameter named `m`, or a second destructure of the same name is
348
+ indistinguishable from the module binding at resolution time — and `m.real()`
349
+ would have claimed the module's definition, putting a caller in `callers real`
350
+ that does not exist. A wrong edge is worse than a missing one.
351
+ `_rebound_local_names` distrusts any require-bound name that is bound more than
352
+ once anywhere in the file: deliberately blunt rather than modelling JavaScript
353
+ scope, because it **fails closed**, giving up an edge instead of inventing one.
354
+ Measured cost in a file that rebinds the name, every `m.x()` in it loses
355
+ binding, which is exactly the pre-#49 behaviour for that file and no worse.
356
+ Applied only to names CommonJS introduced; ESM binding forms are
357
+ statement-level and were never re-derived from a declarator.
358
+
359
+ Also measured and correct without change: a module-object call to a member the
360
+ module does not export (`m.notExported()`) produces an unbound placeholder and
361
+ LOWERS the ratio rather than fabricating a binding.
362
+
363
+ Three caveats retired on re-measured evidence — `ts-destructured-locals-unbound`
364
+ (declared 2026-07-27), `js-require-emits-no-import-edge` and
365
+ `js-module-object-calls-unbound` (both declared that morning). One added:
366
+ `js-dynamic-module-load-unmodelled`, because `require(expr)` and `import()`
367
+ expressions still emit nothing, measured the same day note the second has a
368
+ string literal, so "non-literal argument" is not the test; `import()` is an
369
+ expression rather than an import statement. And one more,
370
+ `js-shadowed-module-local-unbound`, for the shadowing subset above. **Both
371
+ non-detection classes are narrowed, not gone**, so `imports` stays in
372
+ `NON_DETECTION_RELATIONS` for JavaScript and TypeScript. Retiring either
373
+ predecessor without its successor would have removed the honest grade from a
374
+ class of absence that is still not proof the tidy-registry mistake, made once
375
+ and caught, then nearly made again one entry over.
376
+
377
+ ### Fixed
378
+
379
+ **`imported-by` reported a confident false absence for CommonJS.** A
380
+ `require('./mod')` is a call expression, not an import statement, so the import
381
+ extractor never sees it and no candidate edge is emitted. A missing *site*
382
+ cannot lower a ratio computed over sites, so the metric graded its own blind
383
+ spot healthy. Measured on a two-file fixture where `consumer.js` requires
384
+ `./mod` **twice**: the graph held exactly one import edge (the unrelated ESM
385
+ one), the imports cell read `total 1, bound 1, ratio 1.0`, and `imported-by
386
+ src/mod.js` answered nothing at **`decision_grade`**the grade whose contract
387
+ is "an empty result is a trustworthy absence".
388
+
389
+ This is round 55's defect in the relation that had been excused from it. That
390
+ round added `NON_DETECTION_RELATIONS` for `calls`, because a callback-registered
391
+ caller emits no edge, and recorded `imports` as exempt: "an import is a
392
+ syntactic construct that extraction either sees or does not… add a relation only
393
+ with a measured non-detection case, not on suspicion." CommonJS is that measured
394
+ case. **A resolution metric cannot underwrite a coverage claim**, in any
395
+ relation.
396
+
397
+ `imports` now joins the non-detection set, **scoped by language** — Rust `use`
398
+ and Go imports have no dynamic form graphite models, so their absences are still
399
+ evidence and are not downgraded to buy a fix for JavaScript. The empty-listing
400
+ line also names the construct that went undetected, since the reader's next
401
+ action is a grep and which one depends on whether the missing edge is a callback
402
+ registration or a `require()`.
403
+
404
+ Two blind spots declared the day they were measured, per the caveat process:
405
+ `js-require-emits-no-import-edge` (imports) and `js-module-object-calls-unbound`
406
+ (calls — `const m = require('./x'); m.f()` and `import * as ns; ns.f()`, neither
407
+ covered by the existing destructuring entry). Python already binds this shape
408
+ via `alias_map`; JavaScript has no equivalent. Extraction is unchanged — these
409
+ are declarations, not fixes.
410
+
411
+ **A failed Git version probe told the operator to upgrade a working Git.**
412
+ `GitUnsupportedVersionError` carries three unrelated conditions, and the one it
413
+ is named for is the rarest: the other two are a `--version` probe that timed out
414
+ or could not be launched. All three raised the literal "Git 2.38 or newer is
415
+ required" a sanitized message, and a false statement in the two cases where no
416
+ version was ever read. It does not merely fail to help; it names a specific
417
+ remedy, and that remedy is wrong. `review` repeated the same literal one layer
418
+ up, `from None`, so the line a user actually sees on their terminal carried it
419
+ too.
420
+
421
+ Messages now come from a per-`reason` table of module constants
422
+ (`git_version_failure_message`). "Git 2.38 or newer is required" survives for
423
+ `too_old`, where it is exactly right; a timeout says it timed out; an
424
+ unrecognised reason says the version could not be verified rather than
425
+ inheriting a remedy. `review` looks up the same table instead of hardcoding a
426
+ literal and deliberately does not pass `str(exc)` through, because that
427
+ message can carry Git's own output and keeping it off a terminal is what the
428
+ hardcoded literal was protecting. Mutation-proven: passing the exception's text
429
+ through fails three tests.
430
+
431
+ This is diagnosis, not a fix for #37 the flake it makes readable has not
432
+ recurred in 40 CI runs since `b3ae61a`, and absence of a sighting is not
433
+ evidence of a fix.
434
+
435
+ **A package manager that printed anything was reported as not installed.**
436
+ `run_manager_version` gave `<manager> --version` a 64-byte output budget, and
437
+ `run_bounded_process` applies its budget **per stream** so whatever the child
438
+ wrote to stderr competed with a limit sized for the version string on stdout.
439
+ Overflow raises `output_limit`, which the probe reported as
440
+ `manager_unavailable`: the same answer it gives when the manager is absent, so
441
+ TypeScript activation declined to proceed and named the wrong cause.
442
+ `_minimal_node_environment` forwards only locale variables and a PATH, so
443
+ nothing silences npm notices or Node deprecation warnings, and either clears 64
444
+ bytes on its own. The budget is now `MANAGER_VERSION_OUTPUT_LIMIT` (8 KiB)
445
+ still a hard flood bound, three orders of magnitude under the install budget,
446
+ and the only pathological one: a sweep of every `max_output_bytes` call site
447
+ found the next smallest at 4 KiB.
448
+
449
+ Found from the other side, as #48: Python 3.14 added a `site.py` check that
450
+ warns when `sys.prefix` disagrees with the `pyvenv.cfg` layout, and the POSIX
451
+ activation fixture wrote that landmark beside the copied interpreter rather
452
+ than one level above its directory. 287 bytes on stderr, two red legs on ubuntu
453
+ and macOS, reported as a missing package manager. Reproduced locally under WSL
454
+ on CPython 3.14.7a different distribution from the runner's and attributed
455
+ with a 2×2: **either fix alone clears it**, the budget because the probe stops
456
+ caring what the child says, the fixture because the child stops saying it. Both
457
+ shipped; the fixture was wrong on its own terms, and the budget defect was
458
+ never about 3.14.
459
+
460
+ **A version probe would not say what it had refused.** `run_manager_version`
461
+ flattened the provenance revalidation result into `manager_unavailable`, while
462
+ `run_install` had always returned it as-is — one test asserted both, side by
463
+ side, on a single command. "Your toolchain changed under us and graphite
464
+ refused to launch it" and "there is no package manager here" are opposite
465
+ operator situations, and they arrived as one string from a check whose whole
466
+ job is to be believed. The reason is now returned unflattened
467
+ (`executable_changed` / `command_changed`). The user-visible activation reason
468
+ is unchanged: the mapping in `typescript_activation` already defaults unknown
469
+ reasons to `manager_version_unavailable`.
470
+
471
+ **Every bounded subprocess reported a failed containment on macOS.**
472
+ `run_bounded_process` holds an exited child as an unreaped zombie on purpose, so
473
+ its pgid cannot be recycled under the signals that follow. On darwin that makes
474
+ the process group unsignalable and `killpg` answers **EPERM** where Linux
475
+ answers success for the identical state so cleanup called every successful
476
+ probe a failure. That is 46 of the 62 macOS test failures in #46, and in
477
+ ordinary use it made `doctor` and every routing probe unusable on macOS.
478
+
479
+ Measured on macos-latest 3.12.10 with ubuntu-latest as the control, four process
480
+ states each. A live descendant in the same group makes darwin answer OK, which
481
+ is what licenses reading EPERM as "nothing left to signal" rather than "not
482
+ allowed to signal"; the transport also creates the group itself via `setsid()`
483
+ from its own uid, so a member it may not signal is not reachable. The reading is
484
+ gated on the leader having exited — on the timeout path the leader is alive and
485
+ EPERM stays a failure. Linux behaviour is untouched.
486
+
487
+ **A failed cleanup overwrote the diagnosis it was called to follow.** Every
488
+ recheck after cleanup was guarded by "only if nothing failed yet"; the cleanup
489
+ assignment itself was not, so a run that had already determined `timeout`,
490
+ `output_limit` or `input_failed` reported `cleanup_failed` instead. Precedence
491
+ is now explicit a transport failure, then the child's own non-zero exit, then
492
+ `cleanup_failed` only when there is nothing else to report and a failed
493
+ containment rides on `ProbeProcessError.cleanup_failed` rather than replacing
494
+ the code. It is still raised, never returned as success.
495
+
496
+ **The generated daemon launcher ran a wrapper instead of the interpreter.**
497
+ `daemon_task_command` built its command from `resolve_graphite_executable()` —
498
+ whatever `graphite` resolved to on PATH, or `~/.local/bin/graphite.cmd` — and
499
+ launched it with the supervised projects root as the working directory, hidden,
500
+ at every login. It now emits `<interpreter> -P -m graphite daemon …`, and an
501
+ explicit `--graphite-executable` naming a console script is refused rather than
502
+ silently accepted.
503
+
504
+ ⚠️ **The commit subject for that change (`ff34b4f`) states the mechanism
505
+ incorrectly**, and a published subject cannot be amended. `3c5304f` corrects the
506
+ source; this entry is the version a `git log --oneline` reader should trust.
507
+
508
+ A console script is **not** cwd-shadowable: running a script puts the script's
509
+ own directory on `sys.path[0]`, and only `-m` puts the CWD there. The hazard is
510
+ an `-m` **inside a wrapper** — which a generator can neither see into nor add
511
+ `-P` to. Measured from a directory holding a hostile `graphite.py`:
512
+
513
+ | launch | result |
514
+ |---|---|
515
+ | `python -m graphite` | shadow ran |
516
+ | `python -P -m graphite` | real graphite |
517
+ | `.cmd` wrapper `python -B -m graphite` | shadow ran |
518
+ | `.cmd` wrapper → `python -B -P -m graphite` | real graphite |
519
+
520
+ So scope a shadowing sweep by *"does anything in this chain reach `-m` with a
521
+ repo root as its working directory"* not by artifact kind, and not by whether
522
+ the head of the command looks like an interpreter.
523
+
524
+ **Fixing the generator does not fix the launcher it already wrote.** An existing
525
+ install keeps the old command until `graphite daemon-install-startup-windows`
526
+ (or `daemon-install-windows`) is re-run — the same marker-not-version rule this
527
+ file opens with.
528
+
529
+ **The distribution is now named `graphite-code`, and the name it is looked up by
530
+ is pinned.** PyPI's `graphite` belongs to another project. The import package is
531
+ still `graphite`; only the distribution name changed. A test pins the lookup name
532
+ because `importlib.metadata` fails silently on a mismatch which is exactly how
533
+ the CLI came to report no version at all.
534
+
535
+ **The CLI reported nothing instead of an unresolvable distribution.** A failed
536
+ metadata lookup fell through to silence, so a broken install and a working one
537
+ were indistinguishable at the one command a consumer would use to tell them
538
+ apart. It now says which distribution it could not resolve.
539
+
540
+ **One machine's drive layout was welded into a published tool.** Default paths
541
+ and documentation carried this checkout's own absolute paths, which would have
542
+ shipped to every user of a release — and to PyPI's rendered README. Fixed in the
543
+ config defaults and the docs, with a check that keeps them gone.
544
+
545
+ **Six sqlite connections were left to the garbage collector.** `_connect`
546
+ orphaned its handle when configuration failed; the routing and lifecycle stores
547
+ left connections unclosed on several paths; and the test fixtures leaked the same
548
+ handle the source did, which hid the defect from the suite that should have
549
+ caught it. On Windows an unclosed handle blocks the file, so recovery depended on
550
+ GC timing. `conftest` now fails the suite when a handle is never closed — the
551
+ instrument that found the rest.
552
+
553
+ **A running interpreter is trusted by identity, not by its path.** Path-based
554
+ comparison misidentified the active interpreter when the same binary was reachable
555
+ by more than one path, which is normal under virtualenvs and symlinks.
556
+
557
+ **A test could pass while the thing it checked never happened (#50).** Sixteen
558
+ wall-clock assertions were demoted to named hang guards: a stopwatch bound beside
559
+ a structural assertion adds flake surface and no correctness. Two orphan checks
560
+ were worse than flaky their reveal window was too short, so a surviving orphan
561
+ could go unseen and the test would pass. Each fix is mutation-proven.
562
+
563
+ **A failed Git step did not say which step failed (refs #37).** Git errors are now
564
+ attributed to the operation and carry the OS error text, so a CI sighting is
565
+ interpretable without a local repro.
566
+
567
+ ## [0.2.1] — 2026-08-09
568
+
569
+ A portability release. `0.2.0` listed Linux and macOS as unverified; the suite
570
+ now runs green end to end on Linux. Two of the defects hiding behind that gap
571
+ were real, and the rest were tests that had never executed on a POSIX machine at
572
+ all.
573
+
574
+ ### Added
575
+
576
+ - MIT license, declared with PEP 639 (`license = "MIT"` plus `license-files`)
577
+ and shipped in both the wheel and the sdist. The `v0.2.0` tag predates the
578
+ license commit, so that tagged tree carries no LICENSE file.
579
+ - `resolve_trusted_file(..., follow_launcher=True)`: POSIX launcher-aware
580
+ resolution, which keeps trust anchored in the resolved target while preserving
581
+ the caller's own spelling for execution.
582
+
583
+ ### Fixed
584
+
585
+ **Virtual environments on POSIX.** Two call sites canonicalised `sys.executable`
586
+ before launching it. `.venv/bin/python` is a symlink there, and Python locates a
587
+ virtual environment from the executable it was *invoked as*, so the resolved path
588
+ started the base installation instead which cannot `import graphite` at all.
589
+ Both now judge the resolved target and launch the path they were given.
590
+ Containment is unchanged: the rejection still tests the resolved path, so a
591
+ symlink outside the workspace pointing at a workspace-controlled binary is still
592
+ refused. Windows never saw this, because its virtual-environment interpreters are
593
+ copies rather than symlinks.
594
+
595
+ **A test froze the global clock and hung every POSIX CI leg** (#45).
596
+ `monkeypatch.setattr(probes.time, "monotonic", ...)` reads as module-scoped and
597
+ is not `probes.time` *is* the stdlib `time` module — so the patch reached
598
+ `probe_process`'s POSIX grace loop, whose exit condition became unreachable and
599
+ which then spun on a real `time.sleep`. Every POSIX leg was killed at the
600
+ 45-minute timeout. Fixed in the test by offsetting a real clock rather than
601
+ freezing one: an advancing `time.monotonic()` is the function's contract, so a
602
+ production guard would defend a condition that cannot occur.
603
+
604
+ ### Changed
605
+
606
+ - The residual TOCTOU in `_canonical_executable` is now named where it lives.
607
+ Judging the resolved target while executing the given path moves the race from
608
+ "swap the file" to "re-point the symlink". It stays accepted — no path check
609
+ closes it, only an fd-based exec does — and the obvious narrowing, refusing
610
+ group- or world-writable launcher directories, would reject Homebrew's
611
+ `/usr/local/bin` on exactly the platforms the fix exists to support.
612
+
613
+ ### Known limitations
614
+
615
+ As in `0.2.0`, except:
616
+
617
+ - **Linux is now verified**: 2835 passed, zero failures, zero timeouts, on
618
+ Ubuntu under WSL2 with CPython 3.12.13. That is one machine, one distribution
619
+ and one interpreter build — it means "no longer failing here", not "portable".
620
+ - **macOS remains unverified** (#46). Linux evidence is evidence about Linux.
621
+ - **CI has not started a job since 2026-08-05.** Every push since is refused
622
+ with a GitHub billing/spending-limit error before the job begins, so the local
623
+ gate and the WSL runs are currently the only signal.
624
+
625
+ ## [0.2.0] 2026-08-07
626
+
627
+ The first tagged release, covering everything since the initial import on
628
+ 2026-07-10 662 commits in total. `0.1.0` was the version the repo was created
629
+ at; it was never tagged or released, so there is no earlier baseline to diff
630
+ against. Grouped by theme rather than enumerated; `git log` has the detail.
631
+
632
+ ### Added
633
+
634
+ **Answers that grade themselves.** Every query result carries an `answer` block
635
+ scoped to the relations and languages that answer actually used, graded
636
+ `decision_grade` / `advisory` / `inconclusive`, with a registry of named
637
+ caveats. On a `decision_grade` answer an empty result is a trustworthy absence.
638
+ Human output prints `answer health:` and `known limits:` lines only when the
639
+ answer is empty or degraded. Published JSON schemas under `docs/schemas/` with
640
+ compatibility tests.
641
+
642
+ **Resolution health as a first-class signal**, now at schema 3: `calls` and
643
+ `imports` cells each carry an `external` count, and `total` already excludes it,
644
+ so `ratio == bound / total` with no further adjustment.
645
+
646
+ **Language binding.** Python cross-module call binding via symbol/alias import
647
+ maps, plus method dispatch (`is_method` tagging and member flow-through).
648
+ TypeScript/JavaScript arrow-assigned definitions, arrow-valued class fields, and
649
+ `new X()` construction edges. Rust `use` and `mod` resolution against indexed
650
+ Cargo manifests, attributed per crate.
651
+
652
+ **A daemon that supervises only repos open in a coding agent**, with an
653
+ activation registry, a cross-process build lock with TTL staleness, `build
654
+ --detach`, and rebuilds queued when the engine identity changes rather than only
655
+ at restart.
656
+
657
+ **An append-only incident ledger** with event-sourced triage
658
+ (`incidents list/ack/resolve`), fed by build cycles, observer cycles, extraction
659
+ errors, artifact and graph-load failures, and inconclusive queries; surfaced in
660
+ `doctor` and `daemon-health`.
661
+
662
+ **Agent integration.** `graphite init` writes per-agent instruction files and
663
+ git hooks (`--no-hooks` opts out; `--adopt` brings legacy unversioned docs under
664
+ management by appending, never overwriting). A `graphite-first` PreToolUse hook
665
+ in remind or strict mode, where strict denial is gated on proven resolution
666
+ health. An MCP server, and a shared agent channel with a broker, identity
667
+ derivation, create-only rounds and an append-only status log.
668
+
669
+ **CLI.** `query` (with `callers`, `calls`, `reaches`, `path`, `depends-on`,
670
+ `imported-by`, `community-of`, `stats`), `search`, `capabilities`, `context`,
671
+ `impact`, `review-changes`, `doctor`, `incidents`, `channel`, `savings`, and
672
+ `--version`.
673
+
674
+ `graphite --version` reports the engine fingerprint — a digest over the engine's
675
+ own source files — alongside the cache and schema versions. It is byte-exact, so
676
+ the implication runs one way: two installs agreeing on the fingerprint are
677
+ running identical code, but two that differ are not necessarily running
678
+ different code. Line endings are bytes, and `.gitattributes` normalizes to LF on
679
+ commit, so a working tree holding CRLF fingerprints differently from a fresh
680
+ checkout of the same commit. Equality proves sameness; inequality does not prove
681
+ difference.
682
+
683
+ ### Changed
684
+
685
+ - The reported version now comes from `graphite.__version__` in the source tree
686
+ rather than from `importlib.metadata`. Under an editable install the metadata
687
+ is written once, at install time, and never moves again — so a release bump
688
+ reached nobody until every consumer reinstalled. `pyproject.toml` reads the
689
+ version from the source (`[tool.hatch.version]`) rather than declaring it.
690
+ - `graphite --version` names a stale install when the source version and the
691
+ installed distribution metadata disagree, instead of silently preferring one.
692
+ That disagreement is routine for an editable install between reinstalls, but
693
+ it is also what a shadowing `graphite` on `sys.path` looks like.
694
+ - `requires-python = ">=3.11"` is now backed by measurement rather than
695
+ inherited: 3.11, 3.12, 3.13 and 3.14 each run the full suite clean.
696
+ - Extraction cache partitions on engine identity as well as cache version, so an
697
+ extraction change invalidates its own cached extraction and `cache_version` is
698
+ back to being a coarse manual override. Unreachable partitions are reclaimed
699
+ on build.
700
+
701
+ ### Fixed
702
+
703
+ **Module shadowing (security).** `python -m graphite` puts the current directory
704
+ at `sys.path[0]`, so a `graphite.py` — or a `graphite/` directory — at a repo
705
+ root beats the installed package, and a module-shaped shadow *runs* before it
706
+ errors. Every launch graphite generates or performs now passes `-P`, across six
707
+ surfaces: agent hooks, git-hook trampolines, graphite's own repo, `.mcp.json`,
708
+ `.vscode/tasks.json` (which fires on folder open, with no invocation), and
709
+ graphite's own source, where seven launches carried `-B` — a flag that
710
+ suppresses bytecode and does nothing to `sys.path`. A bare `graphite …` console
711
+ script is shadowable identically and cannot express the fix. `-P` rather than
712
+ `-I`, because `-I` implies `-E` and would strip the `GRAPHITE_*` config the CLI
713
+ reads. `doctor` reports foreign hooks in a shadowable form, and never rewrites
714
+ what it did not author.
715
+
716
+ **Channel registry gate.** Emptying, deleting or renaming the committed agent
717
+ registry disarmed the commit-message audit gate as thoroughly as removing it;
718
+ authorisation is now derived from committed state, so a commit can no longer
719
+ register itself, and a corrupt committed registry wedges rather than opening.
720
+
721
+ **Subprocess decode.** `text=True` without `encoding=` decodes with the locale
722
+ codec, and the failure lands on subprocess's reader thread: `stdout` comes back
723
+ `None` while `returncode` stays 0, so the crash surfaces frames from its cause.
724
+ Observed live. Fixed across seven modules and guarded mechanically via `ast`.
725
+
726
+ **Graph-first bypass.** The PreToolUse matcher named tools rather than
727
+ behaviour, so searches issued through the Bash and PowerShell tools never
728
+ reached the hook. Shell commands are now parsed and routed through the same
729
+ denial path. Four subsequent parser defects fixed: redirection operands,
730
+ separated flag values and out-of-repo targets were falsely denied, and a
731
+ case-folded `-E`/`-e` collision failed open.
732
+
733
+ **Git enumeration hardening.** A trusted-path runner, isolated environment,
734
+ containment checks against symlink and absolute-path escapes, and fail-closed
735
+ behaviour — no filesystem fallback when a repository cannot be enumerated
736
+ safely.
737
+
738
+ **Portability.** Zero-argument `super()` inside a `@dataclass(slots=True)`
739
+ raises on Python 3.11 and 3.12 because the decorator rebuilds the class; the
740
+ explicit two-argument form is now an AST invariant, since the interpreter the
741
+ gate runs on cannot catch a reintroduction. `build_graph` no longer drops an
742
+ edge that shares a node pair but differs by relation. Redirected CLI output is
743
+ forced to UTF-8. An MCP probe no longer closes the child's stdin with a reply in
744
+ flight: the server read that EOF as end-of-session and tore down mid-reply, so
745
+ `initialize` was answered — it is handled inline — while `tools/list`, which is
746
+ dispatched to a task racing the same teardown, was dropped.
747
+
748
+ ### Known limitations
749
+
750
+ - **Dynamic dispatch, decorator rebinding and `getattr` calls stay unbound.**
751
+ They are counted honestly in the resolution ratio rather than hidden.
752
+ - **Go and Rust imports ratios are honestly lower** than Python's and
753
+ TypeScript's: neither emits `EXTERNAL_IMPORT`, so external imports are not
754
+ excluded from their denominators. Cross-schema ratio comparison is invalid —
755
+ branch on `schema`.
756
+ - **Linux and macOS are unverified** (#45, #46). A POSIX routing defect is fixed
757
+ — `_canonical_executable` rejected symlinks, and every POSIX interpreter is
758
+ one — but the suite has not been run green on either. CI cannot currently
759
+ confirm it.
760
+ - **The Windows Store Python distribution is not supported.** Its app-execution
761
+ alias injects `PYTHONUSERBASE`, redirects AppData and changes process
762
+ identity, which breaks environment-sanitization and process-cleanup
763
+ behaviour. Use a regular CPython install.
764
+ - **One flaky test remains** (#37).
765
+ - Aggregate `resolution_health.healthy` can report true while the language your
766
+ question actually used is degraded. Gate on `answer.grade`, not the aggregate.
767
+
768
+ ## [0.1.0] — 2026-07-10
769
+
770
+ Initial import as a standalone repository. Never tagged or released.