graphite-code 1.2.0__tar.gz → 1.3.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 (306) hide show
  1. {graphite_code-1.2.0 → graphite_code-1.3.0}/CHANGELOG.md +653 -0
  2. {graphite_code-1.2.0 → graphite_code-1.3.0}/PKG-INFO +3 -2
  3. {graphite_code-1.2.0 → graphite_code-1.3.0}/README.md +2 -1
  4. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/__init__.py +1 -1
  5. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/agent_hooks.py +595 -68
  6. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/answer_contract.py +65 -2
  7. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/cli.py +213 -25
  8. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/extract/ast.py +457 -29
  9. graphite_code-1.3.0/src/graphite/extract/pyscope.py +726 -0
  10. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/ingest.py +84 -3
  11. graphite_code-1.3.0/src/graphite/ownership.py +189 -0
  12. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/query.py +42 -19
  13. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_agent_hooks.py +924 -0
  14. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_answer_contract.py +65 -0
  15. graphite_code-1.3.0/tests/test_cache_file_identity.py +216 -0
  16. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_call_graph.py +27 -9
  17. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_case_ambiguous_answer.py +33 -5
  18. graphite_code-1.3.0/tests/test_cli_below_repository_root.py +781 -0
  19. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_dispatch_evidence.py +42 -0
  20. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_doctor.py +105 -14
  21. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_hardening.py +17 -0
  22. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_incident_ledger.py +72 -0
  23. graphite_code-1.3.0/tests/test_member_call_receiver.py +467 -0
  24. graphite_code-1.3.0/tests/test_no_path_is_graded.py +216 -0
  25. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_published_schemas.py +23 -2
  26. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_query_plan.py +41 -11
  27. graphite_code-1.3.0/tests/test_repository_root_ownership.py +444 -0
  28. graphite_code-1.3.0/tests/test_self_super_dispatch.py +1506 -0
  29. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_typescript_activation.py +9 -0
  30. graphite_code-1.2.0/src/graphite/extract/pyscope.py +0 -398
  31. {graphite_code-1.2.0 → graphite_code-1.3.0}/.gitignore +0 -0
  32. {graphite_code-1.2.0 → graphite_code-1.3.0}/ARCHITECTURE.md +0 -0
  33. {graphite_code-1.2.0 → graphite_code-1.3.0}/CONTRIBUTING.md +0 -0
  34. {graphite_code-1.2.0 → graphite_code-1.3.0}/LICENSE +0 -0
  35. {graphite_code-1.2.0 → graphite_code-1.3.0}/RELEASING.md +0 -0
  36. {graphite_code-1.2.0 → graphite_code-1.3.0}/pyproject.toml +0 -0
  37. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/__main__.py +0 -0
  38. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/_cleanup_worker.py +0 -0
  39. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/_win32_ctypes.py +0 -0
  40. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/activation.py +0 -0
  41. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/agent_settings.py +0 -0
  42. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/analyze.py +0 -0
  43. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/bootstrap.py +0 -0
  44. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/buildlock.py +0 -0
  45. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/cache.py +0 -0
  46. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/channel.py +0 -0
  47. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/cluster.py +0 -0
  48. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/config.py +0 -0
  49. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/context.py +0 -0
  50. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/daemon.py +0 -0
  51. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/daemon_health.py +0 -0
  52. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/daemon_launch.py +0 -0
  53. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/debt.py +0 -0
  54. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/dependency_install.py +0 -0
  55. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/detach.py +0 -0
  56. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/doctor.py +0 -0
  57. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/doctor_probes.py +0 -0
  58. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/engine_identity.py +0 -0
  59. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/export/__init__.py +0 -0
  60. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/export/html.py +0 -0
  61. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/export/json.py +0 -0
  62. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/export/md.py +0 -0
  63. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/extract/__init__.py +0 -0
  64. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/freshness.py +0 -0
  65. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/git.py +0 -0
  66. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/graph.py +0 -0
  67. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/graph_io.py +0 -0
  68. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/health.py +0 -0
  69. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/hook_entry.py +0 -0
  70. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/hookinstall.py +0 -0
  71. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/hookshim.py +0 -0
  72. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/incident_ledger.py +0 -0
  73. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/init.py +0 -0
  74. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/io.py +0 -0
  75. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/launchd_agent.py +0 -0
  76. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/listing.py +0 -0
  77. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/llm.py +0 -0
  78. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/llm_probe.py +0 -0
  79. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/mcp.py +0 -0
  80. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/mcp_server.py +0 -0
  81. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/natural_query.py +0 -0
  82. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/overlays.py +0 -0
  83. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/probe_process.py +0 -0
  84. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/probe_workspace.py +0 -0
  85. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/process_contracts.py +0 -0
  86. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/programs.py +0 -0
  87. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/provider_observer.py +0 -0
  88. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/py.typed +0 -0
  89. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/query_plan.py +0 -0
  90. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/replacement_audit.py +0 -0
  91. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/resolve.py +0 -0
  92. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/review.py +0 -0
  93. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/__init__.py +0 -0
  94. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/approval.py +0 -0
  95. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/classifier.py +0 -0
  96. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/claude_executor.py +0 -0
  97. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/claude_probe.py +0 -0
  98. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/cli_identity.py +0 -0
  99. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/codex_executor.py +0 -0
  100. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/codex_probe.py +0 -0
  101. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/context_builder.py +0 -0
  102. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/contracts.py +0 -0
  103. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/diff_policy.py +0 -0
  104. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/edit_apply.py +0 -0
  105. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/effort.py +0 -0
  106. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/lifecycle.py +0 -0
  107. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/lifecycle_operator.py +0 -0
  108. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/lifecycle_service.py +0 -0
  109. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/lifecycle_storage.py +0 -0
  110. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/ollama_executor.py +0 -0
  111. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/ollama_probe.py +0 -0
  112. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/openrouter_executor.py +0 -0
  113. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/openrouter_probe.py +0 -0
  114. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/policy.py +0 -0
  115. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/probe_runner.py +0 -0
  116. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/process_runner.py +0 -0
  117. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/profiles.py +0 -0
  118. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/prompt.py +0 -0
  119. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/registry.py +0 -0
  120. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/route_pool.py +0 -0
  121. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/route_pool_execution.py +0 -0
  122. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/schema_validation.py +0 -0
  123. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/service.py +0 -0
  124. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/settings.py +0 -0
  125. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/shadow.py +0 -0
  126. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/storage.py +0 -0
  127. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/telemetry.py +0 -0
  128. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/worktree.py +0 -0
  129. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/zai_edit.py +0 -0
  130. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/zai_executor.py +0 -0
  131. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/routing/zai_probe.py +0 -0
  132. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/savings.py +0 -0
  133. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/systemd_unit.py +0 -0
  134. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/ts_bridge.py +0 -0
  135. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/ts_resolver.mjs +0 -0
  136. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/typescript_activation.py +0 -0
  137. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/usage_ledger.py +0 -0
  138. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/validation.py +0 -0
  139. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/watch.py +0 -0
  140. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/windows_job.py +0 -0
  141. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/windows_startup.py +0 -0
  142. {graphite_code-1.2.0 → graphite_code-1.3.0}/src/graphite/windows_task.py +0 -0
  143. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/conftest.py +0 -0
  144. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/data/sqloracle/blindspots.py.txt +0 -0
  145. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/data/sqloracle/round69.py.txt +0 -0
  146. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/fake_clis/fake_cli.py +0 -0
  147. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/fixtures/provider_lifecycle_schema_v1.sql +0 -0
  148. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/fixtures/routing_schema_v2_ca77600.sql +0 -0
  149. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/fixtures/routing_schema_v3_94eb333.sql +0 -0
  150. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/fixtures/routing_schema_v4_lifecycle_migration.sql +0 -0
  151. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test__win32_ctypes.py +0 -0
  152. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_activation.py +0 -0
  153. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_activation_backstop.py +0 -0
  154. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_agent_hook_activation.py +0 -0
  155. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_agent_settings.py +0 -0
  156. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_arrow_and_constructor_binding.py +0 -0
  157. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_atomic_write.py +0 -0
  158. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_bootstrap.py +0 -0
  159. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_build_detach.py +0 -0
  160. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_buildlock.py +0 -0
  161. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_cache_engine_identity.py +0 -0
  162. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_cache_entry_pruning.py +0 -0
  163. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_cache_file_set.py +0 -0
  164. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_cache_pruning.py +0 -0
  165. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_channel_attribution.py +0 -0
  166. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_channel_cli.py +0 -0
  167. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_channel_core.py +0 -0
  168. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_channel_kind.py +0 -0
  169. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_channel_lock.py +0 -0
  170. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_channel_log_integrity.py +0 -0
  171. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_channel_mcp.py +0 -0
  172. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_channel_register.py +0 -0
  173. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_channel_report.py +0 -0
  174. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_channel_status.py +0 -0
  175. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_class_field_arrow_binding.py +0 -0
  176. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_cli_channel.py +0 -0
  177. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_cli_channel_attribution.py +0 -0
  178. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_cli_reference.py +0 -0
  179. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_cli_version.py +0 -0
  180. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_cluster.py +0 -0
  181. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_compare_dist.py +0 -0
  182. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_configuration_reference.py +0 -0
  183. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_context.py +0 -0
  184. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_context_builder.py +0 -0
  185. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_cwd_relative_config.py +0 -0
  186. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_cycle_search.py +0 -0
  187. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_daemon.py +0 -0
  188. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_daemon_activation.py +0 -0
  189. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_daemon_build_lock.py +0 -0
  190. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_daemon_child_lock.py +0 -0
  191. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_daemon_engine_staleness.py +0 -0
  192. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_daemon_health.py +0 -0
  193. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_daemon_health_activation.py +0 -0
  194. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_daemon_launch.py +0 -0
  195. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_daemon_service_cli.py +0 -0
  196. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_debt.py +0 -0
  197. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_dependency_install.py +0 -0
  198. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_destructured_binding.py +0 -0
  199. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_determinism.py +0 -0
  200. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_doctor_agent_hooks.py +0 -0
  201. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_doctor_hooks.py +0 -0
  202. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_doctor_managed_docs.py +0 -0
  203. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_doctor_probes.py +0 -0
  204. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_documentation.py +0 -0
  205. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_edit_apply.py +0 -0
  206. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_engine_identity.py +0 -0
  207. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_exit_codes_reference.py +0 -0
  208. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_external_calls.py +0 -0
  209. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_git_security.py +0 -0
  210. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_go_rust.py +0 -0
  211. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_graph_io.py +0 -0
  212. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_graph_provider_isolation.py +0 -0
  213. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_health.py +0 -0
  214. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_hook_entry.py +0 -0
  215. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_hook_template.py +0 -0
  216. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_hookinstall.py +0 -0
  217. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_hookshim.py +0 -0
  218. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_html_security.py +0 -0
  219. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_init.py +0 -0
  220. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_init_activation_doctrine.py +0 -0
  221. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_init_hooks.py +0 -0
  222. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_init_validation_exit.py +0 -0
  223. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_launchd_agent.py +0 -0
  224. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_lifecycle_operator.py +0 -0
  225. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_listing.py +0 -0
  226. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_listing_surfaces.py +0 -0
  227. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_llm.py +0 -0
  228. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_llm_probe.py +0 -0
  229. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_mcp.py +0 -0
  230. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_mcp_server.py +0 -0
  231. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_mcp_server_channel_attribution.py +0 -0
  232. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_method_dispatch_scope.py +0 -0
  233. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_monorepo.py +0 -0
  234. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_mutation_tests.py +0 -0
  235. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_natural_query.py +0 -0
  236. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_node_identity.py +0 -0
  237. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_ollama_executor.py +0 -0
  238. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_overlays.py +0 -0
  239. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_path_leak_fixture.py +0 -0
  240. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_probe_diagnostics.py +0 -0
  241. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_probe_process.py +0 -0
  242. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_probe_workspace.py +0 -0
  243. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_process_contracts.py +0 -0
  244. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_programs.py +0 -0
  245. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_provider_claude_probe.py +0 -0
  246. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_provider_codex_probe.py +0 -0
  247. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_provider_lifecycle.py +0 -0
  248. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_provider_lifecycle_service.py +0 -0
  249. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_provider_lifecycle_storage.py +0 -0
  250. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_provider_observer.py +0 -0
  251. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_provider_ollama_probe.py +0 -0
  252. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_provider_openrouter_probe.py +0 -0
  253. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_provider_probe_runner.py +0 -0
  254. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_public_surface.py +0 -0
  255. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_pyscope.py +0 -0
  256. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_pyscopeoracle.py +0 -0
  257. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_python_import_bindings.py +0 -0
  258. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_python_resolver.py +0 -0
  259. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_python_scope_identity.py +0 -0
  260. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_reliability.py +0 -0
  261. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_replacement_audit.py +0 -0
  262. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_resolve.py +0 -0
  263. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_review.py +0 -0
  264. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_route_pool.py +0 -0
  265. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_route_pool_execution.py +0 -0
  266. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_approval.py +0 -0
  267. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_benchmark.py +0 -0
  268. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_classifier.py +0 -0
  269. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_claude_executor.py +0 -0
  270. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_cli.py +0 -0
  271. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_cli_contracts.py +0 -0
  272. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_cli_recovery.py +0 -0
  273. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_codex_executor.py +0 -0
  274. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_context.py +0 -0
  275. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_contracts.py +0 -0
  276. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_diff_policy.py +0 -0
  277. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_executor.py +0 -0
  278. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_openrouter_executor.py +0 -0
  279. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_policy.py +0 -0
  280. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_process_runner.py +0 -0
  281. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_profiles.py +0 -0
  282. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_registry.py +0 -0
  283. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_schema_validation.py +0 -0
  284. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_security.py +0 -0
  285. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_service.py +0 -0
  286. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_shadow.py +0 -0
  287. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_storage.py +0 -0
  288. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_telemetry.py +0 -0
  289. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_worktree.py +0 -0
  290. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_zai_executor.py +0 -0
  291. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_routing_zai_probe.py +0 -0
  292. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_savings.py +0 -0
  293. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_search.py +0 -0
  294. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_smoke.py +0 -0
  295. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_sqloracle.py +0 -0
  296. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_synthetic_repo.py +0 -0
  297. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_systemd_unit.py +0 -0
  298. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_ts_bridge.py +0 -0
  299. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_typescript_resolver.py +0 -0
  300. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_usage_ledger.py +0 -0
  301. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_verify_artifact.py +0 -0
  302. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_verify_published_release.py +0 -0
  303. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_watch.py +0 -0
  304. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_windows_startup.py +0 -0
  305. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_windows_task.py +0 -0
  306. {graphite_code-1.2.0 → graphite_code-1.3.0}/tests/test_zai_edit.py +0 -0
