attocode-code-intel 0.1.0__py3-none-any.whl

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 (345) hide show
  1. attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
  2. attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
  3. attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
  4. attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
  5. attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. attocode_intel/GUIDELINES.md +432 -0
  7. attocode_intel/__init__.py +52 -0
  8. attocode_intel/_internal/__init__.py +1 -0
  9. attocode_intel/_internal/errors/__init__.py +126 -0
  10. attocode_intel/_internal/integrations/__init__.py +1 -0
  11. attocode_intel/_internal/integrations/context/__init__.py +1 -0
  12. attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
  13. attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
  14. attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
  15. attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
  16. attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
  17. attocode_intel/_internal/integrations/context/cas.py +466 -0
  18. attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
  19. attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
  20. attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
  21. attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
  22. attocode_intel/_internal/integrations/context/cross_references.py +616 -0
  23. attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
  24. attocode_intel/_internal/integrations/context/embeddings.py +282 -0
  25. attocode_intel/_internal/integrations/context/frecency.py +427 -0
  26. attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
  27. attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
  28. attocode_intel/_internal/integrations/context/hydration.py +90 -0
  29. attocode_intel/_internal/integrations/context/index_repair.py +87 -0
  30. attocode_intel/_internal/integrations/context/index_store.py +586 -0
  31. attocode_intel/_internal/integrations/context/memory_store.py +602 -0
  32. attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
  33. attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
  34. attocode_intel/_internal/integrations/context/query_history.py +417 -0
  35. attocode_intel/_internal/integrations/context/reranker.py +351 -0
  36. attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
  37. attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
  38. attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
  39. attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
  40. attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
  41. attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
  42. attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
  43. attocode_intel/_internal/integrations/context/vector_store.py +795 -0
  44. attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
  45. attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
  46. attocode_intel/_internal/integrations/lsp/client.py +1140 -0
  47. attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
  48. attocode_intel/_internal/integrations/mcp/client.py +253 -0
  49. attocode_intel/_internal/integrations/security/__init__.py +1 -0
  50. attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
  51. attocode_intel/_internal/integrations/security/dataflow.py +522 -0
  52. attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
  53. attocode_intel/_internal/integrations/security/matcher.py +44 -0
  54. attocode_intel/_internal/integrations/security/patterns.py +724 -0
  55. attocode_intel/_internal/integrations/security/scanner.py +351 -0
  56. attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
  57. attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
  58. attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
  59. attocode_intel/_internal/tricks/__init__.py +1 -0
  60. attocode_intel/_internal/tricks/recursive_context.py +357 -0
  61. attocode_intel/_internal/types/__init__.py +1 -0
  62. attocode_intel/_internal/types/events.py +289 -0
  63. attocode_intel/_internal/types/severity.py +15 -0
  64. attocode_intel/_shared.py +341 -0
  65. attocode_intel/analysis_coverage.py +56 -0
  66. attocode_intel/api/__init__.py +1 -0
  67. attocode_intel/api/access.py +32 -0
  68. attocode_intel/api/app.py +247 -0
  69. attocode_intel/api/auth/__init__.py +128 -0
  70. attocode_intel/api/auth/api_keys.py +61 -0
  71. attocode_intel/api/auth/context.py +34 -0
  72. attocode_intel/api/auth/jwt.py +66 -0
  73. attocode_intel/api/auth/oauth.py +146 -0
  74. attocode_intel/api/auth/passwords.py +15 -0
  75. attocode_intel/api/deps.py +351 -0
  76. attocode_intel/api/knowledge_bridge.py +13 -0
  77. attocode_intel/api/mcp_transport.py +51 -0
  78. attocode_intel/api/middleware.py +411 -0
  79. attocode_intel/api/models.py +698 -0
  80. attocode_intel/api/providers/__init__.py +1 -0
  81. attocode_intel/api/providers/db_provider.py +1106 -0
  82. attocode_intel/api/providers/local_provider.py +476 -0
  83. attocode_intel/api/providers/remote_provider.py +740 -0
  84. attocode_intel/api/repository_source.py +21 -0
  85. attocode_intel/api/routes/__init__.py +1 -0
  86. attocode_intel/api/routes/_pin_helper.py +158 -0
  87. attocode_intel/api/routes/activity.py +148 -0
  88. attocode_intel/api/routes/adr.py +102 -0
  89. attocode_intel/api/routes/analysis.py +515 -0
  90. attocode_intel/api/routes/api_keys.py +170 -0
  91. attocode_intel/api/routes/auth.py +428 -0
  92. attocode_intel/api/routes/branches.py +262 -0
  93. attocode_intel/api/routes/cross_repo_search.py +198 -0
  94. attocode_intel/api/routes/embeddings.py +386 -0
  95. attocode_intel/api/routes/files.py +132 -0
  96. attocode_intel/api/routes/files_v2.py +388 -0
  97. attocode_intel/api/routes/gc.py +322 -0
  98. attocode_intel/api/routes/git_v2.py +433 -0
  99. attocode_intel/api/routes/graph.py +174 -0
  100. attocode_intel/api/routes/graph_viz.py +160 -0
  101. attocode_intel/api/routes/health.py +62 -0
  102. attocode_intel/api/routes/history.py +175 -0
  103. attocode_intel/api/routes/jobs.py +84 -0
  104. attocode_intel/api/routes/learning.py +205 -0
  105. attocode_intel/api/routes/lsp.py +272 -0
  106. attocode_intel/api/routes/notify.py +623 -0
  107. attocode_intel/api/routes/operations.py +27 -0
  108. attocode_intel/api/routes/orgs.py +924 -0
  109. attocode_intel/api/routes/preferences.py +100 -0
  110. attocode_intel/api/routes/presence.py +42 -0
  111. attocode_intel/api/routes/projects.py +116 -0
  112. attocode_intel/api/routes/repos.py +47 -0
  113. attocode_intel/api/routes/search.py +180 -0
  114. attocode_intel/api/routes/snapshots.py +614 -0
  115. attocode_intel/api/routes/webhooks.py +313 -0
  116. attocode_intel/api/routes/websocket.py +116 -0
  117. attocode_intel/api/static/graph.html +239 -0
  118. attocode_intel/api/utils.py +96 -0
  119. attocode_intel/artifacts/__init__.py +45 -0
  120. attocode_intel/artifacts/hashing.py +230 -0
  121. attocode_intel/artifacts/provenance.py +190 -0
  122. attocode_intel/artifacts/retrieval_pin.py +159 -0
  123. attocode_intel/audit.py +59 -0
  124. attocode_intel/bug_finder.py +206 -0
  125. attocode_intel/bundle.py +80 -0
  126. attocode_intel/catalog.py +242 -0
  127. attocode_intel/change_review.py +75 -0
  128. attocode_intel/cli.py +3062 -0
  129. attocode_intel/community.py +63 -0
  130. attocode_intel/confidence/__init__.py +144 -0
  131. attocode_intel/confidence/jev.py +234 -0
  132. attocode_intel/confidence/llm.py +330 -0
  133. attocode_intel/confidence/redact.py +44 -0
  134. attocode_intel/confidence/settings.py +74 -0
  135. attocode_intel/config.py +256 -0
  136. attocode_intel/crypto.py +31 -0
  137. attocode_intel/db/__init__.py +7 -0
  138. attocode_intel/db/base.py +33 -0
  139. attocode_intel/db/engine.py +61 -0
  140. attocode_intel/db/models.py +529 -0
  141. attocode_intel/entrypoint.py +185 -0
  142. attocode_intel/focused_evidence.py +130 -0
  143. attocode_intel/freshness.py +154 -0
  144. attocode_intel/gateway.py +734 -0
  145. attocode_intel/git/__init__.py +7 -0
  146. attocode_intel/git/credentials.py +75 -0
  147. attocode_intel/git/manager.py +493 -0
  148. attocode_intel/git/models.py +96 -0
  149. attocode_intel/git/storage.py +98 -0
  150. attocode_intel/graph_query_parser.py +761 -0
  151. attocode_intel/helpers.py +712 -0
  152. attocode_intel/indexing/__init__.py +29 -0
  153. attocode_intel/indexing/debouncer.py +136 -0
  154. attocode_intel/indexing/delta_indexer.py +151 -0
  155. attocode_intel/indexing/full_indexer.py +323 -0
  156. attocode_intel/indexing/incremental.py +240 -0
  157. attocode_intel/indexing/parser.py +403 -0
  158. attocode_intel/indexing/summarizer.py +264 -0
  159. attocode_intel/installer.py +1896 -0
  160. attocode_intel/knowledge.py +269 -0
  161. attocode_intel/local_knowledge.py +77 -0
  162. attocode_intel/migrations/alembic.ini +36 -0
  163. attocode_intel/migrations/env.py +71 -0
  164. attocode_intel/migrations/script.py.mako +27 -0
  165. attocode_intel/migrations/versions/001_initial_schema.py +219 -0
  166. attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
  167. attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
  168. attocode_intel/migrations/versions/004_audit_events.py +38 -0
  169. attocode_intel/migrations/versions/005_user_preferences.py +30 -0
  170. attocode_intel/migrations/versions/006_google_oauth.py +27 -0
  171. attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
  172. attocode_intel/migrations/versions/008_symbol_references.py +37 -0
  173. attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
  174. attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
  175. attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
  176. attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
  177. attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
  178. attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
  179. attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
  180. attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
  181. attocode_intel/migrations/versions/017_provenance_table.py +102 -0
  182. attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
  183. attocode_intel/migrations/versions/019_symref_caller.py +42 -0
  184. attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
  185. attocode_intel/onboarding.py +258 -0
  186. attocode_intel/output.py +178 -0
  187. attocode_intel/precision.py +133 -0
  188. attocode_intel/presence.py +160 -0
  189. attocode_intel/probe.py +110 -0
  190. attocode_intel/project_dir.py +97 -0
  191. attocode_intel/pubsub.py +113 -0
  192. attocode_intel/query_ranking.py +242 -0
  193. attocode_intel/readiness.py +972 -0
  194. attocode_intel/redis.py +51 -0
  195. attocode_intel/reference_pages.py +60 -0
  196. attocode_intel/remote.py +131 -0
  197. attocode_intel/repo_ranker.py +246 -0
  198. attocode_intel/request_context.py +82 -0
  199. attocode_intel/rules/__init__.py +28 -0
  200. attocode_intel/rules/ci.py +320 -0
  201. attocode_intel/rules/combinators.py +293 -0
  202. attocode_intel/rules/enricher.py +127 -0
  203. attocode_intel/rules/evolution.py +452 -0
  204. attocode_intel/rules/executor.py +531 -0
  205. attocode_intel/rules/filters/__init__.py +5 -0
  206. attocode_intel/rules/filters/pipeline.py +151 -0
  207. attocode_intel/rules/formatter.py +198 -0
  208. attocode_intel/rules/hygiene.py +242 -0
  209. attocode_intel/rules/importers/__init__.py +1 -0
  210. attocode_intel/rules/importers/semgrep.py +382 -0
  211. attocode_intel/rules/loader.py +292 -0
  212. attocode_intel/rules/marketplace.py +375 -0
  213. attocode_intel/rules/metavar.py +181 -0
  214. attocode_intel/rules/model.py +317 -0
  215. attocode_intel/rules/packs/__init__.py +1 -0
  216. attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
  217. attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
  218. attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
  219. attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
  220. attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
  221. attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
  222. attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
  223. attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
  224. attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
  225. attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
  226. attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
  227. attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
  228. attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
  229. attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
  230. attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
  231. attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
  232. attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
  233. attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
  234. attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
  235. attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
  236. attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
  237. attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
  238. attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
  239. attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
  240. attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
  241. attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
  242. attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
  243. attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
  244. attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
  245. attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
  246. attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
  247. attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
  248. attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
  249. attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
  250. attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
  251. attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
  252. attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
  253. attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
  254. attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
  255. attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
  256. attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
  257. attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
  258. attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
  259. attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
  260. attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
  261. attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
  262. attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
  263. attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
  264. attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
  265. attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
  266. attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
  267. attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
  268. attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
  269. attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
  270. attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
  271. attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
  272. attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
  273. attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
  274. attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
  275. attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
  276. attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
  277. attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
  278. attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
  279. attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
  280. attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
  281. attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
  282. attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
  283. attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
  284. attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
  285. attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
  286. attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
  287. attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
  288. attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
  289. attocode_intel/rules/packs/pack_loader.py +244 -0
  290. attocode_intel/rules/plugins/__init__.py +1 -0
  291. attocode_intel/rules/plugins/plugin_loader.py +132 -0
  292. attocode_intel/rules/profiling.py +332 -0
  293. attocode_intel/rules/registry.py +213 -0
  294. attocode_intel/rules/sarif.py +187 -0
  295. attocode_intel/rules/synthesis.py +420 -0
  296. attocode_intel/rules/testing.py +326 -0
  297. attocode_intel/server.py +734 -0
  298. attocode_intel/service.py +3416 -0
  299. attocode_intel/sharing.py +57 -0
  300. attocode_intel/storage/__init__.py +6 -0
  301. attocode_intel/storage/branch_overlay.py +352 -0
  302. attocode_intel/storage/content_store.py +220 -0
  303. attocode_intel/storage/dependency_store.py +143 -0
  304. attocode_intel/storage/diff_engine.py +187 -0
  305. attocode_intel/storage/embedding_store.py +594 -0
  306. attocode_intel/storage/provenance_store.py +137 -0
  307. attocode_intel/storage/security_scanner_db.py +164 -0
  308. attocode_intel/storage/symbol_store.py +160 -0
  309. attocode_intel/structural_evidence.py +144 -0
  310. attocode_intel/symbol_inspection.py +141 -0
  311. attocode_intel/symbol_links.py +167 -0
  312. attocode_intel/telemetry.py +42 -0
  313. attocode_intel/test_ranking.py +73 -0
  314. attocode_intel/testing/__init__.py +43 -0
  315. attocode_intel/testing/fixtures.py +122 -0
  316. attocode_intel/testing/helpers.py +75 -0
  317. attocode_intel/testing/mocks.py +99 -0
  318. attocode_intel/tools/__init__.py +2 -0
  319. attocode_intel/tools/adr_tools.py +628 -0
  320. attocode_intel/tools/analysis_tools.py +382 -0
  321. attocode_intel/tools/composite_tools.py +695 -0
  322. attocode_intel/tools/cross_mode_tools.py +173 -0
  323. attocode_intel/tools/dead_code_tools.py +582 -0
  324. attocode_intel/tools/distill_tools.py +353 -0
  325. attocode_intel/tools/frecency_tools.py +220 -0
  326. attocode_intel/tools/fuzzy_tools.py +255 -0
  327. attocode_intel/tools/history_tools.py +486 -0
  328. attocode_intel/tools/learning_tools.py +222 -0
  329. attocode_intel/tools/lsp_tools.py +218 -0
  330. attocode_intel/tools/maintenance_tools.py +1662 -0
  331. attocode_intel/tools/navigation_tools.py +220 -0
  332. attocode_intel/tools/overlay_tools.py +540 -0
  333. attocode_intel/tools/pin_store.py +432 -0
  334. attocode_intel/tools/pin_tools.py +266 -0
  335. attocode_intel/tools/query_constraints_tools.py +262 -0
  336. attocode_intel/tools/query_history_tools.py +158 -0
  337. attocode_intel/tools/readiness_tools.py +44 -0
  338. attocode_intel/tools/rule_tools.py +992 -0
  339. attocode_intel/tools/search_tools.py +770 -0
  340. attocode_intel/tools/snapshot_tools.py +624 -0
  341. attocode_intel/workers/__init__.py +1 -0
  342. attocode_intel/workers/job_utils.py +111 -0
  343. attocode_intel/workers/jobs.py +573 -0
  344. attocode_intel/workers/run.py +30 -0
  345. attocode_intel/workers/settings.py +61 -0
