graphite-code 1.2.1__tar.gz → 1.4.0__tar.gz

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