@@ -15,6 +15,659 @@ machine-checkable identity; the version is for humans.
15
15
 
16
16
  ## [Unreleased]
17
17
 
18
+ ## [1.3.0] — 2026-10-16
19
+
20
+ A minor release, with a security fix: from a folder below a repository's
21
+ root, the agent hooks of 1.2.1 could take a directory another account made
22
+ for the repository (see Security). Upgrade.
23
+
24
+ JSON outputs that carry `schema_version` gain fields and codes, which are
25
+ additions under `docs/compatibility.md`: a `no_path` result is graded
26
+ (`answer`, `resolution`, `inconclusive`; #76); `check --json` gains the
27
+ reason `below_repository_root` and the field `repository_root` (#81); and a
28
+ Python `calls` answer may carry the caveats `python-member-call-bound-by-name`
29
+ (#79) and `python-self-call-bound-for-enclosing-class` (#73). `--graph-json`
30
+ left out below a repository root is now looked for at the root, and
31
+ `docs/compatibility.md` records why that is a fix and not a change of
32
+ default. `docs/reference/exit-codes.md` names the new failure and the new
33
+ reason. No field is removed or renamed, `Requires-Dist` and the entry points
34
+ are those of 1.2.1, and `graphite init` writes what it wrote (`DOC_VERSION`
35
+ 15).
36
+
37
+ The graph changes in this release, in its edges only. On this repository,
38
+ measured against the deployed 1.2.1 engine, with the same interpreter, from
39
+ one tree:
40
+
41
+ - all 9,031 node ids survive, each naming the same definition, file or
42
+ placeholder, and none is new;
43
+ - `calls` goes from 15,811 to 15,771: 44 call triples are gone and 4 are
44
+ new. `contains` (6,502), `imports` (2,383) and `inherits` (120) are
45
+ unchanged, and 24,772 of 24,816 edge triples are unchanged;
46
+ - `analysis` (20 cycles, entry points, god nodes, orphans) is identical. The
47
+ community count goes from 169 to 170;
48
+ - the Python `calls` ratio is 0.976 before and after. The cell counts 40
49
+ fewer call sites (12,001 where there were 12,041), and 40 fewer bound, so
50
+ every site that left it had been bound.
51
+
52
+ Two builds of this tree with either engine are byte-identical (`graph.json`,
53
+ `GRAPH_REPORT.md`, `graph.html`). On a tree where two files of one language
54
+ have the same content, the graph gains the file that had no node, with what
55
+ it defines (#74).
56
+
57
+ The entries below quote figures measured when each change was made, on
58
+ earlier commits of this tree, so they differ from the ones above.
59
+
60
+ ### Added
61
+
62
+ **A "no path" answer is graded (#76).** `reaches` and `path` answer an
63
+ unreachable pair with the error `no_path`. Of everything those two verbs
64
+ return it is the one result that asserts an absence, and nothing graded it.
65
+ On a fixture where nine of a function's ten call edges are unbound, `callers`
66
+ on a node answered `inconclusive` and `reaches` to that same node answered "no
67
+ call path" as a plain error.
68
+
69
+ A `no_path` result now carries three more fields:
70
+
71
+ - `resolution`: how each end was matched, as a found path reports it;
72
+ - `answer`: the block every other answer carries, scoped to the languages of
73
+ the two nodes the question resolved to;
74
+ - `inconclusive`: true exactly when `answer.grade` is `inconclusive`.
75
+
76
+ The grade is `inconclusive` where a cell the answer used is degraded or was
77
+ not measured, and `advisory` otherwise. It is never `decision_grade`, for the
78
+ reason an empty `callers` answer is not: a call the graph did not bind, or a
79
+ caller registered as a callback, leaves a real path out of the graph, and no
80
+ ratio can see an edge that was never emitted.
81
+
82
+ Where one end was a pick between spellings (`Verdict` typed, a function
83
+ `verdict` chosen), that end's `resolution` entry carries `case_ambiguous` and
84
+ the answer carries the caveat `target-resolved-to-another-spelling`. Before
85
+ this change the node id in the message was the only sign of the pick.
86
+
87
+ Not changed: `error_code: no_path`, the message, `truncated` and `limits`. A
88
+ `no_path` cut short by the depth bound still says `truncated: true`. Between
89
+ two files that are not code there is nothing to grade by, and the result
90
+ carries neither `answer` nor `inconclusive`, as before. The grade is taken
91
+ from the two resolved nodes and never from the whole graph: grading a
92
+ `no_path` against every language in the graph was the defect fixed on
93
+ 2026-07-31, and it is why such a result then carried no block at all.
94
+
95
+ `graphite query` records a `query_inconclusive` incident for a `no_path`
96
+ graded `inconclusive`, as it does for every other inconclusive answer. It
97
+ used to skip every error result, which was right while a `no_path` was never
98
+ graded. Such a result carries no `resolution_health`, so the incident's detail
99
+ is the scoped cell and the grade (`answer calls(python) 0.1 inconclusive`).
100
+
101
+ These are fields added to a JSON output that carries `schema_version`, which
102
+ `docs/compatibility.md` makes a minor release. The sentences in
103
+ `docs/agent-integration.md` and in the `query-result` schema that said a
104
+ `no_path` is not graded are replaced with what it now carries.
105
+
106
+ **`check --json` says when it was asked about a folder below a repository's
107
+ root (#81).** The answer for such a folder, below the root of any Git
108
+ repository whether graphite is set up in it or not, is `"reason":
109
+ "below_repository_root"`, with one new field, `"repository_root"`: the
110
+ directory to run it from, or `null` where the `.git` above is not one graphite
111
+ takes (see Security). `stale` is `true` and the exit status is 1, as they
112
+ were for that folder, and `added`, `changed`, `removed` and
113
+ `resolution_health` are still there. `manifest` is not: it belongs to the
114
+ reason `missing manifest`, which this answer replaces for such a folder, and
115
+ no other reason carries it. The fix this belongs to is below.
116
+
117
+ ### Fixed
118
+
119
+ **The strict graph-first hook sees a search behind a newline, a wrapper, a
120
+ subshell, a fused regex escape or a variable (#75).** Each of these passed a
121
+ search the hook refuses when it is written plainly. On a fixture with a
122
+ function `dependents` in `src/`, `grep -rn dependents src/` was refused and
123
+ every one of these was allowed:
124
+
125
+ - **A later line.** `ls`, then `grep -rn dependents src/` on the next line. A
126
+ newline was read as a space, so the first word of the first line was the
127
+ head of the whole command. Most multi-line commands have this shape.
128
+ - **A wrapper.** `sudo`, `env LC_ALL=C`, `time` and `command` before the
129
+ search; `ls src | xargs grep dependents`; `find src -type f -exec grep
130
+ dependents {} +`. Also read now: `nice`, `nohup`, `timeout`, and the shell
131
+ keywords a search follows (`if`, `then`, `do`, `!`, `{`). A `cd` behind one
132
+ of these is not followed: `env cd docs` moves a program that then exits, and
133
+ the shell stays where it was.
134
+ - **A subshell.** `(grep -rn dependents src/)` and
135
+ `wc -l $(grep -rln dependents src/)`, in backticks and inside double quotes
136
+ as well. A subshell is read in the shell of the command that holds it. After
137
+ `cd docs && (...)` it searches `docs/`, and it reads the variables set
138
+ before it. Where it moves and what it sets stay inside it.
139
+ - **A fused escape.** The Grep tool with `\bdependents\b` or `\sdependents`.
140
+ The escape and the name were read as one word, `bdependents`.
141
+ - **A variable.** `P=dependents; grep -rn $P src/`. The hook now reads a
142
+ variable where the same command assigns it: through a chain
143
+ (`S=src; D="$S/pkg"; grep -rn dependents $D`) and in PowerShell
144
+ (`$p = 'dependents'`) as well. The value is dropped once something the hook
145
+ does not model may have set the variable again: a command that names it
146
+ (`read P`, `for P in ...`, `P+=...`), `source`, `.` and `eval`, or an
147
+ assignment after `&&` or `||`, which runs or does not.
148
+
149
+ One more was not in the issue, and it hid a search on a single line too:
150
+
151
+ - **A `#` inside a word.** `echo a#b; grep -rn dependents src/`, and
152
+ `[ $# -gt 0 ] && grep -rn dependents src/`. The lexer took any `#` for the
153
+ start of a comment and dropped the rest of the line, search and all. A
154
+ comment starts only at a `#` that starts a word.
155
+
156
+ Three things changed with these, because reading every line made them matter:
157
+
158
+ - **A pipeline runs where it started.** `cd docs && find . -type f | xargs
159
+ grep word` searches `docs/`. Every stage of a pipeline is read in the
160
+ directory the pipeline started in.
161
+ - **A PowerShell path keeps its backslashes.** A PowerShell command was read
162
+ with the POSIX escape, so `-Path src\pkg\core.py` lost its backslashes,
163
+ named no file, and a search of that one file was refused. The backtick is
164
+ the escape there now.
165
+ - **A PowerShell block comment is text.** What stands between `<#` and `#>`
166
+ is not read as commands.
167
+
168
+ What is text stays text: the body of a heredoc, a PowerShell here-string,
169
+ quoted strings and comments are not read as commands, so
170
+ `git commit -m "$(cat <<'EOF' ... EOF)"` with a search quoted in the message
171
+ is allowed as before. `graphite query ... | grep name` still filters. A search
172
+ of one file, of a directory with no code, of a path outside the repository,
173
+ and of a word that differs in case from the symbol are allowed on a later
174
+ line and behind a wrapper exactly as they are when written plainly.
175
+
176
+ A search on a later line is held to the rules a plain one is. One of them is
177
+ that a search whose path the hook cannot read is refused when its pattern
178
+ names a symbol: a path in a variable the hook could not expand, or a path
179
+ after a `cd` it could not follow. Such a search on a later line was allowed
180
+ only because the line was not read.
181
+
182
+ Limits, stated in `docs/agent-integration.md`. Each of these is allowed, as it
183
+ was:
184
+
185
+ - a pattern in a variable set in the user's profile, by an earlier command,
186
+ from a command's output or as a prefix to the search itself
187
+ (`P=name grep $P`). The variable is not expanded, so the hook has no name
188
+ to check. A path in such a variable is the refused case above;
189
+ - a search behind a wrapper flag that takes a word
190
+ (`sudo -u someone grep ...`), or with its tool named by a substitution
191
+ (`$(which grep) ...`);
192
+ - a search in a command handed to another shell as a string
193
+ (`bash -c 'grep ...'`, `eval`);
194
+ - a search in a shell function written on one line (`f() { grep ...; }`). A
195
+ body on lines of its own is read, whether or not the function is called;
196
+ - a substitution inside the body of a heredoc. The body is read as text, and
197
+ where its delimiter is not quoted the shell runs a `$(...)` in it;
198
+ - in PowerShell, files piped to `Select-String`
199
+ (`Get-ChildItem -Recurse src | Select-String name`). A command after a pipe
200
+ is read as a filter, `xargs` apart.
201
+
202
+ The hook still fails open on a command it cannot read.
203
+
204
+ **Two files with the same bytes each keep their place in the graph, and a
205
+ moved file is named where it is (#74).** The extraction cache keyed an entry
206
+ on a file's bytes and its language. What an entry holds names the file: every
207
+ node id and every `source_file` in it is built from the path. So two files
208
+ with the same bytes shared one entry, and one of them was answered with the
209
+ other's nodes and had none of its own.
210
+
211
+ - **On every build after the first**, one file of each such group was in the
212
+ graph and the others were not, with everything they define and every edge
213
+ from them. A tree of seven files, six of them in identical pairs (two
214
+ modules, two templates, two empty `__init__.py`), went from nine nodes to
215
+ five on a second build with nothing changed.
216
+ - **On a first build** it was a race between worker threads, which is how the
217
+ issue was filed: one tree, built twice from an empty cache, gave two node
218
+ sets.
219
+ - **An import of a package whose `__init__.py` is empty** pointed at a file
220
+ node that was not there, once another empty `__init__.py` had been read.
221
+ - **A file moved with its bytes unchanged** kept its old path in the graph.
222
+ Python and TypeScript were spared, because the set of paths is already in
223
+ their key. Go, Rust, templates and every other file were not, and neither
224
+ were two Python files that exchange their contents.
225
+
226
+ The issue was filed as file nodes with no edges coming and going. It was
227
+ wider than that: definitions went with their file. Measured on Django 5.2.7,
228
+ 1.2.1 against this change, each built from an empty cache two or three times
229
+ and then once more on the cache it had left:
230
+
231
+ - 1.2.1 gave three different graphs, with 109, 110 and 118 files of a kind
232
+ the graph takes left without a node;
233
+ - this change gave one graph four times, the same `graph.json` byte for byte.
234
+ Nothing a 1.2.1 graph holds is missing from it. It gains 128 to 137 nodes
235
+ and 166 to 168 edges, and 24 of the nodes are classes and functions in
236
+ Python files that had a twin.
237
+
238
+ The file's path is now part of the key. An entry belongs to one file, an
239
+ unchanged file is still answered from the cache, and nothing else about the
240
+ cache changes. The first build after the upgrade extracts every file again,
241
+ as after any change to the engine. On a tree where no two files of one
242
+ language share their bytes the graph is what it was: this repository builds
243
+ the same 8,655 nodes and 23,691 edges before and after.
244
+
245
+ Five Django files still have no node, and none of them is this defect: three
246
+ are binary, one is under a dot-directory, and one has the node id of another
247
+ path beside it (`file_txt` and `file.txt`).
248
+
249
+ **A Python member call is bound by what its receiver is, where the source
250
+ says so (#79).** A member call (`obj.m()`) was bound by the name `m` alone: to
251
+ every method of that name in the caller's file and the files it imports, up to
252
+ three. So `Beta().setup()` was also a call to `Alpha.setup`, and
253
+ `callers Alpha.setup` listed a function that only ever builds a `Beta`, at
254
+ `decision_grade`. With one method of the name in reach the call was simply
255
+ pointed at it: `f.read()` on a file opened on the line above was a call to
256
+ the repository's one `read` method.
257
+
258
+ Where the source states the receiver, that now decides:
259
+
260
+ - **One class.** A class called to build the object (`Beta().m()`), the class
261
+ itself (`Beta.m()`), or a local name bound only from such a call
262
+ (`x = Beta()`, then `x.m()`) is bound to that class's own `m` and to nothing
263
+ else. The class may be defined in the file or imported by name.
264
+ - **Not an object of the repository.** A literal (`", ".join(parts)`), the
265
+ result of a builtin that returns exactly a builtin type (`open(p).read()`,
266
+ `set(x).add(y)`), or a local name bound only from those
267
+ (`with open(p) as f:`) has no method the repository defines. The call gets
268
+ no edge.
269
+
270
+ No edge, and not an `EXTERNAL_CALL` edge. That tag is for a call whose NAME
271
+ is shown to leave the repository (`re.compile`, `expect`), and its placeholder
272
+ node carries that name. Here the name on the edge is a local variable's
273
+ (`f.read`), so a placeholder would say nothing, and for a literal the
274
+ placeholder is a function of the module (`", ".join(x)` beside a `def join`),
275
+ which is the false edge over again. No edge is also where a member call that
276
+ binds to nothing already ends: the dispatch pass drops one with no method of
277
+ the name in reach, and the noise list drops `parts.append(x)` before that. So
278
+ these calls are not counted under `external`.
279
+
280
+ A name is "bound only from" a value when every statement that binds it in its
281
+ scope says the same; `None` is skipped, because it has no method to call.
282
+ Flow is not followed.
283
+
284
+ Bound by name as before: a parameter,
285
+ annotated or not, because an annotation admits a subclass; an attribute
286
+ (`self.x.m()`); what any other call returns; a method the stated class
287
+ inherits and does not define; `a = b = Beta()` (only `b` is read);
288
+ `(x := Beta())`; `y = x`; what `with Beta() as x` hands over; a builtin's
289
+ name in a file with a star import, which may bind it; and `str(x)`,
290
+ `repr(x)`, `format(x)` and `bytes(x)`, which return whatever the argument's
291
+ own method returned. `self.m()` and `super().m()` are the next entry.
292
+
293
+ Measured on Django 5.2.7 (2,224 Python files), with every member call of the
294
+ 1.2.1 engine set beside the same call under this change, and each receiver
295
+ read a second time from CPython's own `ast`:
296
+
297
+ - 201 calls on a builtin object that were bound to a method of the
298
+ repository have no edge (`"...".format()` was a call to
299
+ `NumberAwareString.format`, `s = set(); s.add(x)` to `Collector.add`);
300
+ - 6 calls of a string's method that were recorded as calls to a module
301
+ function of that name have no edge;
302
+ - 327 calls bound to their own class's method and to one or two others keep
303
+ the one;
304
+ - 250 calls that had no edge, because more than three methods share the
305
+ name, are bound to their own class's method;
306
+ - no call changed whose receiver the source does not state.
307
+
308
+ On this repository: 3, 0, 45 and 3. The three are `read` called on an opened
309
+ file, which had put two functions that only read a file among the callers of
310
+ `Cache.read` (10 rows before, 8 now). `callers Cache.write` is unchanged at 17
311
+ rows: it still lists a function that writes to a stream it is handed as a
312
+ parameter. That row is false, it is bound by name, and the answer now says
313
+ that a member call may be.
314
+
315
+ What is still bound by name is now said on the answer: every Python `calls`
316
+ answer carries the new caveat `python-member-call-bound-by-name`, next to
317
+ `python-member-call-over-dispatch-cap`, which is the same pass over the cap.
318
+ A new caveat code is an addition to a JSON output that carries
319
+ `schema_version`.
320
+
321
+ **`self.m()` and `super().m()` are bound by class, where the classes say
322
+ which method runs (#73).** A call on `self` or on a bare `super()` was bound
323
+ like any other member call, by the name `m` alone: to every method of that
324
+ name in the caller's file and the files it imports when there are at most
325
+ three, and to nothing when there are more. So `self.get_queryset()` in a
326
+ class with a `get_queryset` of its own was a call to two other classes' as
327
+ well; `super().__init__()` was a call to the `__init__` it is written in; and
328
+ for the names many classes share (`__init__`, `get_queryset`, `to_dict`) the
329
+ call had no edge at all. That last part is the issue as filed: since every
330
+ method has a node of its own (1.2.0), more names are over the three.
331
+
332
+ Python says which method runs: the first `m` in the linearisation of the
333
+ object's class, from the class for `self` and after it for `super()`. The
334
+ call is now bound to that method, for an object of the class the call is
335
+ written in, where the classes graphite reads state which it is:
336
+
337
+ - **Every base read.** Where each base of the class, and of its bases, is a
338
+ class graphite can read (a name that is a class of the file, or one
339
+ imported by name from the file that defines it), the linearisation is
340
+ computed as Python computes it (C3).
341
+ - **A base not read.** A builtin, a dotted name (`models.Model`), a name
342
+ that reaches the file through a re-export, a call, a subscript. Such a
343
+ base may have an `m` of its own. It may also derive from a class that WAS
344
+ read, and Python then puts that class later than the readable bases
345
+ suggest: `class Child(Left, Right)`, `Left(Top)`, `Right(Unread)` reads as
346
+ Child, Left, Top, Right, and is Child, Left, Right, Unread, Top when
347
+ `Unread` derives from `Top`. So a method is bound only if what is written
348
+ puts its class before every other class that binds `m` and before every
349
+ base that was not read. What is written is two things Python keeps
350
+ whatever the rest is: a class comes before its bases, and the bases of a
351
+ class keep their order. So the class's own `m` is bound, and the first
352
+ base's (`class Child(Mixin, models.Model)`: `self.m()` is `Mixin.m`), and
353
+ a single chain up to the class whose base was not read.
354
+
355
+ A method found this way is bound whatever file it is in and however many
356
+ methods share its name. The reach rule and the cap of three exist to limit a
357
+ guess by name, and here the bases are the evidence.
358
+
359
+ **A class is more than the `def`s in its body.** Where the source shows
360
+ another way for a class to get a method, the `def`s do not say which method
361
+ runs, and the call is bound by name as before:
362
+
363
+ - a decorated class on the way, its own `def` included. `@dataclass` makes
364
+ `__init__`, and any decorator may set or replace a name. No list of
365
+ decorators that leave a class alone is kept, and that has a price: an
366
+ engine that trusts every decorator binds 419 more calls on Django and 6
367
+ more on this repository. Telling such decorators from the rest is a
368
+ follow-up;
369
+ - a class made with a keyword (`metaclass=M`, or any other but
370
+ `metaclass=type`) among the classes read, or an `__init_subclass__` on one
371
+ above the class: either runs for every class below it;
372
+ - a class whose name its scope binds more than once: `C = deco(C)`, an
373
+ import of the name beside the class, or the class written twice under an
374
+ `if`, which is two classes and one node of the graph. Nothing is bound in
375
+ such a class, and for a class derived from it, it is a base that was not
376
+ read;
377
+ - `m` set on one of the classes from outside its body in the same file
378
+ (`K.m = ...`, `del K.m`), or by one of its class methods (`cls.m = ...`);
379
+ - a first class that binds `m` some other way than one `def` written
380
+ straight in its body: an assignment, a property object, two `def`s, a
381
+ `def` under an `if`, a `try`, a loop or a `with`, or under `global`;
382
+ - for `self`: a method of any class read assigns `self.m`, because an
383
+ instance attribute shadows a method; or one of them has a
384
+ `__getattribute__`;
385
+ - `m` behind a base that was not read, or nowhere among the bases:
386
+ `super().__init__()` under `class E(Exception)` is still a call to
387
+ whichever `__init__` methods are in reach;
388
+ - a `__private` name, except on the class the call is written in;
389
+ - `super(X, obj)` with arguments; `cls.m()` in a class method, in `__new__`,
390
+ `__init_subclass__` and `__class_getitem__`; a `self` that is given another
391
+ value; `super()` in a lambda, a nested function or a comprehension; a
392
+ `super` the file binds.
393
+
394
+ **What is not read.** A call bound by class is bound to the method that
395
+ runs where none of the following applies. graphite does not see these, and
396
+ binds all the same:
397
+
398
+ - anything behind a base that was not read. A metaclass, an
399
+ `__init_subclass__` or a decorator there can replace even the class's own
400
+ `def`, and a method there can assign `self.m`;
401
+ - `setattr`, `K.__dict__`, `type(self).m = ...`, `self.__class__ = ...`, and
402
+ an assignment made in another file;
403
+ - an alias: `me = self; me.m = f`, or `K = C; K.m = f`. Flow is not followed;
404
+ - a decorator on a method. The call is bound to the `def`, whatever the
405
+ decorator returns, and the method the call is written in is taken to be
406
+ handed the instance, whatever decorates it;
407
+ - a class decorator that returns a class with other bases than the one it
408
+ was handed;
409
+ - a star import written after the class statement, which may bind its name
410
+ again.
411
+
412
+ **Not followed: what runs for an object of a subclass.** For such an object
413
+ `self.m()` runs an override below the class, and `super().m()` can reach a
414
+ class the subclass added to its bases. Those edges are missing, and every
415
+ Python `calls` answer says so with the new caveat
416
+ `python-self-call-bound-for-enclosing-class`. Bound by name, an override was
417
+ listed only where its file happened to be within reach, beside methods of
418
+ unrelated classes. `python-member-call-over-dispatch-cap` and
419
+ `python-member-call-bound-by-name` stay: calls on any other receiver are
420
+ bound by name, and so are these wherever the classes do not state the
421
+ method.
422
+
423
+ Measured on Django 5.2.7 (2,224 Python files; 47,654 calls on `self` and
424
+ 1,705 on `super()`). Each of them under the engine before this change was
425
+ set beside the same call after it, and the method that runs, for an object
426
+ of the class the call is written in, was worked out from a fuller reading
427
+ than the engine has: re-exports and dotted bases followed, builtin and
428
+ standard-library classes taken as Python holds them, and "not known"
429
+ wherever a class of the tree may get the method without a `def`.
430
+
431
+ - 1,742 calls changed. 1,262 are now bound to exactly the method that runs.
432
+ 425 are bound to the method the classes state under a standard-library
433
+ class whose own hook sets no method: 420 under `unittest.TestCase`, which
434
+ has an `__init_subclass__` since Python 3.11, and 5 under `abc.ABCMeta`.
435
+ For 55 the fuller reading still has a base it cannot read, or finds a
436
+ metaclass behind one, so what runs is not known, and they are bound as the
437
+ rule above says. Where the method that runs is known, no call bound by
438
+ class is bound to another.
439
+ - 659 of them had no edge (609 with more than three methods of the name);
440
+ 1,065 were bound to two or three methods and keep one; 18 were bound to
441
+ one method and are bound to another.
442
+ - 1,332 bindings went. 271 were a `super()` call bound to the method it is
443
+ written in; 714 were methods of a class neither above nor below; 178 were
444
+ a base class's method behind the one that runs. 168 were an override in a
445
+ subclass, which does run for an object of that subclass: that is the loss
446
+ the new caveat names. One was a method whose name differs by case.
447
+ - Of the 12,570 calls for which the fuller reading names a method of the
448
+ tree and no class on the way may get one without a `def`, 7,258 were bound
449
+ to exactly it before and 8,945 are now. 3,541 have no edge, and for 3,443
450
+ of those the one thing in the way is a base imported through a re-export
451
+ (`from django.test import TestCase`), which graphite does not read as a
452
+ class.
453
+
454
+ The issue's table, on Django's `django/` package alone (15,563 member
455
+ calls): calls with more than three methods of the name and no edge, 3,413
456
+ before and 2,872 after; among them `self` calls 327 and 128, `super()` calls
457
+ 772 and 435. On this repository: 192 and 191, `self` calls 4 and 4. Those
458
+ four are `self.to_dict()` in `@dataclass` classes: the price above, at home.
459
+
460
+ `graphite impact <symbol>`, every definition asked of both graphs: on Django
461
+ 304 of 27,513 methods answer differently (89 lose an impacted file, 160 gain
462
+ one; 32 lose a likely test, 47 gain one), with 9 of 10,546 class nodes and 9
463
+ of 2,751 functions; on this repository 4 of 809 methods and nothing else.
464
+ `impact <file>` is unchanged for every file of both. The built graphs keep
465
+ their nodes: Django 854 edges gone and 653 new, this repository 15 and 1.
466
+ The Python `calls` ratio is 0.697 before and 0.696 after on Django, because
467
+ an edge bound by name to the wrong method counted as bound, and 0.976 on
468
+ both sides here.
469
+
470
+ Also in the test suite: classes made at random, some behind names graphite
471
+ cannot read, some given a method by a decorator, a metaclass, a hook or an
472
+ assignment, are run by Python itself, and each call graphite binds by class
473
+ is held against the method that ran.
474
+
475
+ **A call bound by name looks only in the files it can reach.** A Python
476
+ call bound by name goes to the methods of that name in the caller's file and
477
+ the files it imports. The engine found them by testing every method of the
478
+ name, wherever it was, against that reach, and did so again for every call:
479
+ in a tree where many classes share a name (`__init__`, `get`, `step`), what
480
+ one call cost grew with the whole tree. The methods of a name are now held by
481
+ the file they are in, and a call looks in its own file and the files it
482
+ imports. The filter that keeps a call among the methods of its own language
483
+ family, in every language, is worked out once for each name and family
484
+ instead of once for each call.
485
+
486
+ What a call is bound to is unchanged. The extraction of this repository, of
487
+ two generated trees of 400 Python files of class hierarchies, and of a small
488
+ tree that calls one name from Python and from TypeScript, which is now a
489
+ test, is identical, node for node and edge for edge, under the engine before
490
+ this change and after it. Measured once each, on one machine: extracting
491
+ either generated tree takes 2.9 or 3.1 seconds where it took 4.3 or 4.5; and
492
+ the test that holds each call bound by class against the method Python runs,
493
+ which extracts such a tree, ran past the 120 seconds the CI run allows one
494
+ test under Linux, Python 3.12 and coverage with branch measurement, and now
495
+ takes 7 to 9 seconds there.
496
+
497
+ **The CLI works from a folder below the repository root (#81).** A command
498
+ took the directory it was typed in for the repository. One `cd` below the
499
+ root, with the graph fresh at the root:
500
+
501
+ - `query`, `search`, `context` and `impact` exited 1 with `graph unavailable:
502
+ graph_missing`, and `validate` with `graph not found`.
503
+ - Each of those runs wrote a `.graphite/local/` ledger into the subfolder and
504
+ registered the subfolder as an open project, which no build can run in (the
505
+ failure 1.2.1 describes for the hooks).
506
+ - `check .` said the graph was stale (`missing manifest`), which sends the
507
+ caller to a build. `build .` made `.cache/graphite/` in the subfolder and
508
+ was then refused with "nested Git roots are unsupported".
509
+
510
+ The repository is now found by the rule the agent hooks have used since
511
+ 1.2.1 (#80): the nearest directory upward that holds `.git`, taken only where
512
+ graphite is set up there (a `graph-out/graph.json`, or the `GRAPHITE.md` that
513
+ `init` commits) and, new in this release for the hooks as well, only where it
514
+ is the user's own (see Security). The rule is in one place,
515
+ `ingest.repository_root`, and the hooks call it. In a folder of no checkout, and below a checkout graphite is
516
+ not set up in, a read uses its own directory, as it did. A repository inside
517
+ the repository, with its own `.git`, stays its own root.
518
+
519
+ Below the root of a repository that is set up:
520
+
521
+ - **Reads follow the root.** `query`, `search`, `context`, `impact`,
522
+ `validate` and `savings` answer from the repository's graph and say what
523
+ they say at the root.
524
+ - **`--graph-json` left out is the repository's graph.** The option shows the
525
+ default it always showed, `graph-out/graph.json`, and
526
+ `docs/reference/cli.md` does not change. What changes is where that path is
527
+ looked for when the option is left out: at the repository's root, not in the
528
+ directory the command is typed in. `docs/compatibility.md` now records why
529
+ that is a fix and not a change of default. Typed out, the option means what it
530
+ meant: a path from the directory it is typed in, which must lie inside that
531
+ directory. The root's graph is never read in place of a file that was named.
532
+ - **A file argument is read the way a shell reads it.** `context core.py`
533
+ typed in `app/` is `app/core.py`. A path from the root typed below it
534
+ (`context app/core.py` in `app/`) still works, and so does a node id or a
535
+ name; where both readings name a file, the one from the working directory
536
+ wins. Only a file is read this way, and from the text typed: no link is
537
+ followed. Beside `--graph-json` the arguments are left as typed.
538
+ - **What a read records goes to the repository**: the usage ledger and the
539
+ `query_inconclusive` and `graph_load_failed` incidents. `incidents list`,
540
+ `ack` and `resolve` work on the ledger of the repository their path is in.
541
+ A `.graphite/` that these commands left in a subfolder under an earlier
542
+ version is no longer written or read by them, and can be deleted.
543
+ - **A run registers the repository**, not the subfolder. Below a checkout
544
+ graphite is not set up in, a run now registers nothing, where it registered
545
+ a folder no build can run in.
546
+
547
+ **`build`, `report`, `scan` and `check` name a path and do not follow the
548
+ root.** Given a path below the root of any Git repository, set up for graphite
549
+ or not, they stop before anything is written and say where the root is:
550
+ "`<path>` is below the root of its Git repository; graphite builds and checks
551
+ a repository from its root: run it from `<root>`". `check --json` still
552
+ answers in JSON (see Added). Below a checkout graphite is not set up in, this
553
+ too is new: `check .` used to say the graph was stale (`missing manifest`),
554
+ and `build .` left `.cache/graphite/` behind before it was refused. Below a
555
+ `.git` that is not the user's own, or one at a drive or filesystem root, they
556
+ refuse as well but name no place to run (see Security).
557
+
558
+ One thing changes at the root as well, because a file argument is read by one
559
+ rule wherever it is typed: `context ./app/core.py`, and `context` given a
560
+ whole path, did not find the file and now do. `impact` likewise.
561
+
562
+ The agent hooks told a session below the root to run graphite from the root,
563
+ every command of it. They now say that queries work from where the session
564
+ is, and that a build or a check is run from the root.
565
+
566
+ Not changed, and still wrong below a root: `doctor` given such a folder
567
+ reports on the folder ("Graph artifact is missing", with `graphite build .`
568
+ as the remedy), and its incident line reads the folder's own ledger, which
569
+ nothing writes any more. `watch`, `review-changes` and the MCP server take
570
+ the path or the directory they are given, as before.
571
+
572
+ ### Security
573
+
574
+ **A repository root is taken from below it only when it is the user's own.**
575
+ From a folder below a repository's root, the agent hooks since 1.2.1 and, in
576
+ this release, the CLI's reads take the nearest directory upward that holds
577
+ `.git` for the repository: they read its graph, write its ledger, and
578
+ register it with the daemon, which then builds it. Nothing checked who owned
579
+ that directory or its `.git`, so one made by another account on the same
580
+ machine, anywhere above the folder, could be taken. Such a directory is now
581
+ taken only when the user running graphite owns it and its `.git`, both as
582
+ named and where a link to either leads (the owner of a link is whoever made
583
+ it, and the hooks name the directory as the session does, links and all),
584
+ and never when either is a drive or filesystem root. Anywhere else the
585
+ folder stands for itself, as it did before 1.2.1 for the hooks and before
586
+ this release for the CLI. `build`, `report`, `scan` and `check` still refuse
587
+ below such a `.git`, and no longer name it as the place to run;
588
+ `check --json` gives `"repository_root": null` there.
589
+
590
+ Who owns a path is asked of the operating system (`graphite.ownership`): on
591
+ POSIX the effective uid; on Windows the owner SID, which must be the user's,
592
+ or the Administrators group while graphite runs with an elevated
593
+ administrator's token. Where the owner cannot be read or is not the local
594
+ user, as can happen on network shares and on volumes without permissions,
595
+ or where a repository belongs to the Administrators group and the token is
596
+ not elevated, or where graphite runs under `sudo`, the folder stands for
597
+ itself: queries below the root then fail as they did before #81, and are run
598
+ from the root instead.
599
+
600
+ Found by aramid's review of #81, before release.
601
+
602
+ ## [1.2.1] — 2026-10-11
603
+
604
+ A patch release. It carries one fix, to the agent hooks (#80). No stable
605
+ surface in `docs/compatibility.md` changes: nothing under `docs/reference/` or
606
+ `docs/schemas/` moves, `graphite agent-hook` still always exits 0, and the
607
+ wording of a hook's message is not a stable surface. The graph itself does not
608
+ change: on this repository, measured against the deployed 1.2.0 engine,
609
+ 8581 of 8581 node ids survive, 23612 of 23612 edge triples are unchanged,
610
+ every relation count is equal, and `analysis` is identical. Only the engine
611
+ fingerprint moves, because graphite's own bytes did.
612
+
613
+ ### Fixed
614
+
615
+ **The agent hooks find the repository from a subfolder (#80).** A hook payload
616
+ carries the session's directory, and in Claude Code that follows a `cd` in the
617
+ shell. All three hooks took it for the repository root. One directory below the
618
+ root, with the graph fresh at the root:
619
+
620
+ - `session-start` said `Graph status: missing`, the status that licenses a full
621
+ fallback from graph-first. The remedy it printed, `python -m graphite build .`,
622
+ is refused there ("nested Git roots are unsupported").
623
+ - `pre-tool-use` wrote nothing: no refusal in strict mode and no reminder. A
624
+ `cd` into any subfolder switched the gate off.
625
+ - `stop` created a `.graphite/local/` in the subfolder for its cursor and looked
626
+ for the turn's usage there.
627
+ - The subfolder was registered as an open project. The daemon cannot build
628
+ one, so it logged `project_snapshot_failed` for it at every discovery cycle,
629
+ and the repository's own marker was no longer renewed.
630
+
631
+ The repository is now the nearest directory upward that holds `.git`, the test
632
+ the build already uses to refuse a nested path (`ingest.enclosing_git_root`).
633
+ It is taken only where graphite is set up: a `graph-out/graph.json`, or the
634
+ `GRAPHITE.md` that `init` commits. In a subfolder of any other checkout, and in
635
+ a directory outside every checkout, the hooks read the session's directory as
636
+ they did. A repository inside the repository, with its own `.git`, stays its
637
+ own root.
638
+
639
+ One reading changes inside a checkout that is set up. A folder there that has
640
+ its own `GRAPHITE.md` or `graph-out/` but no `.git` was read as a project of
641
+ its own: `Graph status: missing`, or `unknown` over a leftover graph, with a
642
+ `build .` that is refused there. It is now read as part of the checkout.
643
+
644
+ Below the root:
645
+
646
+ - The graph, the freshness check, the usage ledger and the activation marker
647
+ are the repository's.
648
+ - A search's relative paths, and a search that names no path, belong to the
649
+ directory the shell is in. `grep <symbol> core.py` beside the file is one
650
+ file. A search with no path in a `docs/` that holds no code is text. `..` and
651
+ `../src` are judged as the directories they are.
652
+ - Once the command line has run a `cd` the hook does not follow, or any command
653
+ it does not model, the shell may be anywhere in the repository. A search then
654
+ keeps an absolute path, and a relative one that names a file in the session's
655
+ directory. With a directory, or no path, it is judged as a search of the
656
+ whole repository: from a `docs/` with no code, `cd ../src ; grep -rn <symbol> .`
657
+ is refused. At the root this case resolves paths against the root, as before.
658
+ - Every status, refusal and reminder ends by naming the root: "This session is
659
+ below the repository root: run graphite from `<root>`." The CLI still looks
660
+ for the graph in its own working directory (#81), so the commands those
661
+ messages print work from the root only.
662
+
663
+ The hooks no longer register a directory below a checkout's root with the
664
+ daemon, whether or not graphite is set up in that checkout.
665
+
666
+ Nothing changes for a session at the repository root. Measured on the 1.2.0
667
+ wheel and on this change: 32 strict-hook arms from the #72 work (25 refused, 7
668
+ allowed) and 12 hook arms on a checkout with `.git` and `GRAPHITE.md` read the
669
+ same on both.
670
+
18
671
  ## [1.2.0] — 2026-10-10
19
672
 
20
673
  A minor release. Query and search results, channel rounds and Python