codecortex 0.25.0__tar.gz → 0.26.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 (177) hide show
  1. {codecortex-0.25.0/src/codecortex.egg-info → codecortex-0.26.0}/PKG-INFO +1 -1
  2. {codecortex-0.25.0 → codecortex-0.26.0/src/codecortex.egg-info}/PKG-INFO +1 -1
  3. {codecortex-0.25.0 → codecortex-0.26.0}/src/codecortex.egg-info/SOURCES.txt +3 -0
  4. codecortex-0.26.0/src/codeintel/__init__.py +1 -0
  5. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/graph_answer.py +26 -7
  6. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/graph_changed.py +50 -11
  7. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/graph_confidence.py +52 -2
  8. codecortex-0.26.0/src/codeintel/graph_dispatch.py +1195 -0
  9. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/graph_edges.py +6 -0
  10. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/graph_ops.py +119 -47
  11. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/graph_render.py +5 -0
  12. codecortex-0.26.0/tests/test_bench_graphify.py +268 -0
  13. codecortex-0.26.0/tests/test_dispatch_callers.py +1695 -0
  14. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_docs_trust_claims.py +13 -1
  15. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_summary_integrity.py +17 -0
  16. codecortex-0.25.0/src/codeintel/__init__.py +0 -1
  17. {codecortex-0.25.0 → codecortex-0.26.0}/LICENSE +0 -0
  18. {codecortex-0.25.0 → codecortex-0.26.0}/README.md +0 -0
  19. {codecortex-0.25.0 → codecortex-0.26.0}/pyproject.toml +0 -0
  20. {codecortex-0.25.0 → codecortex-0.26.0}/setup.cfg +0 -0
  21. {codecortex-0.25.0 → codecortex-0.26.0}/src/codecortex.egg-info/dependency_links.txt +0 -0
  22. {codecortex-0.25.0 → codecortex-0.26.0}/src/codecortex.egg-info/entry_points.txt +0 -0
  23. {codecortex-0.25.0 → codecortex-0.26.0}/src/codecortex.egg-info/requires.txt +0 -0
  24. {codecortex-0.25.0 → codecortex-0.26.0}/src/codecortex.egg-info/top_level.txt +0 -0
  25. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/__main__.py +0 -0
  26. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/agent_prompt.py +0 -0
  27. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/auth.py +0 -0
  28. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/c4.py +0 -0
  29. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/c4_check.py +0 -0
  30. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/c4_layers.py +0 -0
  31. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/cache.py +0 -0
  32. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/changed_range.py +0 -0
  33. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/commands/__init__.py +0 -0
  34. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/commands/_common.py +0 -0
  35. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/commands/c4.py +0 -0
  36. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/commands/doctor.py +0 -0
  37. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/commands/gen_token.py +0 -0
  38. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/commands/graph.py +0 -0
  39. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/commands/index.py +0 -0
  40. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/commands/install.py +0 -0
  41. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/commands/map.py +0 -0
  42. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/commands/prompt.py +0 -0
  43. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/commands/query.py +0 -0
  44. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/commands/reset.py +0 -0
  45. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/commands/serve.py +0 -0
  46. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/commands/serve_http.py +0 -0
  47. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/commands/setup.py +0 -0
  48. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/commands/status.py +0 -0
  49. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/commands/uninstall.py +0 -0
  50. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/config.py +0 -0
  51. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/containment.py +0 -0
  52. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/doctor.py +0 -0
  53. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/filelock.py +0 -0
  54. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/gateway.py +0 -0
  55. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/graph_backend.py +0 -0
  56. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/graph_resolution.py +0 -0
  57. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/graph_targets.py +0 -0
  58. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/grapher.py +0 -0
  59. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/http_server.py +0 -0
  60. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/indexer.py +0 -0
  61. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/injector.py +0 -0
  62. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/installer.py +0 -0
  63. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/lang_config.py +0 -0
  64. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/loc.py +0 -0
  65. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/logconfig.py +0 -0
  66. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/mapper.py +0 -0
  67. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/metrics.py +0 -0
  68. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/onboarding.py +0 -0
  69. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/outcome.py +0 -0
  70. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/paths.py +0 -0
  71. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/per_thread.py +0 -0
  72. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/policy.py +0 -0
  73. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/progress.py +0 -0
  74. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/provider.py +0 -0
  75. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/providers/__init__.py +0 -0
  76. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/providers/graph.py +0 -0
  77. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/providers/lsp.py +0 -0
  78. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/providers/none.py +0 -0
  79. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/providers/semantic.py +0 -0
  80. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/py.typed +0 -0
  81. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/query_ops.py +0 -0
  82. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/redact.py +0 -0
  83. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/reindexer.py +0 -0
  84. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/reset.py +0 -0
  85. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/searcher.py +0 -0
  86. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/semantic_db.py +0 -0
  87. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/server.py +0 -0
  88. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/source_kind.py +0 -0
  89. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/symbol_diff.py +0 -0
  90. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/term.py +0 -0
  91. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/verify.py +0 -0
  92. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/viewer/__init__.py +0 -0
  93. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/viewer/graph_template.html +0 -0
  94. {codecortex-0.25.0 → codecortex-0.26.0}/src/codeintel/wire_text.py +0 -0
  95. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_agent_prompt.py +0 -0
  96. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_backend_failure_detail.py +0 -0
  97. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_backend_reaper.py +0 -0
  98. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_background_index_failure.py +0 -0
  99. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_bench_corpus_typed.py +0 -0
  100. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_bench_gate.py +0 -0
  101. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_bench_oracle.py +0 -0
  102. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_bench_oracle_ts.py +0 -0
  103. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_c4.py +0 -0
  104. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_c4_check.py +0 -0
  105. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_c4_layers.py +0 -0
  106. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_c4_model.py +0 -0
  107. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_c4_output.py +0 -0
  108. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_cache.py +0 -0
  109. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_changed_range.py +0 -0
  110. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_chunking.py +0 -0
  111. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_cli_commands.py +0 -0
  112. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_cli_help.py +0 -0
  113. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_cli_install_dry_run.py +0 -0
  114. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_cli_install_offer_injection.py +0 -0
  115. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_cold_process.py +0 -0
  116. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_config.py +0 -0
  117. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_corpus.py +0 -0
  118. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_could_not_ask_semantic.py +0 -0
  119. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_docs_ci_claims.py +0 -0
  120. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_docs_deadcode_withdrawal.py +0 -0
  121. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_doctor.py +0 -0
  122. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_e2e.py +0 -0
  123. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_edge_confidence.py +0 -0
  124. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_edge_endpoint_cap.py +0 -0
  125. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_engine_adoption.py +0 -0
  126. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_enterprise.py +0 -0
  127. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_gateway.py +0 -0
  128. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_graph_failure_population.py +0 -0
  129. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_graph_provider.py +0 -0
  130. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_graph_real.py +0 -0
  131. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_graph_stdin.py +0 -0
  132. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_grapher.py +0 -0
  133. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_hard_exit.py +0 -0
  134. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_hardening.py +0 -0
  135. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_http_auth.py +0 -0
  136. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_http_server.py +0 -0
  137. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_incompleteness.py +0 -0
  138. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_index_failure_reason.py +0 -0
  139. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_index_incompleteness.py +0 -0
  140. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_index_progress.py +0 -0
  141. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_indexer_binary.py +0 -0
  142. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_installer.py +0 -0
  143. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_integration.py +0 -0
  144. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_lang_config.py +0 -0
  145. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_loc_census.py +0 -0
  146. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_lsp_language_coverage.py +0 -0
  147. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_lsp_provider.py +0 -0
  148. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_lsp_real.py +0 -0
  149. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_mapper.py +0 -0
  150. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_mcp_handshake.py +0 -0
  151. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_mcp_server.py +0 -0
  152. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_missing_target.py +0 -0
  153. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_model_dimension.py +0 -0
  154. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_never_raise.py +0 -0
  155. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_null_reason_and_ripple.py +0 -0
  156. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_onboarding.py +0 -0
  157. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_per_thread.py +0 -0
  158. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_query_budget.py +0 -0
  159. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_rbac.py +0 -0
  160. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_redaction_boundary.py +0 -0
  161. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_reindex_lock.py +0 -0
  162. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_reindexer.py +0 -0
  163. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_rerank.py +0 -0
  164. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_reset.py +0 -0
  165. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_semantic_provider.py +0 -0
  166. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_source_kind.py +0 -0
  167. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_staleness.py +0 -0
  168. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_symbol_context.py +0 -0
  169. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_symbol_diff.py +0 -0
  170. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_term.py +0 -0
  171. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_treesitter.py +0 -0
  172. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_uninstall.py +0 -0
  173. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_upgrade_path.py +0 -0
  174. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_verify.py +0 -0
  175. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_verify_call.py +0 -0
  176. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_version_skew.py +0 -0
  177. {codecortex-0.25.0 → codecortex-0.26.0}/tests/test_wire_text.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.25.0
