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