codecortex 0.26.0__tar.gz → 0.27.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.
- {codecortex-0.26.0/src/codecortex.egg-info → codecortex-0.27.0}/PKG-INFO +1 -1
- {codecortex-0.26.0 → codecortex-0.27.0/src/codecortex.egg-info}/PKG-INFO +1 -1
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codecortex.egg-info/SOURCES.txt +3 -0
- codecortex-0.27.0/src/codeintel/__init__.py +1 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/changed_range.py +5 -1
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/gateway.py +41 -2
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/graph_answer.py +315 -39
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/graph_changed.py +52 -1
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/graph_confidence.py +67 -6
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/graph_dispatch.py +50 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/graph_edges.py +7 -1
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/graph_ops.py +5 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/provider.py +6 -4
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/providers/graph.py +10 -0
- codecortex-0.27.0/src/codeintel/qualifier_scan.py +388 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/server.py +17 -6
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_bench_graphify.py +11 -2
- codecortex-0.27.0/tests/test_bench_qualified.py +157 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_changed_range.py +175 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_dispatch_callers.py +105 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_docs_trust_claims.py +87 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_edge_confidence.py +656 -2
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_gateway.py +66 -0
- codecortex-0.27.0/tests/test_qualifier_scan.py +596 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_summary_integrity.py +13 -1
- codecortex-0.26.0/src/codeintel/__init__.py +0 -1
- {codecortex-0.26.0 → codecortex-0.27.0}/LICENSE +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/README.md +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/pyproject.toml +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/setup.cfg +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codecortex.egg-info/dependency_links.txt +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codecortex.egg-info/entry_points.txt +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codecortex.egg-info/requires.txt +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codecortex.egg-info/top_level.txt +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/__main__.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/agent_prompt.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/auth.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/c4.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/c4_check.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/c4_layers.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/cache.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/commands/__init__.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/commands/_common.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/commands/c4.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/commands/doctor.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/commands/gen_token.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/commands/graph.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/commands/index.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/commands/install.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/commands/map.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/commands/prompt.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/commands/query.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/commands/reset.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/commands/serve.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/commands/serve_http.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/commands/setup.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/commands/status.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/commands/uninstall.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/config.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/containment.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/doctor.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/filelock.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/graph_backend.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/graph_render.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/graph_resolution.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/graph_targets.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/grapher.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/http_server.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/indexer.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/injector.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/installer.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/lang_config.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/loc.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/logconfig.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/mapper.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/metrics.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/onboarding.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/outcome.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/paths.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/per_thread.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/policy.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/progress.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/providers/__init__.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/providers/lsp.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/providers/none.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/providers/semantic.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/py.typed +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/query_ops.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/redact.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/reindexer.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/reset.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/searcher.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/semantic_db.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/source_kind.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/symbol_diff.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/term.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/verify.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/viewer/__init__.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/viewer/graph_template.html +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/src/codeintel/wire_text.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_agent_prompt.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_backend_failure_detail.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_backend_reaper.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_background_index_failure.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_bench_corpus_typed.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_bench_gate.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_bench_oracle.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_bench_oracle_ts.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_c4.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_c4_check.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_c4_layers.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_c4_model.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_c4_output.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_cache.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_chunking.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_cli_commands.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_cli_help.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_cli_install_dry_run.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_cli_install_offer_injection.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_cold_process.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_config.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_corpus.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_could_not_ask_semantic.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_docs_ci_claims.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_docs_deadcode_withdrawal.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_doctor.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_e2e.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_edge_endpoint_cap.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_engine_adoption.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_enterprise.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_graph_failure_population.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_graph_provider.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_graph_real.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_graph_stdin.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_grapher.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_hard_exit.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_hardening.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_http_auth.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_http_server.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_incompleteness.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_index_failure_reason.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_index_incompleteness.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_index_progress.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_indexer_binary.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_installer.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_integration.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_lang_config.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_loc_census.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_lsp_language_coverage.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_lsp_provider.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_lsp_real.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_mapper.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_mcp_handshake.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_mcp_server.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_missing_target.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_model_dimension.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_never_raise.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_null_reason_and_ripple.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_onboarding.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_per_thread.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_query_budget.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_rbac.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_redaction_boundary.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_reindex_lock.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_reindexer.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_rerank.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_reset.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_semantic_provider.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_source_kind.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_staleness.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_symbol_context.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_symbol_diff.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_term.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_treesitter.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_uninstall.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_upgrade_path.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_verify.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_verify_call.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.0}/tests/test_version_skew.py +0 -0
- {codecortex-0.26.0 → codecortex-0.27.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.
|
|
3
|
+
Version: 0.27.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.
|
|
3
|
+
Version: 0.27.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
|
|
@@ -49,6 +49,7 @@ src/codeintel/policy.py
|
|
|
49
49
|
src/codeintel/progress.py
|
|
50
50
|
src/codeintel/provider.py
|
|
51
51
|
src/codeintel/py.typed
|
|
52
|
+
src/codeintel/qualifier_scan.py
|
|
52
53
|
src/codeintel/query_ops.py
|
|
53
54
|
src/codeintel/redact.py
|
|
54
55
|
src/codeintel/reindexer.py
|
|
@@ -94,6 +95,7 @@ tests/test_bench_gate.py
|
|
|
94
95
|
tests/test_bench_graphify.py
|
|
95
96
|
tests/test_bench_oracle.py
|
|
96
97
|
tests/test_bench_oracle_ts.py
|
|
98
|
+
tests/test_bench_qualified.py
|
|
97
99
|
tests/test_c4.py
|
|
98
100
|
tests/test_c4_check.py
|
|
99
101
|
tests/test_c4_layers.py
|
|
@@ -151,6 +153,7 @@ tests/test_never_raise.py
|
|
|
151
153
|
tests/test_null_reason_and_ripple.py
|
|
152
154
|
tests/test_onboarding.py
|
|
153
155
|
tests/test_per_thread.py
|
|
156
|
+
tests/test_qualifier_scan.py
|
|
154
157
|
tests/test_query_budget.py
|
|
155
158
|
tests/test_rbac.py
|
|
156
159
|
tests/test_redaction_boundary.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.27.0"
|
|
@@ -28,6 +28,7 @@ from __future__ import annotations
|
|
|
28
28
|
|
|
29
29
|
import os
|
|
30
30
|
import re
|
|
31
|
+
import stat
|
|
31
32
|
import subprocess
|
|
32
33
|
import tempfile
|
|
33
34
|
import threading
|
|
@@ -323,7 +324,10 @@ def read_new(root: str, path: str) -> str | None:
|
|
|
323
324
|
if safe is None:
|
|
324
325
|
return None
|
|
325
326
|
try:
|
|
326
|
-
|
|
327
|
+
info = os.stat(safe)
|
|
328
|
+
# A regular file only. A FIFO planted at a tracked path has one link and a size of zero, passes
|
|
329
|
+
# containment, and blocks `open` forever — which would hang the whole answer.
|
|
330
|
+
if not stat.S_ISREG(info.st_mode) or info.st_size > MAX_SOURCE_BYTES:
|
|
327
331
|
return None
|
|
328
332
|
with open(safe, "rb") as fh:
|
|
329
333
|
return fh.read().decode("utf-8", "replace")
|
|
@@ -83,6 +83,41 @@ def _is_retryable(result: Result) -> bool:
|
|
|
83
83
|
)
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
def _states_live_file_contents(result: Result | dict[str, Result]) -> bool:
|
|
87
|
+
"""Whether the answer states something read from the files on disk as it was asked — the qualifier
|
|
88
|
+
scan's verdicts (`qualifier_seen` on a row, or the `qualifier_absent` / `qualifier_present`
|
|
89
|
+
counts).
|
|
90
|
+
|
|
91
|
+
The cache is keyed by the target string and the index's freshness generation, and neither moves
|
|
92
|
+
when a caller file starts or stops writing the qualifier — only a reindex does. Caching such an
|
|
93
|
+
answer would serve a verdict about the files as they WERE, in prose that says "as they are on
|
|
94
|
+
disk": the reason `changed` is never cached, met by a narrower population. An answer whose scan
|
|
95
|
+
judged nothing printed only the command — but if it was eligible and judged nothing because a
|
|
96
|
+
call stalled, the budget ran out or a lookup timed out (`qualifier_checked`), caching that would
|
|
97
|
+
keep the check switched off for the symbol until the next reindex, long after the filesystem
|
|
98
|
+
recovered. So an answer whose check was attempted at all is not cached either.
|
|
99
|
+
Walks the whole answer, because a fan-out answer nests each engine's envelope."""
|
|
100
|
+
def walk(node: object, depth: int) -> bool:
|
|
101
|
+
if depth > 8:
|
|
102
|
+
return False
|
|
103
|
+
if isinstance(node, dict):
|
|
104
|
+
evidence = node.get("evidence")
|
|
105
|
+
if isinstance(evidence, dict) and (evidence.get("qualifier_absent")
|
|
106
|
+
or evidence.get("qualifier_present")
|
|
107
|
+
or evidence.get("qualifier_checked")):
|
|
108
|
+
return True
|
|
109
|
+
rows = node.get("rows")
|
|
110
|
+
if isinstance(rows, list) and any(
|
|
111
|
+
isinstance(r, dict) and r.get("qualifier_seen") is not None for r in rows):
|
|
112
|
+
return True
|
|
113
|
+
return any(walk(v, depth + 1) for v in node.values() if isinstance(v, (dict, list)))
|
|
114
|
+
if isinstance(node, list):
|
|
115
|
+
return any(walk(v, depth + 1) for v in node)
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
return walk(result, 0)
|
|
119
|
+
|
|
120
|
+
|
|
86
121
|
class Gateway:
|
|
87
122
|
def __init__(self, graph=None, lsp=None, semantic=None, policy: TieringPolicy | None = None,
|
|
88
123
|
reindexer: Reindexer | None = None, oneshot: bool = False):
|
|
@@ -682,7 +717,10 @@ class Gateway:
|
|
|
682
717
|
engines = ["graph", "lsp"] if engine_str == "both" else ["graph", "lsp", "semantic"]
|
|
683
718
|
fan_results = self._fan_out(engines, op_str, target_str, budget, project_root)
|
|
684
719
|
result = self._merge(fan_results, op_str, target_str, engine_str)
|
|
685
|
-
|
|
720
|
+
# The merged answer drops each engine's `rows` and `evidence` but keeps their prose,
|
|
721
|
+
# so whether it states what the files hold now is read from the engines' own answers.
|
|
722
|
+
if (not uncacheable and not _is_retryable(result)
|
|
723
|
+
and not _states_live_file_contents(fan_results)):
|
|
686
724
|
self._cache.put(op_str, target_str, cache_engine, root_str, result, freshness)
|
|
687
725
|
return _mark_reindexing(result, reindexing)
|
|
688
726
|
|
|
@@ -721,7 +759,8 @@ class Gateway:
|
|
|
721
759
|
result = self._cross_check_name_resolved(
|
|
722
760
|
result, op_str, target_str, budget, project_root, was_auto)
|
|
723
761
|
|
|
724
|
-
if not uncacheable and not _is_retryable(result)
|
|
762
|
+
if (not uncacheable and not _is_retryable(result)
|
|
763
|
+
and not _states_live_file_contents(result)):
|
|
725
764
|
self._cache.put(op_str, target_str, cache_engine, root_str, result, freshness)
|
|
726
765
|
return _mark_reindexing(result, reindexing)
|
|
727
766
|
|
|
@@ -21,10 +21,12 @@ from __future__ import annotations
|
|
|
21
21
|
|
|
22
22
|
from typing import TYPE_CHECKING, Any
|
|
23
23
|
|
|
24
|
+
from codeintel import qualifier_scan
|
|
24
25
|
from codeintel.graph_confidence import (
|
|
25
26
|
_EDGE_CONFIDENCE_FLOOR,
|
|
26
27
|
_EDGE_CONFIDENCE_WEAK,
|
|
27
28
|
_NAME_MATCHED,
|
|
29
|
+
_QUALIFIER_BYPASSES,
|
|
28
30
|
_RESOLVED,
|
|
29
31
|
_SAME_MODULE_FOREIGN,
|
|
30
32
|
_UNSTATED,
|
|
@@ -33,6 +35,9 @@ from codeintel.graph_confidence import (
|
|
|
33
35
|
_edge_confidence,
|
|
34
36
|
_edge_strength,
|
|
35
37
|
_evidence_class,
|
|
38
|
+
_is_own_receiver,
|
|
39
|
+
_own_receivers_of,
|
|
40
|
+
_qualifier_badge,
|
|
36
41
|
_same_module_call,
|
|
37
42
|
_why,
|
|
38
43
|
)
|
|
@@ -51,10 +56,18 @@ from codeintel.graph_render import (
|
|
|
51
56
|
_is_module_scope_node,
|
|
52
57
|
_is_non_code,
|
|
53
58
|
_lang_family,
|
|
59
|
+
_node_labels,
|
|
54
60
|
_strip_project_prefix,
|
|
55
61
|
)
|
|
56
62
|
from codeintel.graph_targets import _SymbolTarget
|
|
57
63
|
|
|
64
|
+
# What the discriminating token IS, and the two are not interchangeable. The first names a program
|
|
65
|
+
# entity a caller must get hold of to reach the symbol; the second names a path a caller may reach
|
|
66
|
+
# through a re-export without ever spelling it. Only the first is strong enough to scan on — see
|
|
67
|
+
# `_scan_for_qualifier`.
|
|
68
|
+
_WHY_QUALIFIED = "the name it is qualified by"
|
|
69
|
+
_WHY_MODULE = "the module it is defined in"
|
|
70
|
+
|
|
58
71
|
|
|
59
72
|
class AnswerRendering:
|
|
60
73
|
"""Row rendering, ambiguity disclosure and the notes that qualify a count.
|
|
@@ -69,6 +82,7 @@ class AnswerRendering:
|
|
|
69
82
|
# checker verifies it at the join, so a future host that does not record gaps fails here
|
|
70
83
|
# instead of silently dropping every caveat these methods raise.
|
|
71
84
|
_answered_root: str | None
|
|
85
|
+
_scan_budget: qualifier_scan.Budget | None
|
|
72
86
|
_pending_gaps: tuple[dict[str, Any], ...]
|
|
73
87
|
_pending_rows: tuple[dict[str, Any], ...]
|
|
74
88
|
_pending_row_cap: bool
|
|
@@ -101,7 +115,7 @@ class AnswerRendering:
|
|
|
101
115
|
# "N other reference(s)" is always supported by the rows beneath it.
|
|
102
116
|
kind = str(row.get("type(c)") or "").strip()
|
|
103
117
|
kind_badge = f" [{kind}]" if kind and kind not in ("CALLS", "USAGE") else ""
|
|
104
|
-
mark = kind_badge + _confidence_badge(row)
|
|
118
|
+
mark = kind_badge + _confidence_badge(row) + _qualifier_badge(row)
|
|
105
119
|
return f"- module scope of {where}{mark}" if where else f"- module scope{mark}"
|
|
106
120
|
name = str(row.get(name_key) or "?")
|
|
107
121
|
qn = _strip_project_prefix(str(row.get(qn_key) or ""), may_be_filename=False)
|
|
@@ -113,7 +127,7 @@ class AnswerRendering:
|
|
|
113
127
|
# A row the backend resolved by bare name rather than by import carries its score into the
|
|
114
128
|
# line itself. The summary note says how many there are; only the badge says WHICH, and a
|
|
115
129
|
# reader scanning for "is my symbol in here" reads rows, not notes.
|
|
116
|
-
badge += _confidence_badge(row)
|
|
130
|
+
badge += _confidence_badge(row) + _qualifier_badge(row)
|
|
117
131
|
return f"- {label}{badge}{tail}"
|
|
118
132
|
|
|
119
133
|
@staticmethod
|
|
@@ -240,17 +254,15 @@ class AnswerRendering:
|
|
|
240
254
|
return str(row.get(qn_key) or row.get(name_key) or ""), str(row.get(file_key) or "")
|
|
241
255
|
|
|
242
256
|
def is_test(row: dict) -> bool:
|
|
243
|
-
|
|
244
|
-
return tests_last and cls._looks_like_test(
|
|
245
|
-
str(scope) if scope is not None else str(row.get(file_key) or ""),
|
|
246
|
-
str(row.get(name_key) or ""))
|
|
257
|
+
return tests_last and cls._is_test_row(row, name_key, file_key)
|
|
247
258
|
|
|
248
259
|
# Production before tests inside each group, whether or not anything is cut. This is NOT the
|
|
249
|
-
# whole printed order: `_render_edge_answer` re-sorts every group afterwards by edge kind
|
|
250
|
-
# then by evidence bucket,
|
|
251
|
-
#
|
|
252
|
-
#
|
|
253
|
-
# within one kind of evidence
|
|
260
|
+
# whole printed order: `_render_edge_answer` re-sorts every group afterwards by edge kind,
|
|
261
|
+
# then by evidence bucket, then by production-versus-test again, and only then by what the
|
|
262
|
+
# qualifier scan found, so a RESOLVED test caller prints ahead of a name-matched production
|
|
263
|
+
# one. That is the one way a test precedes production, and it is the evidence sort's, not
|
|
264
|
+
# this partition's: within one kind of evidence production still comes first, which is what
|
|
265
|
+
# a reader who stops early gets.
|
|
254
266
|
if tests_last:
|
|
255
267
|
for group in groups:
|
|
256
268
|
group.rows.sort(key=is_test)
|
|
@@ -332,6 +344,15 @@ class AnswerRendering:
|
|
|
332
344
|
group.rows = keep
|
|
333
345
|
return dropped
|
|
334
346
|
|
|
347
|
+
@classmethod
|
|
348
|
+
def _is_test_row(cls, row: dict, name_key: str, file_key: str) -> bool:
|
|
349
|
+
"""Whether an edge row's displayed endpoint is test code — by the file it is attributed to,
|
|
350
|
+
which for module-scope code is the location it was relabelled to."""
|
|
351
|
+
scope = row.get("_module_scope")
|
|
352
|
+
return cls._looks_like_test(
|
|
353
|
+
str(scope) if scope is not None else str(row.get(file_key) or ""),
|
|
354
|
+
str(row.get(name_key) or ""))
|
|
355
|
+
|
|
335
356
|
@staticmethod
|
|
336
357
|
def _looks_like_test(fp: str, name: str) -> bool:
|
|
337
358
|
"""Heuristic test detection. The backend's own ``is_test`` flag comes back False for pytest
|
|
@@ -836,17 +857,197 @@ class AnswerRendering:
|
|
|
836
857
|
parts = [p for p in str(qualified).replace("/", ".").split(".") if p]
|
|
837
858
|
# The segment before the leaf, when it is not the leaf itself.
|
|
838
859
|
if len(parts) >= 2 and parts[-1] == wanted.name and parts[-2] != wanted.name:
|
|
839
|
-
return parts[-2],
|
|
860
|
+
return parts[-2], _WHY_QUALIFIED
|
|
840
861
|
defining = wanted.file_hint or (groups[0].file if groups else "")
|
|
841
862
|
stem = str(defining).replace("\\", "/").rsplit("/", 1)[-1].rsplit(".", 1)[0]
|
|
842
863
|
if stem and stem != wanted.name:
|
|
843
|
-
return stem,
|
|
864
|
+
return stem, _WHY_MODULE
|
|
844
865
|
return None
|
|
845
866
|
|
|
867
|
+
@staticmethod
|
|
868
|
+
def _shown_groups(groups: list[_EdgeGroup]) -> list[_EdgeGroup]:
|
|
869
|
+
"""The groups an edge answer PRINTS — the population the scan and the note about it describe.
|
|
870
|
+
|
|
871
|
+
Rows past the candidate cap are published by nobody (`_render_edge_answer` withholds them), so
|
|
872
|
+
a count that included them would be a count of rows no reader can see, and a verifier that
|
|
873
|
+
re-derives it from `rows[]` could not agree."""
|
|
874
|
+
return [g for g in groups if g.rows][:_CANDIDATE_CAP]
|
|
875
|
+
|
|
876
|
+
@classmethod
|
|
877
|
+
def _guesses_dominate(cls, groups: list[_EdgeGroup]) -> bool:
|
|
878
|
+
"""Whether name-matched rows are enough of the answer for a check on them to be stated — the
|
|
879
|
+
floor `_settle_name_matches` has always applied, and the one the scan applies BEFORE it reads a
|
|
880
|
+
file. Below it there is no note, so a scan that ran anyway would badge rows with the note and
|
|
881
|
+
its caveat nowhere in the body: a mark with the qualification left off.
|
|
882
|
+
|
|
883
|
+
"Dominate" is at least `_SETTLE_FLOOR` rows and at least half the answer, or all of it. Below
|
|
884
|
+
half, the resolved rows carry the answer and a command here would be noise on a result that is
|
|
885
|
+
mostly evidence."""
|
|
886
|
+
matched = sum(1 for g in groups for r in g.rows if r.get("_bucket") == _NAME_MATCHED)
|
|
887
|
+
total = sum(len(g.rows) for g in groups)
|
|
888
|
+
return matched >= cls._SETTLE_FLOOR and matched * 2 >= total
|
|
889
|
+
|
|
890
|
+
@staticmethod
|
|
891
|
+
def _names_a_class_member(group: _EdgeGroup) -> bool:
|
|
892
|
+
"""Whether the symbol this group's rows call is a METHOD, from the label the backend put on
|
|
893
|
+
the called node — the first of the two things that say the qualifier in front of it is a class.
|
|
894
|
+
|
|
895
|
+
`StrategyChain.resolve` and `api.routes.handle` are both dotted targets, and the text of the
|
|
896
|
+
target cannot tell them apart: the first names a class a caller must get hold of to reach the
|
|
897
|
+
method, the second a module a caller may reach through a re-export without ever spelling it.
|
|
898
|
+
The node can. Rows from a backend that does not report the label are not guessed at.
|
|
899
|
+
|
|
900
|
+
The label alone is not enough (`_owned_by` is the other half): a node can be labelled `Method`
|
|
901
|
+
and still have no class in front of it."""
|
|
902
|
+
labels = next((r.get("labels(b)") for r in group.rows if r.get("labels(b)") is not None), None)
|
|
903
|
+
return "Method" in _node_labels(labels)
|
|
904
|
+
|
|
905
|
+
@staticmethod
|
|
906
|
+
def _owned_by(group: _EdgeGroup, token: str) -> bool:
|
|
907
|
+
"""Whether the class the backend records as defining the group's symbol is the one *token*
|
|
908
|
+
names — the second half of "the qualifier is a class". `owner` comes from a lookup of the
|
|
909
|
+
`DEFINES_METHOD` edge into the method (`DispatchCallers._establish_method_owners`), so a
|
|
910
|
+
method with no class node behind it, a lookup that failed and a qualifier that is a module path
|
|
911
|
+
all leave it blank, and a blank owner is not scanned."""
|
|
912
|
+
owner = group.owner.replace("/", ".").rsplit(".", 1)[-1]
|
|
913
|
+
return bool(owner) and owner == token
|
|
914
|
+
|
|
915
|
+
@staticmethod
|
|
916
|
+
def _text_can_judge(row: dict, group: _EdgeGroup, file_key: str) -> bool:
|
|
917
|
+
"""Whether a text search of this row's file says anything about the row, so whether it is
|
|
918
|
+
scanned at all. Each exclusion is a case where "the file does not write the qualifier" would be
|
|
919
|
+
a claim about something other than the call — and where it cannot be told whether it is, the
|
|
920
|
+
row is left `null` and not judged, because a verdict that fails open refutes a caller nobody
|
|
921
|
+
looked at.
|
|
922
|
+
|
|
923
|
+
* Not name-matched. A `resolved` row followed a real binding and needs no corroboration from a
|
|
924
|
+
text search; scanning it would spend reads to re-derive something already known, and invite
|
|
925
|
+
the reading that a proven caller whose file happens not to name the class is suspect. An
|
|
926
|
+
`unstated` row carries no provenance for the qualifier to corroborate or undercut.
|
|
927
|
+
* A `via` row. It called the BASE (`CodeProvider.build_result`), and a caller of a base reaches
|
|
928
|
+
this override through the object's class at run time — `gateway.py` calls
|
|
929
|
+
`provider.build_result` and never writes `LspProvider`, which is exactly why it is a caller.
|
|
930
|
+
Scanning it would refute the real dispatch callers this section exists to show.
|
|
931
|
+
* Module-scope code. The backend attributes a module's scope to the file path it has for that
|
|
932
|
+
node, and has been seen naming a sibling file there (`_collapse_module_scope`), so a miss in
|
|
933
|
+
that file is not evidence about the code the row is about.
|
|
934
|
+
* The file that DEFINES the symbol. It writes the class's name because it declares it, whatever
|
|
935
|
+
the call does — the verdict could only ever be `true`, which would rank the row above its
|
|
936
|
+
peers on evidence that cannot discriminate. This is what a `same_module` edge downgraded for
|
|
937
|
+
being written through a receiver always is: the backend binds those inside the caller's own
|
|
938
|
+
file, and that file is the one that defines the callee.
|
|
939
|
+
* No recorded call text. The text is the only thing that says what the receiver was, and an
|
|
940
|
+
older index or an edge the extractor left blank has none; `_same_module_call` treats it the
|
|
941
|
+
same way, as unchecked.
|
|
942
|
+
* A language with no own-receiver rule (`_OWN_RECEIVERS`: Python, JavaScript, TypeScript). In
|
|
943
|
+
Java, Kotlin, C# or Ruby a call reaches an inherited member with no receiver at all, and in
|
|
944
|
+
every language here `self`/`this` do — in a file that never names the ancestor.
|
|
945
|
+
* A call on the enclosing object (`self.m()`, `this.m()`). The receiver is the caller's own
|
|
946
|
+
class, which reaches the method by inheritance and never by writing the ancestor's name, so
|
|
947
|
+
the class hierarchy is the question and a text search cannot ask it.
|
|
948
|
+
"""
|
|
949
|
+
if row.get("_bucket") != _NAME_MATCHED or row.get("_via"):
|
|
950
|
+
return False
|
|
951
|
+
if row.get("_module_scope") is not None:
|
|
952
|
+
return False
|
|
953
|
+
file = str(row.get(file_key) or "")
|
|
954
|
+
if not file or file == group.file:
|
|
955
|
+
return False
|
|
956
|
+
callee = str(row.get("callee") or "").strip()
|
|
957
|
+
own = _own_receivers_of(file)
|
|
958
|
+
if not callee or own is None:
|
|
959
|
+
return False
|
|
960
|
+
receiver, dot, _ = callee.rpartition(".")
|
|
961
|
+
return not (dot and _is_own_receiver(receiver, own))
|
|
962
|
+
|
|
963
|
+
def _scan_plan(
|
|
964
|
+
self, op: str, shown: list[_EdgeGroup], wanted: _SymbolTarget, file_key: str,
|
|
965
|
+
) -> tuple[str, list[tuple[_EdgeGroup, int]]] | None:
|
|
966
|
+
"""What the scan WOULD judge, decided before anything is read or looked up: the token, and the
|
|
967
|
+
`(group, row index)` of every row `_text_can_judge` accepts in a group whose symbol is a method.
|
|
968
|
+
``None`` when it would judge nothing.
|
|
969
|
+
|
|
970
|
+
Split out so the lookup of the methods' classes (`_establish_method_owners`) and the scan itself
|
|
971
|
+
ask the same question of the same rows — and so neither runs for an answer the note will not
|
|
972
|
+
speak about (`_guesses_dominate`)."""
|
|
973
|
+
if op != "callers" or not wanted.qualified or not self._guesses_dominate(shown):
|
|
974
|
+
return None
|
|
975
|
+
found = self._discriminator(wanted, shown)
|
|
976
|
+
if found is None or found[1] != _WHY_QUALIFIED:
|
|
977
|
+
return None
|
|
978
|
+
doubt = [(g, i) for g in shown if self._names_a_class_member(g)
|
|
979
|
+
for i, r in enumerate(g.rows) if self._text_can_judge(r, g, file_key)]
|
|
980
|
+
return (found[0], doubt) if doubt else None
|
|
981
|
+
|
|
982
|
+
def _scan_for_qualifier(
|
|
983
|
+
self, op: str, shown: list[_EdgeGroup], wanted: _SymbolTarget, file_key: str,
|
|
984
|
+
) -> str | None:
|
|
985
|
+
"""Run the check the settle note used to tell readers to run by hand, and mark the rows with
|
|
986
|
+
what it found. Returns the token it scanned for, or ``None`` when nothing was judged.
|
|
987
|
+
|
|
988
|
+
The note printed the `rg` command: the token, the root and the files in doubt are all in hand
|
|
989
|
+
at this point, so the answer can state what the grep would have found instead of describing
|
|
990
|
+
how to find it. Each judged row is replaced in its group by a copy carrying `_qualifier_seen`
|
|
991
|
+
and `_qualifier` (the backend's own rows are shared and are never written to), and everything
|
|
992
|
+
downstream — the sort, the printed label, `rows[]`, the evidence counts, `changed <ref>` —
|
|
993
|
+
reads the rows, so none of it can disagree with the note about which rows were refuted.
|
|
994
|
+
|
|
995
|
+
`shown` is the groups the answer prints (`_shown_groups`): a count over rows nobody can see
|
|
996
|
+
is not one a reader can check.
|
|
997
|
+
|
|
998
|
+
Scoped, and each limit was either measured or is a reason a text search cannot speak:
|
|
999
|
+
|
|
1000
|
+
* `callers` only. On `callees` the displayed rows are what the target CALLS, and a callee's
|
|
1001
|
+
file has no reason to write the target's own class.
|
|
1002
|
+
* Only when the answer is mostly name-matched rows (`_guesses_dominate`). Where the note will
|
|
1003
|
+
not be printed, nothing is scanned: a badge on a row without the note that qualifies it says
|
|
1004
|
+
more than the evidence does.
|
|
1005
|
+
* Only when the CALLER wrote the qualifier, and the called node is a method of a class named
|
|
1006
|
+
by it. The token the settle command falls back to for a bare target is the stem of the
|
|
1007
|
+
defining file, and a caller reaching the symbol through a re-export never writes that:
|
|
1008
|
+
measured on `corpus-ts`, `callerFacade.ts` imports `forwardReleasedItem` from `./facade`,
|
|
1009
|
+
contains no `proxy`, and a scan on the stem refuted a TRUE caller. Gating on which branch of
|
|
1010
|
+
`_discriminator` fired did not fix it — the discriminator also falls back to the backend's
|
|
1011
|
+
dotted name, so `src.proxy.forwardReleasedItem` yields `proxy` through the qualifier branch,
|
|
1012
|
+
a module path wearing a class qualifier's clothes. The node's own label separates the two,
|
|
1013
|
+
and the class the backend records as defining the method (`_owned_by`) is the check that the
|
|
1014
|
+
label did not over-claim.
|
|
1015
|
+
* Only the rows `_text_can_judge` accepts, which is where the cases a text search cannot
|
|
1016
|
+
speak to are left out. They stay `null`, which leaves them out of any filter built on the
|
|
1017
|
+
field rather than counting them as clean.
|
|
1018
|
+
|
|
1019
|
+
Nothing is removed. Absence of the qualifier is strong evidence and it is not proof — see
|
|
1020
|
+
`qualifier_scan` for the ways a genuine caller avoids the name.
|
|
1021
|
+
"""
|
|
1022
|
+
plan = self._scan_plan(op, shown, wanted, file_key)
|
|
1023
|
+
if plan is None:
|
|
1024
|
+
return None
|
|
1025
|
+
# Eligible from here on, whatever the scan then finds — and whatever it fails to: a stalled
|
|
1026
|
+
# read, a spent budget or an owner lookup that timed out leave it judging nothing, and an
|
|
1027
|
+
# answer that says so is still about the filesystem as it was NOW. Recorded so the gateway
|
|
1028
|
+
# does not cache it (`evidence.qualifier_checked`).
|
|
1029
|
+
self._qualifier_attempted = True
|
|
1030
|
+
token, planned = plan
|
|
1031
|
+
doubt = [(g, i) for g, i in planned if self._owned_by(g, token)]
|
|
1032
|
+
if not doubt:
|
|
1033
|
+
return None
|
|
1034
|
+
root = getattr(self, "_answered_root", None) or ""
|
|
1035
|
+
scanned = qualifier_scan.files_naming(
|
|
1036
|
+
root, token, [str(g.rows[i].get(file_key) or "") for g, i in doubt],
|
|
1037
|
+
budget=getattr(self, "_scan_budget", None))
|
|
1038
|
+
if scanned is None:
|
|
1039
|
+
return None
|
|
1040
|
+
for g, i in doubt:
|
|
1041
|
+
seen = scanned.get(str(g.rows[i].get(file_key) or ""))
|
|
1042
|
+
if seen is not None:
|
|
1043
|
+
g.rows[i] = dict(g.rows[i], _qualifier=token, _qualifier_seen=seen)
|
|
1044
|
+
return token
|
|
1045
|
+
|
|
846
1046
|
def _settle_name_matches(
|
|
847
1047
|
self, groups: list[_EdgeGroup], wanted: _SymbolTarget, unit: str, file_key: str,
|
|
1048
|
+
scanned: str | None = None,
|
|
848
1049
|
) -> str:
|
|
849
|
-
"""
|
|
1050
|
+
"""What the name-matched rows come to under the one check that can discharge the doubt, or `""`.
|
|
850
1051
|
|
|
851
1052
|
DISCLOSING doubt and DISCHARGING it are different services, and this project had only been
|
|
852
1053
|
doing the first. An agent reading `43 name-matched` is told the answer may be wrong and
|
|
@@ -856,29 +1057,61 @@ class AnswerRendering:
|
|
|
856
1057
|
the same time.
|
|
857
1058
|
|
|
858
1059
|
On the case that motivated it — `callers StrategyChain.resolve` on a 1,483-file monorepo,
|
|
859
|
-
48 rows of which 43 were name-matched — the
|
|
860
|
-
`StrategyChain` at all
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1060
|
+
48 rows of which 43 were name-matched — the check finds that the five files mentioning
|
|
1061
|
+
`StrategyChain` at all contain none of the 43. That is the check a person ran by hand to
|
|
1062
|
+
establish the finding in `bench/README.md`.
|
|
1063
|
+
|
|
1064
|
+
Two shapes. When `_scan_for_qualifier` ran (`scanned` is the token it used), the note STATES
|
|
1065
|
+
the result — and keeps the command beneath it, because a claim a reader cannot re-run is a
|
|
1066
|
+
claim they have to take on trust, which is what this note exists to avoid. Otherwise nobody
|
|
1067
|
+
looked, and the reader is handed the command and the list of files to hold against it: the
|
|
1068
|
+
bare-target case, an unreadable root, a root that is not on disk.
|
|
1069
|
+
|
|
1070
|
+
`groups` is the groups the answer prints, so `N of M` counts rows a reader can see: M is the
|
|
1071
|
+
name-matched rows of the DIRECT list as printed — not the callers through a base, which
|
|
1072
|
+
`_render_via` prints apart, and not what the distinct-caller cap left out — and N the ones
|
|
1073
|
+
whose file never writes the token. Both are re-derivable from `rows[]`.
|
|
1074
|
+
|
|
1075
|
+
Deliberately not a verdict, in either shape. It narrows; it does not decide. A file that
|
|
1076
|
+
never writes the qualifier can still reach the method — `_QUALIFIER_BYPASSES` — and a file
|
|
1077
|
+
that does write it may not call it. Overstating what a grep proves would be the same defect
|
|
1078
|
+
this file is full of fixes for.
|
|
867
1079
|
"""
|
|
868
|
-
|
|
869
|
-
if len(matched) < self._SETTLE_FLOOR:
|
|
870
|
-
return ""
|
|
871
|
-
total = sum(len(g.rows) for g in groups)
|
|
872
|
-
# "Dominate": at least half the answer, or all of it. Below half, the resolved rows carry
|
|
873
|
-
# the answer and a command here would be noise on a result that is mostly evidence.
|
|
874
|
-
if len(matched) * 2 < total:
|
|
1080
|
+
if not self._guesses_dominate(groups):
|
|
875
1081
|
return ""
|
|
1082
|
+
matched = [r for g in groups for r in g.rows if r.get("_bucket") == _NAME_MATCHED]
|
|
876
1083
|
found = self._discriminator(wanted, groups)
|
|
877
1084
|
if found is None:
|
|
878
1085
|
return ""
|
|
879
1086
|
token, why = found
|
|
880
1087
|
|
|
881
1088
|
root = getattr(self, "_answered_root", None) or "."
|
|
1089
|
+
if scanned is not None:
|
|
1090
|
+
absent = sum(1 for r in matched if r.get("_qualifier_seen") is False)
|
|
1091
|
+
unjudged = sum(1 for r in matched if r.get("_qualifier_seen") is None)
|
|
1092
|
+
left = (f" {unjudged} could not be judged (a file that could not be read in time or is "
|
|
1093
|
+
f"not plain text, the file that defines the symbol, a call on `self`/`this`, a "
|
|
1094
|
+
f"call in a language this check has no rule for, or module-scope code) and carry "
|
|
1095
|
+
f"`qualifier_seen: null`." if unjudged else "")
|
|
1096
|
+
judged = [str(r.get(file_key) or "") for r in matched
|
|
1097
|
+
if r.get("_qualifier_seen") is not None]
|
|
1098
|
+
rerun = f"Re-run it yourself: `{qualifier_scan.rerun_command(token, root, judged)}`_\n"
|
|
1099
|
+
if not absent:
|
|
1100
|
+
return (
|
|
1101
|
+
f"\n\n_Checked: **none of the {len(matched) - unjudged}** name-matched {unit}s "
|
|
1102
|
+
f"shown that could be judged is in a file that never writes `{token}`, {why} — "
|
|
1103
|
+
f"so this check narrows nothing here.{left}_\n"
|
|
1104
|
+
f"_It is a text search of the files as they are on disk: a file that writes the "
|
|
1105
|
+
f"name has not shown that it calls the symbol. {rerun}")
|
|
1106
|
+
return (
|
|
1107
|
+
f"\n\n_Checked: **{absent} of {len(matched)}** name-matched {unit}s shown are in "
|
|
1108
|
+
f"files that never write `{token}`, {why}, and the part of the target the name match "
|
|
1109
|
+
f"did not use. They are ranked last below and carry `qualifier_seen: false`.{left}_\n"
|
|
1110
|
+
f"_This is a text search of the files as they are on disk, and it narrows; it does "
|
|
1111
|
+
f"not decide. A file can reach the symbol without writing the name — through "
|
|
1112
|
+
f"{_QUALIFIER_BYPASSES} — so these are the {unit}s to doubt first, not {unit}s "
|
|
1113
|
+
f"proven false. No class-qualified Python target is in the benchmark "
|
|
1114
|
+
f"(`bench/README.md`). {rerun}")
|
|
882
1115
|
files = sorted({str(r.get(file_key) or "") for r in matched if r.get(file_key)})
|
|
883
1116
|
listing = ""
|
|
884
1117
|
if files:
|
|
@@ -886,12 +1119,13 @@ class AnswerRendering:
|
|
|
886
1119
|
more = f", … (+{len(files) - 10} more)" if len(files) > 10 else ""
|
|
887
1120
|
listing = f"_The {len(files)} file(s) to check against: {shown}{more}._\n"
|
|
888
1121
|
return (
|
|
889
|
-
f"\n\n_Settle it: `
|
|
1122
|
+
f"\n\n_Settle it: `{qualifier_scan.rerun_command(token, root)}`_\n"
|
|
890
1123
|
f"_`{token}` is {why}, and it is the part of the target the name match did not use. "
|
|
891
|
-
f"A file that never names it
|
|
892
|
-
f"
|
|
893
|
-
f"
|
|
894
|
-
f"
|
|
1124
|
+
f"A file that never names it is less likely to be reaching this symbol, and that is not "
|
|
1125
|
+
f"proof it is not: a caller can reach it through {_QUALIFIER_BYPASSES}. So the "
|
|
1126
|
+
f"{len(matched)} name-matched {unit}s below whose file is absent from that output are "
|
|
1127
|
+
f"the ones to doubt first. Appearing in it is not proof of a call either — it narrows "
|
|
1128
|
+
f"the list, it does not decide it._\n"
|
|
895
1129
|
+ listing
|
|
896
1130
|
)
|
|
897
1131
|
|
|
@@ -922,6 +1156,15 @@ class AnswerRendering:
|
|
|
922
1156
|
There is no receiver/type evidence field. The readiness doc asks for one "when available"
|
|
923
1157
|
and it is never available: the backend reports no receiver type, so a key here would be
|
|
924
1158
|
`null` on every row of every answer — a field that promises a capability nobody has.
|
|
1159
|
+
|
|
1160
|
+
`qualifier_seen` is the field that IS available, and it is a different kind of claim: not
|
|
1161
|
+
the receiver's type, which nothing here knows, but whether this row's file contains the text
|
|
1162
|
+
of the qualifier the name match did not use (`qualifier`, so the field says WHICH). A fact
|
|
1163
|
+
about the file, checkable by the reader with one `rg`, and `null` wherever nobody looked —
|
|
1164
|
+
every row that followed a binding, every row that reached the symbol through a base, and
|
|
1165
|
+
every row a text search cannot speak to (`_text_can_judge`). It is deliberately not folded
|
|
1166
|
+
into `verified`: a genuine caller can reach a method without ever writing its class's name,
|
|
1167
|
+
so an absent qualifier lowers a row's standing without settling it.
|
|
925
1168
|
"""
|
|
926
1169
|
bucket = str(row.get("_bucket") or _UNSTATED)
|
|
927
1170
|
strategy = str(row.get("strategy") or "").strip()
|
|
@@ -937,6 +1180,8 @@ class AnswerRendering:
|
|
|
937
1180
|
"module_scope": scope is not None,
|
|
938
1181
|
"edge": edge or None,
|
|
939
1182
|
"verified": bucket == _RESOLVED,
|
|
1183
|
+
"qualifier": row.get("_qualifier"),
|
|
1184
|
+
"qualifier_seen": row.get("_qualifier_seen"),
|
|
940
1185
|
"evidence": bucket,
|
|
941
1186
|
"strategy": strategy or None,
|
|
942
1187
|
"confidence": float(confidence) if confidence is not None else None,
|
|
@@ -1006,10 +1251,19 @@ class AnswerRendering:
|
|
|
1006
1251
|
counts = {bucket: sum(1 for r in rows if r["evidence"] == bucket)
|
|
1007
1252
|
for bucket in (_RESOLVED, _NAME_MATCHED, _UNSTATED)}
|
|
1008
1253
|
withheld = self._pending_withheld
|
|
1254
|
+
# Three states, counted separately for the same reason the buckets above are: a row whose
|
|
1255
|
+
# file was never opened is not a row whose file was opened and came back clean. Both counts
|
|
1256
|
+
# are over `possible` rows by construction (`_text_can_judge`), so `possible` minus the two
|
|
1257
|
+
# is how many nobody judged.
|
|
1258
|
+
checked = [r.get("qualifier_seen") for r in rows]
|
|
1009
1259
|
return {
|
|
1010
1260
|
"verified": counts[_RESOLVED],
|
|
1011
1261
|
"possible": counts[_NAME_MATCHED],
|
|
1012
1262
|
"unstated": counts[_UNSTATED],
|
|
1263
|
+
# Of the `possible` rows, how many sit in a file that never writes the qualifier — the
|
|
1264
|
+
# ones a reader would have eliminated with the `rg` the note used to hand them.
|
|
1265
|
+
"qualifier_absent": sum(1 for q in checked if q is False),
|
|
1266
|
+
"qualifier_present": sum(1 for q in checked if q is True),
|
|
1013
1267
|
"returned": len(rows),
|
|
1014
1268
|
"total": None if self._pending_row_cap else len(rows) + withheld,
|
|
1015
1269
|
"truncated": bool(self._pending_row_cap or withheld),
|
|
@@ -1020,6 +1274,11 @@ class AnswerRendering:
|
|
|
1020
1274
|
"safe_for_destructive": bool(
|
|
1021
1275
|
counts[_NAME_MATCHED] == 0 and counts[_UNSTATED] == 0
|
|
1022
1276
|
and not self._pending_row_cap and not withheld and not self._pending_gaps),
|
|
1277
|
+
# Present only when the qualifier check was eligible and attempted, so every other
|
|
1278
|
+
# answer's evidence is as it was. It marks an answer that depends on the files as they
|
|
1279
|
+
# were when it was asked — judged or, after a stall or a spent budget, not — which the
|
|
1280
|
+
# gateway therefore never caches.
|
|
1281
|
+
**({"qualifier_checked": True} if getattr(self, "_qualifier_attempted", False) else {}),
|
|
1023
1282
|
}
|
|
1024
1283
|
|
|
1025
1284
|
def _row_noun(self) -> str:
|
|
@@ -1088,14 +1347,30 @@ class AnswerRendering:
|
|
|
1088
1347
|
name_key, qn_key, file_key = row_keys
|
|
1089
1348
|
answered = [g for g in groups if g.rows]
|
|
1090
1349
|
kept = sum(len(g.rows) for g in answered)
|
|
1350
|
+
# What the answer prints, which is what the scan and the note about it are over: `answered`
|
|
1351
|
+
# less the symbols past the candidate cap, whose rows nobody is shown.
|
|
1352
|
+
shown_groups = self._shown_groups(answered)
|
|
1353
|
+
scanned = self._scan_for_qualifier(op, shown_groups, wanted, file_key)
|
|
1091
1354
|
# Rows are ordered so the direct calls come first — they are the answer to the question
|
|
1092
1355
|
# that was asked — and, within those, the rows that followed a real binding come before the
|
|
1093
1356
|
# ones matched by name. A reader who stops after the first few should be stopping on the
|
|
1094
1357
|
# evidence, not on whichever guess the backend happened to return first.
|
|
1358
|
+
#
|
|
1359
|
+
# The scan verdict is the LAST key: it only ever reorders within a bucket, and within
|
|
1360
|
+
# production-versus-test inside it. A name-matched row whose file writes the qualifier sorts
|
|
1361
|
+
# above one nobody could judge, which sorts above one whose file does not. It cannot promote a
|
|
1362
|
+
# guess above a binding, because it is weaker evidence than either and ordering it as though
|
|
1363
|
+
# it were equal would be the same over-claim in a different place; and it cannot promote a
|
|
1364
|
+
# test above production, because "production code is listed first and test files last" is a
|
|
1365
|
+
# promise the truncation note makes and a verdict is not a reason to break it. It is a key and
|
|
1366
|
+
# not a filter: every row is still printed.
|
|
1095
1367
|
_ORDER = {_RESOLVED: 0, _UNSTATED: 1, _NAME_MATCHED: 2}
|
|
1368
|
+
_SCAN_ORDER = {True: 0, None: 1, False: 2}
|
|
1096
1369
|
for g in answered:
|
|
1097
1370
|
g.rows.sort(key=lambda r: (str(r.get("type(c)") or "") != _DIRECT_KIND,
|
|
1098
|
-
_ORDER.get(str(r.get("_bucket") or ""), 1)
|
|
1371
|
+
_ORDER.get(str(r.get("_bucket") or ""), 1),
|
|
1372
|
+
op == "callers" and self._is_test_row(r, name_key, file_key),
|
|
1373
|
+
_SCAN_ORDER[r.get("_qualifier_seen")]))
|
|
1099
1374
|
counts = self._kind_counts(answered)
|
|
1100
1375
|
direct = counts.get(_DIRECT_KIND, 0)
|
|
1101
1376
|
others = kept - direct
|
|
@@ -1104,9 +1379,10 @@ class AnswerRendering:
|
|
|
1104
1379
|
else f"## {op.capitalize()} of {target} "
|
|
1105
1380
|
f"({direct} direct, {others} other reference(s))\n")
|
|
1106
1381
|
head += self._evidence_headline(answered, unit)
|
|
1107
|
-
# Disclosure says the answer may be wrong; this says how to find out
|
|
1108
|
-
# the headline because that is the number it is
|
|
1109
|
-
|
|
1382
|
+
# Disclosure says the answer may be wrong; this says how to find out — or, when the scan
|
|
1383
|
+
# ran, what finding out came to. Placed with the headline because that is the number it is
|
|
1384
|
+
# about.
|
|
1385
|
+
head += self._settle_name_matches(shown_groups, wanted, unit, file_key, scanned)
|
|
1110
1386
|
|
|
1111
1387
|
if len(answered) == 1:
|
|
1112
1388
|
rendered = list(answered[0].rows)
|
|
@@ -1123,7 +1399,7 @@ class AnswerRendering:
|
|
|
1123
1399
|
f"{unit}s are grouped separately rather than merged into one list. Narrow the target "
|
|
1124
1400
|
f"with a qualified name or `{wanted.name}@<file>` to answer about one of them",
|
|
1125
1401
|
)
|
|
1126
|
-
shown =
|
|
1402
|
+
shown = shown_groups
|
|
1127
1403
|
rendered = [r for g in shown for r in g.rows]
|
|
1128
1404
|
# Rows past the candidate cap are printed by nobody, so they are published by nobody
|
|
1129
1405
|
# either — but they are counted, because "12 of 17 shown" and "12 shown, total
|