3
+ Version: 0.26.0
4
4
  Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
5
5
  Author: Shammai Hamilton
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.25.0
3
+ Version: 0.26.0
4
4
  Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
5
5
  Author: Shammai Hamilton
6
6
  License-Expression: MIT
@@ -25,6 +25,7 @@ src/codeintel/graph_answer.py
25
25
  src/codeintel/graph_backend.py
26
26
  src/codeintel/graph_changed.py
27
27
  src/codeintel/graph_confidence.py
28
+ src/codeintel/graph_dispatch.py
28
29
  src/codeintel/graph_edges.py
29
30
  src/codeintel/graph_ops.py
30
31
  src/codeintel/graph_render.py
@@ -90,6 +91,7 @@ tests/test_backend_reaper.py
90
91
  tests/test_background_index_failure.py
91
92
  tests/test_bench_corpus_typed.py
92
93
  tests/test_bench_gate.py
94
+ tests/test_bench_graphify.py
93
95
  tests/test_bench_oracle.py
94
96
  tests/test_bench_oracle_ts.py
95
97
  tests/test_c4.py
@@ -108,6 +110,7 @@ tests/test_cold_process.py
108
110
  tests/test_config.py
109
111
  tests/test_corpus.py
110
112
  tests/test_could_not_ask_semantic.py