@@ -0,0 +1,616 @@
1
+ """Cross-reference index for codebase symbol analysis.
2
+
3
+ Builds a bidirectional index of symbol definitions and references,
4
+ file-level import dependencies, and dependents from parsed AST data.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import re
11
+ import threading
12
+ from dataclasses import dataclass, field
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ if TYPE_CHECKING:
16
+ from attocode_intel._internal.integrations.context.index_store import IndexStore
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def _split_name_tokens(name: str) -> list[str]:
22
+ """Split a symbol name into lowercase tokens.
23
+
24
+ Handles camelCase, PascalCase, snake_case, UPPER_CASE, and mixtures.
25
+
26
+ Examples::
27
+
28
+ "parseConfig" -> ["parse", "config"]
29
+ "parse_config" -> ["parse", "config"]
30
+ "HTTPServer" -> ["http", "server"]
31
+ "HTMLParser" -> ["html", "parser"]
32
+ "_main" -> ["main"]
33
+ "IO" -> ["io"]
34
+ """
35
+ # First split on underscores
36
+ parts = name.split("_")
37
+ tokens: list[str] = []
38
+ for part in parts:
39
+ if not part:
40
+ continue
41
+ # Split camelCase / PascalCase: insert boundary before uppercase
42
+ # that follows lowercase, or before uppercase followed by lowercase
43
+ # (to handle "HTTPServer" -> "HTTP", "Server")
44
+ sub = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", part)
45
+ sub = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", sub)
46
+ for s in sub.split("_"):
47
+ if s:
48
+ tokens.append(s.lower())
49
+ return tokens
50
+
51
+
52
+ @dataclass(slots=True)
53
+ class SymbolRef:
54
+ """A reference (call site, import, or attribute access) to a symbol."""
55
+
56
+ symbol_name: str # "parse_file" or "CodeIndex.build"
57
+ ref_kind: str # "call" | "import" | "attribute"
58
+ file_path: str
59
+ line: int
60
+ source: str = "tree-sitter" # "tree-sitter" | "lsp"
61
+ syntax_name: str = "" # identifier/member expression as written in source
62
+ # Qualified name of the function/method enclosing this reference. Empty
63
+ # when the reference is at module top level or the enclosing symbol
64
+ # could not be resolved. Populated for call-graph construction.
65
+ caller_qualified_name: str = ""
66
+
67
+
68
+ @dataclass(slots=True)
69
+ class SymbolLocation:
70
+ """Location where a symbol is defined."""
71
+
72
+ name: str
73
+ qualified_name: str # "CodebaseContextManager.discover_files"
74
+ kind: str # "function" | "class" | "method"
75
+ file_path: str
76
+ start_line: int
77
+ end_line: int
78
+ source: str = "tree-sitter" # "tree-sitter" | "lsp"
79
+
80
+
81
+ @dataclass(slots=True)
82
+ class CrossRefIndex:
83
+ """Bidirectional cross-reference index for a codebase.
84
+
85
+ Tracks symbol definitions, call sites, file-level import dependencies,
86
+ and reverse dependents. Includes an inverted name index for fast
87
+ multi-strategy symbol search.
88
+
89
+ Optionally backed by an ``IndexStore`` for persistence across restarts.
90
+ """
91
+
92
+ # qualified_name -> list of definition locations
93
+ definitions: dict[str, list[SymbolLocation]] = field(default_factory=dict)
94
+ # symbol name -> list of reference sites
95
+ references: dict[str, list[SymbolRef]] = field(default_factory=dict)
96
+ # file path -> set of qualified symbol names defined in that file
97
+ file_symbols: dict[str, set[str]] = field(default_factory=dict)
98
+ # file path -> set of file paths it imports from
99
+ file_dependencies: dict[str, set[str]] = field(default_factory=dict)
100
+ # file path -> set of file paths that import it
101
+ file_dependents: dict[str, set[str]] = field(default_factory=dict)
102
+ # caller qualified name -> set of callee bare/qualified names
103
+ call_edges: dict[str, set[str]] = field(default_factory=dict)
104
+ # callee bare/qualified name -> set of caller qualified names (reverse)
105
+ callers_of: dict[str, set[str]] = field(default_factory=dict)
106
+
107
+ # --- Inverted name indexes (populated by add_definition) ---
108
+ # bare name -> set of qualified names
109
+ _name_to_qnames: dict[str, set[str]] = field(default_factory=dict)
110
+ # lowercased bare name -> set of qualified names
111
+ _lower_to_qnames: dict[str, set[str]] = field(default_factory=dict)
112
+ # individual token -> set of qualified names
113
+ _tokens_to_qnames: dict[str, set[str]] = field(default_factory=dict)
114
+
115
+ # Optional persistent store (set via set_store)
116
+ _store: Any = field(default=None, repr=False)
117
+
118
+ # Re-entrant lock guarding all mutating methods plus the iterating
119
+ # reads that need a consistent snapshot. Reentrancy matters because
120
+ # ``add_reference`` may call ``add_call_edge`` and ``remove_file``
121
+ # walks every other map. Background hydration (``ASTService``'s
122
+ # daemon thread) and LSP/MCP request threads can mutate the index
123
+ # concurrently, so the lock prevents corrupted dicts and torn lists.
124
+ _lock: threading.RLock = field(
125
+ default_factory=threading.RLock, repr=False, compare=False,
126
+ )
127
+
128
+ def set_store(self, store: IndexStore) -> None:
129
+ """Attach an IndexStore for write-through persistence."""
130
+ self._store = store
131
+
132
+ def persist_file(self, file_path: str) -> None:
133
+ """Write-through: save current in-memory symbols/refs for a file to the store."""
134
+ if self._store is None:
135
+ return
136
+ with self._lock:
137
+ self._persist_file_locked(file_path)
138
+
139
+ def persist_files(self, file_paths: set[str]) -> None:
140
+ """Persist a changed batch without rescanning all references per file."""
141
+ with self._lock:
142
+ grouped: dict[str, list[SymbolRef]] = {}
143
+ for refs in self.references.values():
144
+ for ref in refs:
145
+ if ref.file_path in file_paths:
146
+ grouped.setdefault(ref.file_path, []).append(ref)
147
+ for path in sorted(file_paths):
148
+ self._persist_file_locked(path, grouped.get(path, []))
149
+
150
+ def _persist_file_locked(self, file_path: str, references=None) -> None:
151
+ if self._store is None:
152
+ return
153
+ # Collect symbols for this file
154
+ sym_dicts: list[dict[str, Any]] = []
155
+ for qname in self.file_symbols.get(file_path, set()):
156
+ for loc in self.definitions.get(qname, []):
157
+ if loc.file_path == file_path:
158
+ sym_dicts.append({
159
+ "name": loc.name,
160
+ "qualified_name": loc.qualified_name,
161
+ "kind": loc.kind,
162
+ "line": loc.start_line,
163
+ "end_line": loc.end_line,
164
+ "source": loc.source,
165
+ })
166
+ self._store.save_symbols(file_path, sym_dicts)
167
+
168
+ # Collect references originating from this file
169
+ ref_dicts: list[dict[str, Any]] = []
170
+ refs = references if references is not None else (
171
+ ref for refs in self.references.values() for ref in refs if ref.file_path == file_path
172
+ )
173
+ for ref in refs:
174
+ ref_dicts.append({
175
+ "symbol_name": ref.symbol_name,
176
+ "ref_kind": ref.ref_kind,
177
+ "line": ref.line,
178
+ "column": 0,
179
+ "source": ref.source,
180
+ "syntax_name": ref.syntax_name,
181
+ "caller_qualified_name": ref.caller_qualified_name,
182
+ })
183
+ self._store.save_references(file_path, ref_dicts)
184
+
185
+ def load_from_store(self) -> int:
186
+ """Bulk-load symbols, references, and dependencies from the store.
187
+
188
+ Returns the number of files loaded.
189
+ """
190
+ if self._store is None:
191
+ return 0
192
+ with self._lock:
193
+ return self._load_from_store_locked()
194
+
195
+ def _load_from_store_locked(self) -> int:
196
+ if self._store is None:
197
+ return 0
198
+
199
+ stored_symbols = self._store.load_symbols()
200
+ stored_refs = self._store.load_references()
201
+ stored_deps = self._store.load_dependencies()
202
+
203
+ files_loaded: set[str] = set()
204
+
205
+ for s in stored_symbols:
206
+ loc = SymbolLocation(
207
+ name=s.name,
208
+ qualified_name=s.qualified_name,
209
+ kind=s.kind,
210
+ file_path=s.file_path,
211
+ start_line=s.line,
212
+ end_line=s.end_line,
213
+ source=s.source,
214
+ )
215
+ self.add_definition(loc)
216
+ files_loaded.add(s.file_path)
217
+
218
+ for r in stored_refs:
219
+ ref = SymbolRef(
220
+ symbol_name=r.symbol_name,
221
+ ref_kind=r.ref_kind,
222
+ file_path=r.file_path,
223
+ line=r.line,
224
+ source=r.source,
225
+ syntax_name=getattr(r, "syntax_name", ""),
226
+ caller_qualified_name=getattr(r, "caller_qualified_name", ""),
227
+ )
228
+ self.add_reference(ref)
229
+
230
+ for src, targets in stored_deps.items():
231
+ for tgt in targets:
232
+ self.add_file_dependency(src, tgt)
233
+
234
+ logger.debug(
235
+ "CrossRefIndex loaded from store: %d files, %d symbols, %d refs",
236
+ len(files_loaded), len(stored_symbols), len(stored_refs),
237
+ )
238
+ return len(files_loaded)
239
+
240
+ def merge_lsp_results(
241
+ self,
242
+ file_path: str,
243
+ definitions: list[SymbolLocation],
244
+ references: list[SymbolRef],
245
+ *, verified_symbol: bool = False,
246
+ ) -> int:
247
+ """Merge LSP-sourced results into the index.
248
+
249
+ Deduplicates by (qualified_name, file, line) — LSP wins on conflict.
250
+ Returns the number of new entries added.
251
+ """
252
+ with self._lock:
253
+ return self._merge_lsp_results_locked(file_path, definitions, references, verified_symbol=verified_symbol)
254
+
255
+ def _merge_lsp_results_locked(
256
+ self,
257
+ file_path: str,
258
+ definitions: list[SymbolLocation],
259
+ references: list[SymbolRef],
260
+ *, verified_symbol: bool = False,
261
+ ) -> int:
262
+ added = 0
263
+
264
+ for loc in definitions:
265
+ # Create a copy with source="lsp" — don't mutate caller's input
266
+ lsp_loc = SymbolLocation(
267
+ name=loc.name, qualified_name=loc.qualified_name,
268
+ kind=loc.kind, file_path=loc.file_path,
269
+ start_line=loc.start_line, end_line=loc.end_line,
270
+ source="lsp",
271
+ )
272
+ existing = self.definitions.get(lsp_loc.qualified_name, [])
273
+ # Check for duplicate at same location
274
+ dup = False
275
+ for i, ex in enumerate(existing):
276
+ if ex.file_path == lsp_loc.file_path and ex.start_line == lsp_loc.start_line:
277
+ # LSP wins: replace tree-sitter entry
278
+ existing[i] = lsp_loc
279
+ dup = True
280
+ break
281
+ if not dup:
282
+ self.add_definition(lsp_loc)
283
+ added += 1
284
+
285
+ for ref in references:
286
+ lsp_ref = SymbolRef(
287
+ symbol_name=ref.symbol_name, ref_kind=ref.ref_kind,
288
+ file_path=ref.file_path, line=ref.line,
289
+ source="lsp",
290
+ syntax_name=ref.syntax_name,
291
+ caller_qualified_name=ref.caller_qualified_name,
292
+ )
293
+ # LSP wins on (file, line) collision — but ONLY when the
294
+ # incoming ref carries a resolved caller. Without one, the
295
+ # legacy ``ingest_lsp_results`` fallback path stores the
296
+ # *enclosing function* in ``symbol_name`` (not the callee),
297
+ # so replacing a tree-sitter entry would invert the edge.
298
+ # In that case we dedupe-skip, preserving the existing
299
+ # higher-precision tree-sitter ref.
300
+ existing = self.references.get(lsp_ref.symbol_name, [])
301
+ dup_index: int | None = None
302
+ for i, ex in enumerate(existing):
303
+ if ex.file_path == lsp_ref.file_path and ex.line == lsp_ref.line:
304
+ dup_index = i
305
+ break
306
+
307
+ if dup_index is not None:
308
+ if lsp_ref.caller_qualified_name or verified_symbol:
309
+ if not lsp_ref.syntax_name:
310
+ lsp_ref.syntax_name = existing[dup_index].syntax_name
311
+ existing[dup_index] = lsp_ref
312
+ if lsp_ref.ref_kind == "call":
313
+ self.add_call_edge(
314
+ lsp_ref.caller_qualified_name,
315
+ lsp_ref.symbol_name,
316
+ )
317
+ # else: no caller info — leave the tree-sitter entry alone.
318
+ else:
319
+ self.add_reference(lsp_ref)
320
+ added += 1
321
+
322
+ # Persist if store is available
323
+ self.persist_file(file_path)
324
+ return added
325
+
326
+ def add_definition(self, loc: SymbolLocation) -> None:
327
+ """Register a symbol definition."""
328
+ with self._lock:
329
+ self.definitions.setdefault(loc.qualified_name, []).append(loc)
330
+ self.file_symbols.setdefault(loc.file_path, set()).add(loc.qualified_name)
331
+ # Populate inverted indexes
332
+ bare = loc.qualified_name.rsplit(".", 1)[-1]
333
+ self._name_to_qnames.setdefault(bare, set()).add(loc.qualified_name)
334
+ self._lower_to_qnames.setdefault(bare.lower(), set()).add(loc.qualified_name)
335
+ for token in _split_name_tokens(bare):
336
+ self._tokens_to_qnames.setdefault(token, set()).add(loc.qualified_name)
337
+
338
+ def add_reference(self, ref: SymbolRef) -> None:
339
+ """Register a symbol reference."""
340
+ with self._lock:
341
+ self.references.setdefault(ref.symbol_name, []).append(ref)
342
+ if ref.ref_kind == "call" and ref.caller_qualified_name:
343
+ self.add_call_edge(ref.caller_qualified_name, ref.symbol_name)
344
+
345
+ def add_call_edge(self, caller: str, callee: str) -> None:
346
+ """Record that ``caller`` calls ``callee``.
347
+
348
+ ``caller`` should be a qualified name (e.g. ``MyClass.do_thing`` or a
349
+ bare top-level function name). ``callee`` is whatever the source
350
+ wrote — bare name when type/scope resolution is unavailable; the
351
+ consumer can disambiguate against ``self.definitions``.
352
+ """
353
+ if not caller or not callee:
354
+ return
355
+ with self._lock:
356
+ self.call_edges.setdefault(caller, set()).add(callee)
357
+ self.callers_of.setdefault(callee, set()).add(caller)
358
+
359
+ def add_file_dependency(self, source: str, target: str) -> None:
360
+ """Record that *source* imports from *target*."""
361
+ with self._lock:
362
+ self.file_dependencies.setdefault(source, set()).add(target)
363
+ self.file_dependents.setdefault(target, set()).add(source)
364
+
365
+ def remove_file(self, file_path: str) -> None:
366
+ """Remove all index entries for a file (in-memory and store)."""
367
+ with self._lock:
368
+ self._remove_file_locked(file_path)
369
+
370
+ def _remove_file_locked(self, file_path: str) -> None:
371
+ if self._store is not None:
372
+ self._store.remove_file(file_path)
373
+ # Snapshot callers defined here BEFORE we mutate file_symbols below.
374
+ callers_in_file = set(self.file_symbols.get(file_path, set()))
375
+ # Remove definitions and clean up inverted indexes
376
+ for qname in list(self.file_symbols.get(file_path, [])):
377
+ defs = self.definitions.get(qname, [])
378
+ self.definitions[qname] = [d for d in defs if d.file_path != file_path]
379
+ if not self.definitions[qname]:
380
+ del self.definitions[qname]
381
+ # Clean up inverted indexes — qname fully gone
382
+ bare = qname.rsplit(".", 1)[-1]
383
+ s = self._name_to_qnames.get(bare)
384
+ if s:
385
+ s.discard(qname)
386
+ if not s:
387
+ del self._name_to_qnames[bare]
388
+ s = self._lower_to_qnames.get(bare.lower())
389
+ if s:
390
+ s.discard(qname)
391
+ if not s:
392
+ del self._lower_to_qnames[bare.lower()]
393
+ for token in _split_name_tokens(bare):
394
+ s = self._tokens_to_qnames.get(token)
395
+ if s:
396
+ s.discard(qname)
397
+ if not s:
398
+ del self._tokens_to_qnames[token]
399
+ self.file_symbols.pop(file_path, None)
400
+
401
+ # Remove references originating from this file
402
+ for name, refs in list(self.references.items()):
403
+ self.references[name] = [r for r in refs if r.file_path != file_path]
404
+ if not self.references[name]:
405
+ del self.references[name]
406
+
407
+ # Drop call edges whose caller was defined in this file. We can't
408
+ # reliably attribute callees from the file path alone (callees are
409
+ # bare names), so this is a best-effort scrub keyed on caller.
410
+ for caller in callers_in_file:
411
+ for callee in self.call_edges.pop(caller, set()):
412
+ rev = self.callers_of.get(callee)
413
+ if rev is not None:
414
+ rev.discard(caller)
415
+ if not rev:
416
+ del self.callers_of[callee]
417
+
418
+ # Remove dependency edges
419
+ for dep in self.file_dependencies.pop(file_path, set()):
420
+ dependents = self.file_dependents.get(dep)
421
+ if dependents:
422
+ dependents.discard(file_path)
423
+ if not dependents:
424
+ del self.file_dependents[dep]
425
+
426
+ # Remove as a dependent target
427
+ for src in list(self.file_dependents.get(file_path, [])):
428
+ deps = self.file_dependencies.get(src)
429
+ if deps:
430
+ deps.discard(file_path)
431
+ if not deps:
432
+ del self.file_dependencies[src]
433
+ self.file_dependents.pop(file_path, None)
434
+
435
+ # ------------------------------------------------------------------
436
+ # Search
437
+ # ------------------------------------------------------------------
438
+
439
+ def get_definitions(self, symbol_name: str, *, include_shadowed: bool = False) -> list[SymbolLocation]:
440
+ """Look up definitions for a symbol (exact or suffix match)."""
441
+ # Try exact qualified name first
442
+ if symbol_name in self.definitions and not include_shadowed:
443
+ return self.definitions[symbol_name]
444
+ # Try suffix match (e.g. "discover_files" matches "CodebaseContextManager.discover_files")
445
+ results: list[SymbolLocation] = []
446
+ for qname, locs in self.definitions.items():
447
+ if qname == symbol_name or qname.endswith(f".{symbol_name}"):
448
+ results.extend(locs)
449
+ return results
450
+
451
+ def search_definitions(
452
+ self,
453
+ symbol_name: str,
454
+ *,
455
+ limit: int = 50,
456
+ kind_filter: str = "",
457
+ ) -> list[tuple[SymbolLocation, float]]:
458
+ """Multi-strategy symbol search returning (location, score) pairs.
459
+
460
+ Search strategies (in priority order):
461
+ 1. Exact qualified name match (score 1.0)
462
+ 2. Exact bare name match via index (score 0.95)
463
+ 3. Case-insensitive bare name match (score 0.85)
464
+ 4. Prefix match on bare name (score 0.75)
465
+ 5. Substring match on bare name (score 0.60)
466
+ 6. Token overlap (camelCase/snake_case) (score 0.50)
467
+ """
468
+ # Collect candidates as {qualified_name: best_match_score}
469
+ candidates: dict[str, float] = {}
470
+
471
+ # 1. Exact qualified name
472
+ if symbol_name in self.definitions:
473
+ candidates[symbol_name] = 1.0
474
+
475
+ # 2. Exact bare name via inverted index — O(1)
476
+ bare_qnames = self._name_to_qnames.get(symbol_name, set())
477
+ for qn in bare_qnames:
478
+ if qn not in candidates:
479
+ candidates[qn] = 0.95
480
+
481
+ # 3. Case-insensitive bare name — O(1)
482
+ lower_qnames = self._lower_to_qnames.get(symbol_name.lower(), set())
483
+ for qn in lower_qnames:
484
+ if qn not in candidates:
485
+ candidates[qn] = 0.85
486
+
487
+ name_lower = symbol_name.lower()
488
+
489
+ # 4. Prefix match on bare names — scan index keys
490
+ for bare, qnames in self._name_to_qnames.items():
491
+ if bare.lower().startswith(name_lower) and len(bare) > len(symbol_name):
492
+ for qn in qnames:
493
+ if qn not in candidates:
494
+ candidates[qn] = 0.75
495
+
496
+ # 5. Substring match on bare names
497
+ if len(symbol_name) >= 2:
498
+ for bare, qnames in self._name_to_qnames.items():
499
+ if name_lower in bare.lower() and bare.lower() != name_lower:
500
+ for qn in qnames:
501
+ if qn not in candidates:
502
+ candidates[qn] = 0.60
503
+
504
+ # 6. Token overlap (camelCase/snake_case)
505
+ query_tokens = set(_split_name_tokens(symbol_name))
506
+ if query_tokens:
507
+ token_hits: dict[str, int] = {}
508
+ for token in query_tokens:
509
+ for qn in self._tokens_to_qnames.get(token, set()):
510
+ if qn not in candidates:
511
+ token_hits[qn] = token_hits.get(qn, 0) + 1
512
+ for qn, hit_count in token_hits.items():
513
+ # Score proportional to fraction of query tokens matched
514
+ score = 0.50 * (hit_count / len(query_tokens))
515
+ if score >= 0.25: # at least half the tokens must match
516
+ candidates[qn] = score
517
+
518
+ if not candidates:
519
+ return []
520
+
521
+ # Resolve candidates to SymbolLocations and compute composite scores
522
+ scored: list[tuple[SymbolLocation, float]] = []
523
+ for qname, match_score in candidates.items():
524
+ locs = self.definitions.get(qname, [])
525
+ for loc in locs:
526
+ if kind_filter and loc.kind != kind_filter:
527
+ continue
528
+ score = _rank_score(loc, match_score)
529
+ scored.append((loc, score))
530
+
531
+ scored.sort(key=lambda x: -x[1])
532
+ return scored[:limit]
533
+
534
+ def get_references(self, symbol_name: str) -> list[SymbolRef]:
535
+ """Look up all call sites / references for a symbol (exact or suffix match)."""
536
+ # Include exact and qualified candidates; an exact key must not hide others.
537
+ # Suffix match (e.g. "clear" matches refs keyed as "MCPMetaTools.clear")
538
+ results: list[SymbolRef] = []
539
+ for ref_name, refs in self.references.items():
540
+ if ref_name == symbol_name or ref_name.endswith(f".{symbol_name}"):
541
+ results.extend(refs)
542
+ return results
543
+
544
+ def get_dependents(self, file_path: str) -> set[str]:
545
+ """Files that import from *file_path*."""
546
+ return self.file_dependents.get(file_path, set())
547
+
548
+ def get_dependencies(self, file_path: str) -> set[str]:
549
+ """Files that *file_path* imports from."""
550
+ return self.file_dependencies.get(file_path, set())
551
+
552
+ # ------------------------------------------------------------------
553
+ # Call graph
554
+ # ------------------------------------------------------------------
555
+
556
+ def get_callees(self, caller: str, *, depth: int = 1) -> set[str]:
557
+ """Return symbols called by ``caller`` up to ``depth`` hops.
558
+
559
+ depth=1 returns direct callees only; depth=N follows up to N hops
560
+ through the in-memory ``call_edges`` map. Cycles are handled by
561
+ an internal visited set so the traversal always terminates.
562
+ """
563
+ if depth < 1:
564
+ return set()
565
+ with self._lock:
566
+ visited: set[str] = set()
567
+ frontier: set[str] = {caller}
568
+ result: set[str] = set()
569
+ for _ in range(depth):
570
+ next_frontier: set[str] = set()
571
+ for node in frontier:
572
+ if node in visited:
573
+ continue
574
+ visited.add(node)
575
+ callees = self.call_edges.get(node, set())
576
+ result.update(callees)
577
+ next_frontier.update(callees - visited)
578
+ if not next_frontier:
579
+ break
580
+ frontier = next_frontier
581
+ return result
582
+
583
+ def get_callers(self, callee: str, *, depth: int = 1) -> set[str]:
584
+ """Return symbols that call ``callee`` up to ``depth`` hops back."""
585
+ if depth < 1:
586
+ return set()
587
+ with self._lock:
588
+ visited: set[str] = set()
589
+ frontier: set[str] = {callee}
590
+ result: set[str] = set()
591
+ for _ in range(depth):
592
+ next_frontier: set[str] = set()
593
+ for node in frontier:
594
+ if node in visited:
595
+ continue
596
+ visited.add(node)
597
+ callers = self.callers_of.get(node, set())
598
+ result.update(callers)
599
+ next_frontier.update(callers - visited)
600
+ if not next_frontier:
601
+ break
602
+ frontier = next_frontier
603
+ return result
604
+
605
+
606
+ def _rank_score(loc: SymbolLocation, match_score: float) -> float:
607
+ """Composite ranking combining match quality and symbol importance."""
608
+ importance = 0.0
609
+ if not loc.name.startswith("_"):
610
+ importance += 0.02
611
+ kind_boost = {"class": 0.03, "interface": 0.03, "function": 0.01, "method": 0.0}
612
+ importance += kind_boost.get(loc.kind, 0.0)
613
+ # LSP-sourced entries are more precise
614
+ if loc.source == "lsp":
615
+ importance += 0.05
616
+ return match_score + importance