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,240 @@
1
+ """Content-hash-gated incremental update pipeline.
2
+
3
+ This is the core pipeline that processes file changes from hook notifications.
4
+ It reads file content, computes SHA-256, compares to the current branch manifest,
5
+ and only re-parses/re-indexes files whose content has actually changed.
6
+
7
+ Flow:
8
+ paths → read content → hash → compare to manifest → skip unchanged
9
+ → store new content → update overlay → parse symbols → queue embeddings
10
+ → publish event
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ import uuid
17
+ from pathlib import Path
18
+ from typing import TYPE_CHECKING
19
+
20
+ from attocode_intel.indexing.parser import detect_language, extract_imports, extract_symbols
21
+
22
+ if TYPE_CHECKING:
23
+ from sqlalchemy.ext.asyncio import AsyncSession
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ _MAX_FILE_SIZE = 1_000_000 # 1MB
28
+
29
+
30
+ class IncrementalPipeline:
31
+ """Content-hash-gated incremental indexing pipeline.
32
+
33
+ Given a list of changed file paths and a branch, this pipeline:
34
+ 1. Reads each file from disk (local mode) or git (service mode)
35
+ 2. Computes SHA-256 of the content
36
+ 3. Compares to current manifest — skips if SHA matches (no-op)
37
+ 4. Stores new content in content-addressed store
38
+ 5. Updates branch overlay (path → new SHA)
39
+ 6. Extracts symbols (skips if SHA already has symbols — dedup)
40
+ 7. Queues embedding generation for new content
41
+ 8. Returns stats about what changed
42
+ """
43
+
44
+ def __init__(self, session: AsyncSession) -> None:
45
+ from attocode_intel.storage.branch_overlay import BranchOverlay
46
+ from attocode_intel.storage.content_store import ContentStore
47
+ from attocode_intel.storage.embedding_store import EmbeddingStore
48
+ from attocode_intel.storage.symbol_store import SymbolStore
49
+
50
+ self._session = session
51
+ from attocode_intel.storage.dependency_store import DependencyStore
52
+
53
+ self._content_store = ContentStore(session)
54
+ self._symbol_store = SymbolStore(session)
55
+ self._embedding_store = EmbeddingStore(session)
56
+ self._overlay = BranchOverlay(session)
57
+ self._dep_store = DependencyStore(session)
58
+
59
+ @staticmethod
60
+ async def acquire_branch_lock(session: AsyncSession, branch_id: uuid.UUID) -> None:
61
+ """Acquire a PostgreSQL advisory lock for a branch to prevent concurrent indexing."""
62
+ from sqlalchemy import text
63
+
64
+ lock_key = int.from_bytes(branch_id.bytes[:8], "big") & 0x7FFFFFFFFFFFFFFF
65
+ await session.execute(
66
+ text("SELECT pg_advisory_xact_lock(:key)").bindparams(key=lock_key)
67
+ )
68
+
69
+ async def process_file_changes(
70
+ self,
71
+ branch_id: uuid.UUID,
72
+ paths: list[str],
73
+ base_dir: str | None = None,
74
+ git_reader: object | None = None,
75
+ repo_id: str | None = None,
76
+ ref: str | None = None,
77
+ file_contents: dict[str, bytes] | None = None,
78
+ ) -> dict:
79
+ """Process a batch of file changes for a branch.
80
+
81
+ Content sources (checked in order):
82
+ 1. file_contents — inline content sent by the client (remote mode)
83
+ 2. base_dir — read from local filesystem
84
+ 3. git_reader+repo_id+ref — read from git
85
+
86
+ Returns stats: {processed, skipped_unchanged, skipped_missing,
87
+ skipped_too_large, symbols_updated, errors}
88
+ """
89
+ stats = {
90
+ "processed": 0,
91
+ "skipped_unchanged": 0,
92
+ "skipped_missing": 0,
93
+ "skipped_too_large": 0,
94
+ "skipped_binary": 0,
95
+ "symbols_updated": 0,
96
+ "errors": 0,
97
+ }
98
+
99
+ # Acquire advisory lock to prevent concurrent indexing on same branch
100
+ try:
101
+ await self.acquire_branch_lock(self._session, branch_id)
102
+ except Exception:
103
+ pass # Non-Postgres backends (e.g. tests) — skip locking
104
+
105
+ # Get current manifest for comparison
106
+ manifest = await self._overlay.resolve_manifest(branch_id)
107
+
108
+ # Batch: collect files to update
109
+ overlay_updates: list[tuple[str, str, str]] = [] # (path, sha, status)
110
+
111
+ for path in paths:
112
+ try:
113
+ content = await self._read_file(path, base_dir, git_reader, repo_id, ref, file_contents=file_contents)
114
+
115
+ if content is None:
116
+ # File was deleted
117
+ if path in manifest:
118
+ await self._overlay.delete_file(branch_id, path)
119
+ stats["processed"] += 1
120
+ else:
121
+ stats["skipped_missing"] += 1
122
+ continue
123
+
124
+ if len(content) > _MAX_FILE_SIZE:
125
+ stats["skipped_too_large"] += 1
126
+ continue
127
+
128
+ # Content-hash comparison — the key optimization
129
+ sha = self._content_store.hash_content(content)
130
+ current_sha = manifest.get(path)
131
+
132
+ if sha == current_sha:
133
+ stats["skipped_unchanged"] += 1
134
+ continue
135
+
136
+ # New or changed content
137
+ language = detect_language(path)
138
+ await self._content_store.store(content, language)
139
+
140
+ # Determine status
141
+ status = "added" if path not in manifest else "modified"
142
+ overlay_updates.append((path, sha, status))
143
+
144
+ # Extract symbols (skip if SHA already indexed — content dedup)
145
+ symbols = extract_symbols(content, path)
146
+ if symbols:
147
+ count = await self._symbol_store.upsert_symbols(
148
+ sha, symbols, skip_if_exists=True
149
+ )
150
+ stats["symbols_updated"] += count or len(symbols)
151
+
152
+ # Extract imports and resolve to dependencies
153
+ imports = extract_imports(content, path, language)
154
+ if imports and not await self._dep_store.has_dependencies(sha):
155
+ from attocode_intel.indexing.full_indexer import _resolve_import_path
156
+
157
+ known_paths = set(manifest.keys()) | {u[0] for u in overlay_updates}
158
+ path_to_sha = dict(manifest)
159
+ for u_path, u_sha, _ in overlay_updates:
160
+ path_to_sha[u_path] = u_sha
161
+
162
+ deps = []
163
+ for imp in imports:
164
+ target = _resolve_import_path(imp, path, known_paths, language)
165
+ if target and target in path_to_sha:
166
+ deps.append({
167
+ "target_sha": path_to_sha[target],
168
+ "dep_type": "import",
169
+ "weight": 1.0,
170
+ })
171
+ if deps:
172
+ await self._dep_store.upsert_dependencies(sha, deps)
173
+ stats["dependencies_extracted"] = stats.get("dependencies_extracted", 0) + len(deps)
174
+
175
+ # Queue embedding generation (dedup: skip if already embedded)
176
+ if not await self._embedding_store.has_embeddings(sha):
177
+ if repo_id:
178
+ from attocode_intel.workers.job_utils import enqueue_embedding_job
179
+ await enqueue_embedding_job(repo_id, ref or "main")
180
+
181
+ stats["processed"] += 1
182
+
183
+ except Exception as e:
184
+ logger.warning("Error processing %s: %s", path, e)
185
+ stats["errors"] += 1
186
+
187
+ # Batch update overlay (single version bump)
188
+ if overlay_updates:
189
+ await self._overlay.set_files_batch(branch_id, overlay_updates)
190
+
191
+ return stats
192
+
193
+ async def _read_file(
194
+ self,
195
+ path: str,
196
+ base_dir: str | None,
197
+ git_reader: object | None,
198
+ repo_id: str | None,
199
+ ref: str | None,
200
+ file_contents: dict[str, bytes] | None = None,
201
+ ) -> bytes | None:
202
+ """Read file content from inline content, disk, or git.
203
+
204
+ Content sources (checked in order):
205
+ 1. file_contents — inline content sent by the client
206
+ 2. base_dir — local filesystem
207
+ 3. git_reader — git object reader
208
+
209
+ Returns None if file doesn't exist (deleted) or no content source available.
210
+ C1 fix: Validates resolved path is within base_dir to prevent path traversal.
211
+ """
212
+ # Priority 1: inline content (sent by client for remote servers)
213
+ if file_contents is not None and path in file_contents:
214
+ return file_contents[path]
215
+
216
+ # Priority 2: local filesystem
217
+ if base_dir:
218
+ base = Path(base_dir).resolve()
219
+ full_path = (base / path).resolve()
220
+ # C1: prevent path traversal (e.g. ../../etc/passwd)
221
+ if not full_path.is_relative_to(base):
222
+ raise ValueError(
223
+ f"Path traversal detected: '{path}' resolves outside base directory"
224
+ )
225
+ if not full_path.is_file():
226
+ return None
227
+ try:
228
+ return full_path.read_bytes()
229
+ except (OSError, PermissionError):
230
+ return None
231
+
232
+ # Priority 3: git reader
233
+ if git_reader and repo_id and ref:
234
+ try:
235
+ return git_reader.read_file(repo_id, ref, path) # type: ignore[union-attr]
236
+ except Exception:
237
+ return None
238
+
239
+ # No content source — file is missing or deleted
240
+ return None
@@ -0,0 +1,403 @@
1
+ """Parser bridge — connects indexing pipeline with existing AST/CodeAnalyzer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ pass
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Language detection by extension
15
+ _EXT_TO_LANG: dict[str, str] = {
16
+ ".py": "python",
17
+ ".js": "javascript",
18
+ ".ts": "typescript",
19
+ ".tsx": "typescript",
20
+ ".jsx": "javascript",
21
+ ".go": "go",
22
+ ".rs": "rust",
23
+ ".java": "java",
24
+ ".rb": "ruby",
25
+ ".c": "c",
26
+ ".cpp": "cpp",
27
+ ".h": "c",
28
+ ".hpp": "cpp",
29
+ ".metal": "cpp",
30
+ ".cs": "csharp",
31
+ ".php": "php",
32
+ ".swift": "swift",
33
+ ".kt": "kotlin",
34
+ ".scala": "scala",
35
+ ".lua": "lua",
36
+ ".ex": "elixir",
37
+ ".exs": "elixir",
38
+ ".sh": "bash",
39
+ ".bash": "bash",
40
+ ".zig": "zig",
41
+ ".nim": "nim",
42
+ ".fs": "fsharp",
43
+ ".fsi": "fsharp",
44
+ ".cr": "crystal",
45
+ ".m": "objc",
46
+ ".mm": "objc",
47
+ ".yaml": "yaml",
48
+ ".yml": "yaml",
49
+ ".toml": "toml",
50
+ ".json": "json",
51
+ ".html": "html",
52
+ ".css": "css",
53
+ }
54
+
55
+
56
+ def detect_language(path: str) -> str | None:
57
+ """Detect programming language from file extension."""
58
+ ext = Path(path).suffix.lower()
59
+ return _EXT_TO_LANG.get(ext)
60
+
61
+
62
+ def parse_content(content_sha: str, content: bytes, filename: str) -> dict:
63
+ """Parse file content and return structured result keyed by content SHA.
64
+
65
+ Bridge function for the content-hash-gated incremental pipeline.
66
+ Returns: {content_sha, language, symbols, imports}
67
+ """
68
+ language = detect_language(filename)
69
+ symbols = extract_symbols(content, filename)
70
+ imports = extract_imports(content, filename, language)
71
+
72
+ return {
73
+ "content_sha": content_sha,
74
+ "language": language,
75
+ "symbols": symbols,
76
+ "imports": imports,
77
+ }
78
+
79
+
80
+ def extract_imports(content: bytes, path: str, language: str | None) -> list[str]:
81
+ """Extract import statements from file content. Returns list of module names."""
82
+ import re
83
+
84
+ if language is None:
85
+ return []
86
+
87
+ try:
88
+ text = content.decode("utf-8", errors="replace")
89
+ except Exception:
90
+ return []
91
+
92
+ imports = []
93
+ if language == "python":
94
+ for m in re.finditer(r"^from\s+([\w.]+)\s+import\s+", text, re.MULTILINE):
95
+ imports.append(m.group(1))
96
+ for m in re.finditer(r"^import\s+([\w.]+(?:\s*,\s*[\w.]+)*)", text, re.MULTILINE):
97
+ for name in m.group(1).split(","):
98
+ name = name.strip().split(" as ")[0].strip()
99
+ if name:
100
+ imports.append(name)
101
+ elif language in ("javascript", "typescript"):
102
+ # import X from 'module'; import { X } from 'module'; import 'module'
103
+ for m in re.finditer(r"""import\s+(?:.*?\s+from\s+)?['"]([\w@/.\\-]+)['"]""", text):
104
+ imports.append(m.group(1))
105
+ # require('module')
106
+ for m in re.finditer(r"""require\s*\(\s*['"]([\w@/.\\-]+)['"]""", text):
107
+ imports.append(m.group(1))
108
+ # Re-exports: export { X } from 'module'; export * from 'module'
109
+ for m in re.finditer(r"""export\s+(?:\*|(?:\{[^}]*\}|\w+)\s+)from\s+['"]([\w@/.\\-]+)['"]""", text):
110
+ imports.append(m.group(1))
111
+ elif language == "go":
112
+ # Restrict to the Go import block, not all quoted strings.
113
+ import_block = re.search(r'import\s*\((.*?)\)', text, re.DOTALL)
114
+ if import_block:
115
+ for m in re.finditer(r'"([\w/.\\-]+)"', import_block.group(1)):
116
+ imports.append(m.group(1))
117
+ # Single import: import "pkg"
118
+ for m in re.finditer(r'^import\s+"([\w/.\\-]+)"', text, re.MULTILINE):
119
+ imports.append(m.group(1))
120
+ elif language == "rust":
121
+ for m in re.finditer(r"^use\s+([\w:]+)", text, re.MULTILINE):
122
+ imports.append(m.group(1))
123
+ elif language == "java":
124
+ for m in re.finditer(r"^import\s+([\w.]+);", text, re.MULTILINE):
125
+ imports.append(m.group(1))
126
+
127
+ return imports
128
+
129
+
130
+ def _enclosing_caller(symbols: list[dict], line: int) -> str:
131
+ """Return the qualified name of the function/method whose line range
132
+ contains ``line``. When several symbols nest, the innermost (smallest
133
+ span) wins. Returns an empty string when no enclosing function exists
134
+ (top-level call) or when symbols extraction failed.
135
+ """
136
+ best_qname = ""
137
+ best_span = -1
138
+ for sym in symbols:
139
+ kind = sym.get("kind", "")
140
+ if kind not in ("function", "method"):
141
+ continue
142
+ start = sym.get("line_start", 0)
143
+ end = sym.get("line_end", start)
144
+ if start <= line <= end:
145
+ span = end - start
146
+ if best_span < 0 or span < best_span:
147
+ qname = sym.get("qualified_name") or sym.get("name") or ""
148
+ if qname:
149
+ best_qname = qname
150
+ best_span = span
151
+ return best_qname
152
+
153
+
154
+ def extract_references(content: bytes, path: str) -> list[dict]:
155
+ """Extract symbol references (call sites, imports) from file content.
156
+
157
+ Returns list of dicts: {symbol_name, ref_kind, line, caller_qualified_name}.
158
+ ``caller_qualified_name`` is the enclosing function/method (best-effort,
159
+ inferred by line range against extracted symbols); empty for top-level
160
+ calls or when symbol extraction is unavailable.
161
+ """
162
+ import re
163
+
164
+ language = detect_language(path)
165
+ if not language or language not in ("python", "javascript", "typescript"):
166
+ return []
167
+
168
+ try:
169
+ text = content.decode("utf-8", errors="replace")
170
+ except Exception:
171
+ return []
172
+
173
+ # Symbols are needed to attribute callers; failure here is non-fatal,
174
+ # we just emit refs without caller_qualified_name.
175
+ try:
176
+ symbols = extract_symbols(content, path)
177
+ except Exception:
178
+ symbols = []
179
+
180
+ refs: list[dict] = []
181
+ seen: set[tuple[str, int]] = set() # dedupe (name, line)
182
+
183
+ for i, line_text in enumerate(text.split("\n"), 1):
184
+ stripped = line_text.strip()
185
+ # Skip comment lines
186
+ if stripped.startswith("#") or stripped.startswith("//"):
187
+ continue
188
+ # Skip definition lines (def/class/function/interface/type)
189
+ if re.match(r"^\s*(?:async\s+)?(?:def|class|function|interface|type)\s", line_text):
190
+ continue
191
+ # Skip import lines — those are tracked as dependencies
192
+ if re.match(r"^\s*(?:from\s|import\s|export\s.*from)", line_text):
193
+ continue
194
+
195
+ caller = _enclosing_caller(symbols, i)
196
+
197
+ # Function/method calls: name(
198
+ for m in re.finditer(r"\b(\w+)\s*\(", stripped):
199
+ name = m.group(1)
200
+ # Skip language keywords and builtins
201
+ if name in _SKIP_NAMES:
202
+ continue
203
+ key = (name, i)
204
+ if key not in seen:
205
+ seen.add(key)
206
+ refs.append({
207
+ "symbol_name": name,
208
+ "ref_kind": "call",
209
+ "line": i,
210
+ "caller_qualified_name": caller,
211
+ })
212
+
213
+ # Method calls: obj.method(
214
+ for m in re.finditer(r"\b\w+\.(\w+)\s*\(", stripped):
215
+ name = m.group(1)
216
+ if name in _SKIP_NAMES:
217
+ continue
218
+ key = (name, i)
219
+ if key not in seen:
220
+ seen.add(key)
221
+ refs.append({
222
+ "symbol_name": name,
223
+ "ref_kind": "call",
224
+ "line": i,
225
+ "caller_qualified_name": caller,
226
+ })
227
+
228
+ return refs
229
+
230
+
231
+ _SKIP_NAMES = frozenset({
232
+ # Python keywords/builtins
233
+ "if", "else", "elif", "for", "while", "try", "except", "finally",
234
+ "with", "as", "return", "yield", "raise", "pass", "break", "continue",
235
+ "print", "len", "range", "list", "dict", "set", "tuple", "str", "int",
236
+ "float", "bool", "type", "super", "isinstance", "issubclass", "hasattr",
237
+ "getattr", "setattr", "delattr", "property", "staticmethod", "classmethod",
238
+ "enumerate", "zip", "map", "filter", "sorted", "reversed", "min", "max",
239
+ "sum", "abs", "any", "all", "next", "iter", "open", "round", "repr",
240
+ # JS/TS keywords/builtins
241
+ "require", "import", "export", "new", "typeof", "void", "delete",
242
+ "Array", "Object", "String", "Number", "Boolean", "Promise", "Date",
243
+ "Math", "JSON", "console", "setTimeout", "setInterval", "clearTimeout",
244
+ "clearInterval", "parseInt", "parseFloat", "Error", "RegExp", "Map", "Set",
245
+ })
246
+
247
+
248
+ def extract_symbols(content: bytes, path: str) -> list[dict]:
249
+ """Extract symbols from file content using tree-sitter if available.
250
+
251
+ Falls back to regex-based extraction for common patterns.
252
+ Returns list of symbol dicts with keys: name, kind, line_start, line_end,
253
+ signature, exported.
254
+ """
255
+ language = detect_language(path)
256
+ if not language:
257
+ return []
258
+
259
+ try:
260
+ text = content.decode("utf-8", errors="replace")
261
+ except Exception:
262
+ return []
263
+
264
+ # Try tree-sitter first
265
+ try:
266
+ return _extract_with_tree_sitter(text, language)
267
+ except Exception:
268
+ pass
269
+
270
+ # Fallback to regex
271
+ return _extract_with_regex(text, language)
272
+
273
+
274
+ def _extract_with_tree_sitter(text: str, language: str) -> list[dict]:
275
+ """Extract symbols using tree-sitter."""
276
+
277
+ # This will raise if tree-sitter is not available
278
+ from attocode_intel._internal.integrations.context.ts_parser import _get_parser as get_parser
279
+
280
+ parser = get_parser(language)
281
+ if parser is None:
282
+ raise RuntimeError("No parser available")
283
+
284
+ tree = parser.parse(bytes(text, "utf-8"))
285
+ symbols = []
286
+
287
+ # Walk tree for function/class definitions
288
+ _walk_tree(tree.root_node, symbols, language)
289
+ return symbols
290
+
291
+
292
+ def _walk_tree(node, symbols: list[dict], language: str) -> None:
293
+ """Recursively walk AST nodes to extract symbol definitions."""
294
+ kind_map = {
295
+ "function_definition": "function",
296
+ "function_declaration": "function",
297
+ "method_definition": "method",
298
+ "method_declaration": "method",
299
+ "class_definition": "class",
300
+ "class_declaration": "class",
301
+ "interface_declaration": "interface",
302
+ "type_alias_declaration": "type",
303
+ "variable_declaration": "variable",
304
+ "const_declaration": "constant",
305
+ "struct_item": "class",
306
+ "enum_item": "class",
307
+ "trait_item": "interface",
308
+ "type_declaration": "type",
309
+ "enum_declaration": "class",
310
+ "struct_declaration": "class",
311
+ "object_declaration": "class",
312
+ "protocol_declaration": "interface",
313
+ }
314
+
315
+ node_type = node.type
316
+ if node_type in kind_map:
317
+ name_node = node.child_by_field_name("name")
318
+ if name_node:
319
+ is_async = node.parent and node.parent.type in (
320
+ "async_function_definition", "decorated_definition",
321
+ ) or node_type.startswith("async_")
322
+ # Build signature from parameters node if available
323
+ sig = None
324
+ params_node = node.child_by_field_name("parameters")
325
+ if params_node:
326
+ sig = params_node.text.decode("utf-8", errors="replace")
327
+ ret_node = node.child_by_field_name("return_type")
328
+ if ret_node:
329
+ sig += f" -> {ret_node.text.decode('utf-8', errors='replace')}"
330
+ exported = False
331
+ if node.parent and node.parent.type == "export_statement":
332
+ exported = True
333
+ symbols.append({
334
+ "name": name_node.text.decode("utf-8"),
335
+ "kind": kind_map[node_type],
336
+ "line_start": node.start_point[0] + 1,
337
+ "line_end": node.end_point[0] + 1,
338
+ "signature": sig,
339
+ "exported": exported,
340
+ "metadata": {"async": True} if is_async else {},
341
+ })
342
+
343
+ for child in node.children:
344
+ _walk_tree(child, symbols, language)
345
+
346
+
347
+ def _extract_with_regex(text: str, language: str) -> list[dict]:
348
+ """Regex-based symbol extraction fallback."""
349
+ import re
350
+
351
+ symbols = []
352
+ lines = text.split("\n")
353
+
354
+ patterns = {
355
+ "python": [
356
+ (r"^(?:async\s+)?def\s+(\w+)", "function"),
357
+ (r"^class\s+(\w+)", "class"),
358
+ ],
359
+ "javascript": [
360
+ (r"(?:export\s+)?(?:async\s+)?function\s+(\w+)", "function"),
361
+ (r"(?:export\s+)?class\s+(\w+)", "class"),
362
+ (r"(?:export\s+)?const\s+(\w+)\s*=", "constant"),
363
+ ],
364
+ "typescript": [
365
+ (r"(?:export\s+)?(?:async\s+)?function\s+(\w+)", "function"),
366
+ (r"(?:export\s+)?class\s+(\w+)", "class"),
367
+ (r"(?:export\s+)?interface\s+(\w+)", "interface"),
368
+ (r"(?:export\s+)?type\s+(\w+)", "type"),
369
+ ],
370
+ }
371
+
372
+ lang_patterns = patterns.get(language, patterns.get("javascript", []))
373
+
374
+ for i, line in enumerate(lines, 1):
375
+ stripped = line.strip()
376
+ for pattern, kind in lang_patterns:
377
+ match = re.match(pattern, stripped)
378
+ if match:
379
+ is_async = "async" in stripped.split("def ")[0] if "def " in stripped else "async" in stripped.split("function")[0] if "function" in stripped else False
380
+ # Extract signature from the line
381
+ sig = None
382
+ paren_start = stripped.find("(")
383
+ if paren_start != -1:
384
+ # Find matching close paren (simple single-line)
385
+ paren_end = stripped.rfind(")")
386
+ if paren_end > paren_start:
387
+ sig = stripped[paren_start:paren_end + 1]
388
+ # Check for return type annotation
389
+ rest = stripped[paren_end + 1:].strip()
390
+ if rest.startswith("->"):
391
+ sig += f" {rest.split(':')[0].strip()}"
392
+ symbols.append({
393
+ "name": match.group(1),
394
+ "kind": kind,
395
+ "line_start": i,
396
+ "line_end": i,
397
+ "signature": sig,
398
+ "exported": "export" in line,
399
+ "metadata": {"async": True} if is_async else {},
400
+ })
401
+ break
402
+
403
+ return symbols