113
+ tests/test_dispatch_callers.py
111
114
  tests/test_docs_ci_claims.py
112
115
  tests/test_docs_deadcode_withdrawal.py
113
116
  tests/test_docs_trust_claims.py
@@ -0,0 +1 @@
1
+ __version__ = "0.26.0"
@@ -605,7 +605,7 @@ class AnswerRendering:
605
605
  guessed_by.add(str(r.get("strategy") or "").strip())
606
606
  unverified += 1
607
607
  continue
608
- if evidence in ("lsp", "import", "same-module"):
608
+ if evidence in ("lsp", "import", "same-module", "self-mro"):
609
609
  r["_evidence"] = evidence
610
610
  if evidence == "same-module":
611
611
  # `same_module` is a bare-name lookup inside the caller's own file, and it
@@ -806,7 +806,8 @@ class AnswerRendering:
806
806
  tally = " · ".join(f"{n} {b}" for b, n in present)
807
807
  return (
808
808
  f"**{tally}.** The heading counts rows, not confirmed {unit}s — only the `{_RESOLVED}` "
809
- f"rows were bound by an import, a language server or the caller's own module scope. "
809
+ f"rows were bound by an import, a language server, the caller's own module scope or, for "
810
+ f"a `self.` call, the caller's own class hierarchy (`self_mro`, over INHERITS edges). "
810
811
  f"Per-row detail below.\n"
811
812
  )
812
813
 
@@ -905,10 +906,11 @@ class AnswerRendering:
905
906
  structured row and the printed row cannot disagree about a fact.
906
907
 
907
908
  `verified` is the field to filter on and the only one that is a VERDICT: true exactly when
908
- a binding was followed — through an import, a language server, or (for a bare call to a
909
- symbol the caller's own module defines) the module's own scope. `why` says WHICH, per
910
- strategy, because the three are different strengths of claim and one sentence for the
911
- bucket was false for one of them. An unstamped row is not verified — silence from the
909
+ a binding was followed — through an import, a language server, (for a bare call to a symbol
910
+ the caller's own module defines) the module's own scope, or (for a `self.m()` call) the
911
+ caller's own class hierarchy, over INHERITS edges. `why` says WHICH, per strategy, because
912
+ the four are different strengths of claim and one sentence for the bucket was false for one
913
+ of them. An unstamped row is not verified — silence from the
912
914
  backend is not evidence — and it is not `possible` either, which is why `evidence` keeps
913
915
  all three states rather than collapsing to a boolean pair.
914
916
 
@@ -926,7 +928,7 @@ class AnswerRendering:
926
928
  confidence = row.get("_low_confidence")
927
929
  scope = row.get("_module_scope")
928
930
  edge = str(row.get("type(c)") or "").strip()
929
- return {
931
+ out: dict[str, Any] = {
930
932
  "relation": unit,
931
933
  "name": str(row.get(name_key) or ""),
932
934
  "qualified_name": _strip_project_prefix(
@@ -940,6 +942,16 @@ class AnswerRendering:
940
942
  "confidence": float(confidence) if confidence is not None else None,
941
943
  "why": _why(row, bucket),
942
944
  }
945
+ via = row.get("_via")
946
+ if via:
947
+ # Only on a row that reached the symbol through a base, so every other answer keeps the
948
+ # exact row shape it always had. `via` is the base method the call was actually resolved
949
+ # to; `via_kind` says whether that base is a Protocol the symbol satisfies structurally or
950
+ # a class it inherits from. Neither changes `verified`, which stays false: a call to the
951
+ # base is not a binding to this override.
952
+ out["via"] = str(via)
953
+ out["via_kind"] = str(row.get("_via_kind") or "")
954
+ return out
943
955
 
944
956
  def _record_rows(self, rendered: list[dict], row_keys: tuple[str, str, str], unit: str,
945
957
  *, row_cap_hit: bool, withheld: int) -> None:
@@ -1046,6 +1058,13 @@ class AnswerRendering:
1046
1058
  if ev["verified"] or ev["possible"]:
1047
1059
  lines.append(f"> Verified {noun}s: {ev['verified']} · "
1048
1060
  f"possible: {ev['possible']} · unstated: {ev['unstated']}")
1061
+ # Said here because `possible` alone reads as "weak evidence about this symbol", and a row
1062
+ # that called the BASE method is a different claim: it may be the only production caller
1063
+ # there is, and no binding ties it to the symbol asked about.
1064
+ through = sum(1 for r in self._pending_rows if r.get("via"))
1065
+ if through:
1066
+ lines.append(f"> Of the possible {noun}s, {through} call a base type or Protocol rather "
1067
+ "than this symbol, and reach it only by dispatch")
1049
1068
  if ev["truncated"]:
1050
1069
  total = ev["total"]
1051
1070
  shown = (f"{ev['returned']} shown, {total} in total" if total is not None
@@ -111,6 +111,9 @@ class _Lookup:
111
111
  detail: str = ""
112
112
  untouched: list[dict] = field(default_factory=list)
113
113
  also: list[dict] = field(default_factory=list)
114
+ # Callers of a BASE method (`graph_dispatch.py`). Kept out of the two lists above: whether the
115
+ # diff touched such a caller says nothing about whether its author considered THIS symbol.
116
+ through: list[dict] = field(default_factory=list)
114
117
  mentions: list[dict] = field(default_factory=list)
115
118
  quiet: int = 0 # mentions that are only comments or definitions
116
119
  withheld: int = 0 # rows found and not printed
@@ -396,15 +399,19 @@ class ChangedSince(AnswerRendering):
396
399
  if rows:
397
400
  lk.state = "ok"
398
401
  for r in rows:
399
- (lk.also if self._caller_is_changed(r, touched) else lk.untouched).append(
400
- dict(r, module_scope_in_diff=bool(
401
- r.get("module_scope") and str(r.get("file") or "") in files)))
402
+ row = dict(r, module_scope_in_diff=bool(
403
+ r.get("module_scope") and str(r.get("file") or "") in files))
404
+ if r.get("via"):
405
+ lk.through.append(row)
406
+ else:
407
+ (lk.also if self._caller_is_changed(r, touched) else lk.untouched).append(row)
402
408
  shown = self._RANGE_ROWS_SHOWN
403
409
  # Two different ways a known row goes unprinted, and both are WITHHELD: the rows this
404
410
  # group does not print (its own per-list limit), and the ones `callers` itself kept back
405
411
  # (its distinct-caller cap) — which it counts and does not publish. Counting only the
406
412
  # first made "120 callers, 50 kept" read as "50 in total" in the envelope.
407
413
  lk.withheld = (max(0, len(lk.untouched) - shown) + max(0, len(lk.also) - shown)
414
+ + sum(max(0, len(rs) - shown) for rs in _by_base(lk.through).values())
408
415
  + omitted)
409
416
  lk.complete = not lk.cap_hit and not lk.gaps
410
417
  if miss is not None:
@@ -638,7 +645,9 @@ class ChangedSince(AnswerRendering):
638
645
  if lk.source == "text":
639
646
  return [dict(r, group_class=klass) for r in lk.mentions[:shown]]
640
647
  out: list[dict[str, Any]] = []
641
- for status, rows in (("untouched", lk.untouched), ("also-changed", lk.also)):
648
+ lists = [("untouched", lk.untouched), ("also-changed", lk.also),
649
+ *(("through-base", rs) for rs in _by_base(lk.through).values())]
650
+ for status, rows in lists:
642
651
  out.extend(dict(r, changed_symbol=e.qn, change=e.change.change,
643
652
  caller_status=status, group_class=klass) for r in rows[:shown])
644
653
  return out
@@ -653,7 +662,7 @@ class ChangedSince(AnswerRendering):
653
662
  graph lookup produced is `advisory`."""
654
663
  if lk.source == "text":
655
664
  return _DISCOVERY
656
- rows = lk.untouched + lk.also
665
+ rows = lk.untouched + lk.also + lk.through
657
666
  if (lk.state == "ok" and lk.complete and rows and not lk.withheld
658
667
  and all(r.get("verified") is True for r in rows)):
659
668
  return _EVIDENCE
@@ -670,11 +679,16 @@ class ChangedSince(AnswerRendering):
670
679
  raw["_module_scope"] = row.get("file")
671
680
  if row.get("evidence") == _NAME_MATCHED:
672
681
  raw["_low_confidence"] = row.get("confidence")
682
+ if row.get("via"):
683
+ # A caller of a BASE method (see `graph_dispatch.py`): the badge says which kind of
684
+ # base, and its strategy — whatever it is — is the account of how the call reached
685
+ # the base, so the `same_module` reading below must not be applied to it.
686
+ raw["_via"], raw["_via_kind"] = row.get("via"), row.get("via_kind")
673
687
  # A `same_module` edge is only ever name-matched because the call-site check found it
674
688
  # written through a receiver (see `_same_module_call`), so the strategy IS the verdict.
675
689
  # Without it the badge reads `[?0.90]` — the backend's high score behind a question
676
690
  # mark, which is the reading `callers` prints "qualified call" to prevent.
677
- if _evidence_class(str(row.get("strategy") or "")) == "same-module":
691
+ elif _evidence_class(str(row.get("strategy") or "")) == "same-module":
678
692
  raw["_same_module"] = _SAME_MODULE_FOREIGN
679
693
  line = self._display(raw, "a.name", "a.qualified_name", "a.file_path")
680
694
  if row.get("module_scope_in_diff"):
@@ -702,13 +716,24 @@ class ChangedSince(AnswerRendering):
702
716
  out = [self._group_heading(lk)]
703
717
  shown = self._RANGE_ROWS_SHOWN
704
718
  if lk.state == "ok":
705
- for title, rows in (
706
- ("Callers this diff did NOT touch — these may break", lk.untouched),
707
- ("Callers also changed in this diff — probably updated together", lk.also),
708
- ):
719
+ lists = [
720
+ (f"**Callers this diff did NOT touch — these may break ({len(lk.untouched)}):**",
721
+ lk.untouched),
722
+ (f"**Callers also changed in this diff — probably updated together ({len(lk.also)}):**",
723
+ lk.also),
724
+ # Apart from the two above, and one list per base: a caller of the BASE is not a
725
+ # caller of this symbol, and whether the diff touched it does not say that its
726
+ # author considered this override. It may be the only production caller there is.
727
+ *((f"**Callers through `{_short_base(via)}` — they call the "
728
+ f"{'base class' if rs[0].get('via_kind') == 'base' else 'Protocol'}, not this symbol, "
729
+ "and reach it only by dispatch; never counted as verified callers "
730
+ f"({len(rs)}):**", rs)
731
+ for via, rs in _by_base(lk.through).items()),
732
+ ]
733
+ for title, rows in lists:
709
734
  if not rows:
710
735
  continue
711
- out.append(f"**{title} ({len(rows)}):**")
736
+ out.append(title)
712
737
  out.extend(self._caller_line(r) for r in rows[:shown])
713
738
  if len(rows) > shown:
714
739
  out.append(f"… (+{len(rows) - shown} more, not shown)")
@@ -854,6 +879,20 @@ class ChangedSince(AnswerRendering):
854
879
 
855
880
  # ----------------------------------------------------------------------------- module helpers
856
881
 
882
+ def _by_base(rows: list[dict]) -> dict[str, list[dict]]:
883
+ """Rows that reached a symbol through a base, grouped by the base method they called, in the
884
+ order the bases first appear."""
885
+ out: dict[str, list[dict]] = {}
886
+ for r in rows:
887
+ out.setdefault(str(r.get("via") or ""), []).append(r)
888
+ return out
889
+
890
+
891
+ def _short_base(via: str) -> str:
892
+ """`Base.m` from the qualified name of a base method."""
893
+ return ".".join(via.split(".")[-2:])
894
+
895
+
857
896
  def _diff_change(root: str, base_sha: str, change: FileChange) -> FileSymbolDiff:
858
897
  """One file's symbol diff. A side that SHOULD exist and cannot be read is `unparsable`, not
859
898
  absent: passing `None` for it would classify every definition in the file as added or removed."""
@@ -68,6 +68,11 @@ def _evidence_class(strategy: str) -> str:
68
68
  return "import"
69
69
  if st.startswith("same_module"):
70
70
  return "same-module"
71
+ # Not a strategy the backend reports: the label this project gives an edge it RE-RESOLVED from the
72
+ # class hierarchy (`self.m()` bound through the caller's own ancestry — `graph_dispatch.py`). It
73
+ # is classified here, with the others, so every reader of `strategy` agrees it is a resolution.
74
+ if st.startswith("self_mro"):
75
+ return "self-mro"
71
76
  if st.startswith("language_rule"):
72
77
  return "language-rule"
73
78
  if st.startswith("unresolved"):
@@ -227,7 +232,7 @@ def _edge_strength(row: dict) -> tuple[int, float]:
227
232
  """
228
233
  evidence = _evidence_class(str(row.get("strategy") or ""))
229
234
  conf = _edge_confidence(row)
230
- if evidence in ("lsp", "import"):
235
+ if evidence in ("lsp", "import", "self-mro"):
231
236
  rank = 0
232
237
  elif evidence == "same-module":
233
238
  rank = 2 if _same_module_call(row) == _SAME_MODULE_FOREIGN else 0
@@ -246,6 +251,12 @@ def _callee_for_display(callee: str) -> str:
246
251
  return flat if len(flat) <= 60 else flat[:57] + "..."
247
252
 
248
253
 
254
+ def _via_kind(row: dict) -> str:
255
+ """`protocol`, `protocol-undecided` or `base`: what kind of base method a `via` row called."""
256
+ kind = str(row.get("_via_kind") or "")
257
+ return kind if kind in ("protocol", "protocol-undecided") else "base"
258
+
259
+
249
260
  def _why(row: dict, bucket: str) -> str:
250
261
  """One sentence saying what ACTUALLY happened to produce this row's bucket.
251
262
 
@@ -256,8 +267,37 @@ def _why(row: dict, bucket: str) -> str:
256
267
  stronger.
257
268
  """
258
269
  strategy = str(row.get("strategy") or "").strip()
270
+ via = str(row.get("_via") or "")
271
+ if via:
272
+ # A caller of a BASE method listed under a symbol that overrides it. Whatever the edge's own
273
+ # strategy was, it bound the call to `via` and not to this symbol, so the row is never
274
+ # `resolved` for the symbol asked about; the strategy is kept here as the account of how it
275
+ # reached `via`, because that is the part of the claim the backend did make.
276
+ kind = "base class" if _via_kind(row) == "base" else "Protocol"
277
+ how = (f"the receiver's declared type (`{strategy}`)" if strategy == "field_type_hint"
278
+ else f"`{strategy}`" if strategy else "an edge the backend left unlabelled")
279
+ text = (f"resolved to the {kind} method `{via}` by {how}, not to this symbol; it reaches this "
280
+ "override only by dispatch, when the object is an instance of this symbol's class, "
281
+ "so no binding the index records ties it to this symbol")
282
+ # Each of these is a way the claim is weaker than the line above says, stated on the row so
283
+ # it is not only in a heading three screens up.
284
+ if _via_kind(row) == "protocol-undecided":
285
+ text += ("; and whether that class satisfies the Protocol is undecided — an attribute or "
286
+ "property could supply a member it lacks as a method")
287
+ if row.get("_via_how") == "base_classes":
288
+ text += ("; and the link from this symbol's class to that base was found by resolving the "
289
+ "base-class NAME its statement wrote, not from an INHERITS edge")
290
+ if row.get("_via_super"):
291
+ text += (f"; it is a `super()` call inside `{row['_via_super']}`, a class that descends from "
292
+ "this symbol's class, so it can reach this symbol")
293
+ elif row.get("_via_super_unplaced"):
294
+ text += ("; it is a `super()` call from a class the index could not place in this symbol's "
295
+ "hierarchy, so whether it can reach this symbol was not decided")
296
+ return text
259
297
  if bucket == _RESOLVED:
260
298
  evidence = str(row.get("_evidence") or "") or _evidence_class(strategy)
299
+ if evidence == "self-mro":
300
+ return str(row.get("_self_mro") or "resolved through the caller's own class hierarchy")
261
301
  if evidence == "lsp":
262
302
  return f"a language server resolved the call to this symbol ({strategy})"
263
303
  if evidence == "import":
@@ -295,8 +335,12 @@ def _why(row: dict, bucket: str) -> str:
295
335
  return (f"matched by name inside the caller's own module, but the call is written "
296
336
  f"`{callee}` — through a receiver, not as this module's own symbol — so the "
297
337
  f"match is a guess about what the receiver is ({strategy})")
298
- return (f"matched by name ({strategy})" if strategy
338
+ text = (f"matched by name ({strategy})" if strategy
299
339
  else "matched by bare symbol name, not by following a binding")
340
+ # A `self.m()` the class hierarchy would bind if one of its links were an edge: said here so
341
+ # a reader who sees a `self.` call still badged knows the rule looked and why it declined.
342
+ note = str(row.get("_hierarchy_note") or "")
343
+ return f"{text}; {note}" if note else text
300
344
  if bucket == _UNSTATED:
301
345
  return "the backend reported no provenance for this edge"
302
346
  return "unclassified"
@@ -311,6 +355,12 @@ def _confidence_badge(row: dict) -> str:
311
355
  if "_low_confidence" not in row:
312
356
  return ""
313
357
  conf = row.get("_low_confidence")
358
+ if row.get("_via"):
359
+ # The backend's score is for the edge to the BASE method — kept on show as detail, as every
360
+ # other badge does — but the verdict is about this symbol, and the glyph says which kind of
361
+ # base the call went through, since that is what a reader scanning the list needs to know.
362
+ kind = _via_kind(row)
363
+ return f" [?via {kind}]" if conf is None else f" [?via {kind} {float(conf):.2f}]"
314
364
  # A `same_module` edge this module refused to trust keeps the backend's own score on show — the
315
365
  # number is the backend's and is still detail — but the verdict is OURS, and a bare `[?0.90]`
316
366
  # reads as high confidence behind a question mark. Say what the doubt is about.