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,602 @@
1
+ """SQLite-backed learning store for cross-agent memory.
2
+
3
+ Stores project learnings (patterns, conventions, gotchas, workarounds,
4
+ anti-patterns) with FTS5 full-text search and scope-aware recall.
5
+ Follows the same storage conventions as ``graph_store.py``.
6
+
7
+ Usage::
8
+
9
+ store = MemoryStore("/path/to/project")
10
+ lid = store.add("convention", "Always use dataclass(slots=True)", scope="src/")
11
+ results = store.recall("dataclass patterns", scope="src/models/")
12
+ store.record_feedback(lid, helpful=True)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import contextlib
18
+ import logging
19
+ import os
20
+ import sqlite3
21
+ import time
22
+ from dataclasses import dataclass, field
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ VALID_TYPES = {"pattern", "antipattern", "workaround", "convention", "gotcha"}
27
+ VALID_STATUSES = {"active", "archived"}
28
+
29
+ # FTS rank threshold for deduplication.
30
+ # FTS5 rank is negative; closer to 0 = better match.
31
+ # -1.0 is a very generous threshold (catches near-exact duplicates).
32
+ _DEDUP_RANK_THRESHOLD = -1.0
33
+
34
+ # Confidence thresholds
35
+ _CONFIDENCE_FLOOR = 0.1
36
+ _CONFIDENCE_CAP = 1.0
37
+ _AUTO_ARCHIVE_THRESHOLD = 0.15
38
+ _UNHELPFUL_AUTO_ARCHIVE = 5
39
+
40
+ # Confidence adjustments
41
+ _HELPFUL_BOOST = 0.05
42
+ _UNHELPFUL_REDUCE = 0.1
43
+ _RECALL_DECAY = 0.001
44
+
45
+
46
+ @dataclass(slots=True)
47
+ class MemoryStore:
48
+ """SQLite-backed persistent store for project learnings.
49
+
50
+ Stores learnings with FTS5 search, scope hierarchy, confidence
51
+ tracking, and automatic deduplication.
52
+ """
53
+
54
+ project_dir: str
55
+ db_path: str = field(default="", repr=False)
56
+ _conn: sqlite3.Connection | None = field(default=None, repr=False)
57
+ _last_decay_at: float = field(default=0.0, repr=False)
58
+
59
+ def __post_init__(self) -> None:
60
+ if not self.db_path:
61
+ store_dir = os.path.join(self.project_dir, ".attocode", "cache")
62
+ os.makedirs(store_dir, exist_ok=True)
63
+ self.db_path = os.path.join(store_dir, "memory.db")
64
+
65
+ self._conn = sqlite3.connect(self.db_path)
66
+ self._conn.execute("PRAGMA journal_mode=WAL")
67
+ self._conn.execute("PRAGMA synchronous=NORMAL")
68
+ self._create_tables()
69
+
70
+ def _get_conn(self) -> sqlite3.Connection:
71
+ """Return the active connection or raise if closed."""
72
+ if self._conn is None:
73
+ raise RuntimeError("MemoryStore connection is closed")
74
+ return self._conn
75
+
76
+ def _create_tables(self) -> None:
77
+ """Create tables if they don't exist."""
78
+ conn = self._get_conn()
79
+ conn.executescript("""
80
+ CREATE TABLE IF NOT EXISTS learnings (
81
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
82
+ type TEXT NOT NULL,
83
+ description TEXT NOT NULL,
84
+ details TEXT NOT NULL DEFAULT '',
85
+ scope TEXT NOT NULL DEFAULT '',
86
+ confidence REAL NOT NULL DEFAULT 0.7,
87
+ apply_count INTEGER NOT NULL DEFAULT 0,
88
+ help_count INTEGER NOT NULL DEFAULT 0,
89
+ unhelpful_count INTEGER NOT NULL DEFAULT 0,
90
+ created_at TEXT NOT NULL,
91
+ updated_at TEXT NOT NULL,
92
+ status TEXT NOT NULL DEFAULT 'active',
93
+ anchor_blob_oid TEXT NOT NULL DEFAULT ''
94
+ );
95
+
96
+ CREATE INDEX IF NOT EXISTS idx_learnings_scope ON learnings(scope);
97
+ CREATE INDEX IF NOT EXISTS idx_learnings_status ON learnings(status);
98
+ CREATE INDEX IF NOT EXISTS idx_learnings_type ON learnings(type);
99
+ """)
100
+ # The anchor index references a column that may not exist yet
101
+ # on pre-2c databases — migrate first, then create the index.
102
+ self._migrate_schema()
103
+
104
+ # FTS5 virtual table — created separately (can't be in executescript
105
+ # with IF NOT EXISTS reliably on all SQLite versions)
106
+ with contextlib.suppress(sqlite3.OperationalError):
107
+ conn.execute("""
108
+ CREATE VIRTUAL TABLE IF NOT EXISTS learnings_fts
109
+ USING fts5(description, details, content=learnings, content_rowid=id)
110
+ """)
111
+
112
+ # Triggers to keep FTS in sync
113
+ for trigger_sql in [
114
+ """CREATE TRIGGER IF NOT EXISTS learnings_ai AFTER INSERT ON learnings BEGIN
115
+ INSERT INTO learnings_fts(rowid, description, details)
116
+ VALUES (new.id, new.description, new.details);
117
+ END""",
118
+ """CREATE TRIGGER IF NOT EXISTS learnings_ad AFTER DELETE ON learnings BEGIN
119
+ INSERT INTO learnings_fts(learnings_fts, rowid, description, details)
120
+ VALUES ('delete', old.id, old.description, old.details);
121
+ END""",
122
+ """CREATE TRIGGER IF NOT EXISTS learnings_au AFTER UPDATE ON learnings BEGIN
123
+ INSERT INTO learnings_fts(learnings_fts, rowid, description, details)
124
+ VALUES ('delete', old.id, old.description, old.details);
125
+ INSERT INTO learnings_fts(rowid, description, details)
126
+ VALUES (new.id, new.description, new.details);
127
+ END""",
128
+ ]:
129
+ with contextlib.suppress(sqlite3.OperationalError):
130
+ conn.execute(trigger_sql)
131
+
132
+ conn.commit()
133
+
134
+ def _migrate_schema(self) -> None:
135
+ """Additive in-place migration for pre-anchor learnings databases.
136
+
137
+ Adds the ``anchor_blob_oid`` column if missing so an existing
138
+ Phase 1 / 2a / 2b store keeps working after the Phase 2c
139
+ upgrade. Idempotent.
140
+ """
141
+ conn = self._get_conn()
142
+ columns = {
143
+ row[1]
144
+ for row in conn.execute("PRAGMA table_info(learnings)").fetchall()
145
+ }
146
+ if "anchor_blob_oid" not in columns:
147
+ conn.execute(
148
+ "ALTER TABLE learnings ADD COLUMN anchor_blob_oid "
149
+ "TEXT NOT NULL DEFAULT ''"
150
+ )
151
+ logger.info("memory_store: migrated learnings table (+ anchor_blob_oid)")
152
+ # Ensure the new partial index is present regardless of whether
153
+ # we just added the column or it was already there.
154
+ with contextlib.suppress(sqlite3.OperationalError):
155
+ conn.execute(
156
+ "CREATE INDEX IF NOT EXISTS idx_learnings_anchor "
157
+ "ON learnings(anchor_blob_oid) WHERE anchor_blob_oid != ''"
158
+ )
159
+ conn.commit()
160
+
161
+ # ------------------------------------------------------------------
162
+ # Write operations
163
+ # ------------------------------------------------------------------
164
+
165
+ def add(
166
+ self,
167
+ type: str, # noqa: A002
168
+ description: str,
169
+ details: str = "",
170
+ scope: str = "",
171
+ confidence: float = 0.7,
172
+ anchor_blob_oid: str = "",
173
+ ) -> int:
174
+ """Record a new learning. Returns learning ID.
175
+
176
+ Deduplicates via FTS similarity — if a highly similar learning
177
+ exists with the same type and scope, updates its confidence instead.
178
+
179
+ Args:
180
+ anchor_blob_oid: Optional content-addressed anchor for the
181
+ file this learning references (e.g. ``"git:abc123"``).
182
+ When set, ``orphan_scan`` can check git reachability to
183
+ detect learnings whose referenced content has been removed
184
+ from the repo. Pass an empty string to fall back to the
185
+ path-based ``scope`` heuristic.
186
+ """
187
+ conn = self._get_conn()
188
+
189
+ if type not in VALID_TYPES:
190
+ raise ValueError(f"Invalid type '{type}'. Must be one of: {VALID_TYPES}")
191
+
192
+ confidence = max(_CONFIDENCE_FLOOR, min(_CONFIDENCE_CAP, confidence))
193
+ now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
194
+
195
+ # Deduplication: check FTS for similar existing learning
196
+ existing_id = self._find_duplicate(type, description, scope)
197
+ if existing_id is not None:
198
+ # Boost confidence of existing learning. Also backfill the
199
+ # anchor_blob_oid if the new call has one and the existing
200
+ # row doesn't — a learning gains provenance on re-record.
201
+ if anchor_blob_oid:
202
+ conn.execute(
203
+ """UPDATE learnings
204
+ SET confidence = MIN(?, confidence + 0.05),
205
+ updated_at = ?,
206
+ anchor_blob_oid = CASE
207
+ WHEN anchor_blob_oid = '' THEN ?
208
+ ELSE anchor_blob_oid
209
+ END
210
+ WHERE id = ?""",
211
+ (_CONFIDENCE_CAP, now, anchor_blob_oid, existing_id),
212
+ )
213
+ else:
214
+ conn.execute(
215
+ """UPDATE learnings
216
+ SET confidence = MIN(?, confidence + 0.05),
217
+ updated_at = ?
218
+ WHERE id = ?""",
219
+ (_CONFIDENCE_CAP, now, existing_id),
220
+ )
221
+ conn.commit()
222
+ return existing_id
223
+
224
+ cursor = conn.execute(
225
+ """INSERT INTO learnings
226
+ (type, description, details, scope, confidence,
227
+ anchor_blob_oid, created_at, updated_at)
228
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
229
+ (type, description, details, scope, confidence,
230
+ anchor_blob_oid, now, now),
231
+ )
232
+ conn.commit()
233
+ return cursor.lastrowid # type: ignore[return-value]
234
+
235
+ def _find_duplicate(self, type: str, description: str, scope: str) -> int | None: # noqa: A002
236
+ """Check if a similar learning already exists."""
237
+ conn = self._get_conn()
238
+ try:
239
+ cursor = conn.execute(
240
+ """SELECT l.id, rank
241
+ FROM learnings_fts f
242
+ JOIN learnings l ON l.id = f.rowid
243
+ WHERE learnings_fts MATCH ?
244
+ AND l.type = ?
245
+ AND l.scope = ?
246
+ AND l.status = 'active'
247
+ ORDER BY rank DESC
248
+ LIMIT 1""",
249
+ (self._escape_fts_query(description), type, scope),
250
+ )
251
+ row = cursor.fetchone()
252
+ if row and row[1] > _DEDUP_RANK_THRESHOLD:
253
+ return row[0]
254
+ except sqlite3.OperationalError:
255
+ pass # FTS match syntax issue — skip dedup
256
+ return None
257
+
258
+ def record_feedback(self, learning_id: int, helpful: bool) -> None:
259
+ """Record feedback on a learning.
260
+
261
+ Helpful: boost confidence by 0.05 (cap 1.0).
262
+ Unhelpful: reduce confidence by 0.1 (floor 0.1).
263
+ Auto-archive if unhelpful_count >= 5 or confidence < 0.15.
264
+ """
265
+ conn = self._get_conn()
266
+ now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
267
+
268
+ if helpful:
269
+ conn.execute(
270
+ """UPDATE learnings
271
+ SET help_count = help_count + 1,
272
+ confidence = MIN(?, confidence + ?),
273
+ updated_at = ?
274
+ WHERE id = ?""",
275
+ (_CONFIDENCE_CAP, _HELPFUL_BOOST, now, learning_id),
276
+ )
277
+ else:
278
+ conn.execute(
279
+ """UPDATE learnings
280
+ SET unhelpful_count = unhelpful_count + 1,
281
+ confidence = MAX(?, confidence - ?),
282
+ updated_at = ?
283
+ WHERE id = ?""",
284
+ (_CONFIDENCE_FLOOR, _UNHELPFUL_REDUCE, now, learning_id),
285
+ )
286
+
287
+ # Auto-archive check
288
+ conn.execute(
289
+ """UPDATE learnings
290
+ SET status = 'archived', updated_at = ?
291
+ WHERE id = ?
292
+ AND (unhelpful_count >= ? OR confidence < ?)""",
293
+ (now, learning_id, _UNHELPFUL_AUTO_ARCHIVE, _AUTO_ARCHIVE_THRESHOLD),
294
+ )
295
+ conn.commit()
296
+
297
+ def record_applied(self, learning_id: int) -> None:
298
+ """Increment apply_count (called when learning injected into context)."""
299
+ conn = self._get_conn()
300
+ conn.execute(
301
+ "UPDATE learnings SET apply_count = apply_count + 1 WHERE id = ?",
302
+ (learning_id,),
303
+ )
304
+ conn.commit()
305
+
306
+ def update(self, learning_id: int, **kwargs: str | float) -> None:
307
+ """Update fields on a learning."""
308
+ conn = self._get_conn()
309
+ allowed = {"type", "description", "details", "scope", "confidence", "status"}
310
+ updates = {k: v for k, v in kwargs.items() if k in allowed}
311
+ if not updates:
312
+ return
313
+
314
+ if "type" in updates and updates["type"] not in VALID_TYPES:
315
+ raise ValueError(f"Invalid type '{updates['type']}'. Must be one of: {VALID_TYPES}")
316
+ if "status" in updates and updates["status"] not in VALID_STATUSES:
317
+ raise ValueError(
318
+ f"Invalid status '{updates['status']}'. Must be one of: {VALID_STATUSES}"
319
+ )
320
+
321
+ now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
322
+ updates["updated_at"] = now
323
+
324
+ set_clause = ", ".join(f"{k} = ?" for k in updates)
325
+ values = list(updates.values()) + [learning_id]
326
+ conn.execute(
327
+ f"UPDATE learnings SET {set_clause} WHERE id = ?",
328
+ values,
329
+ )
330
+ conn.commit()
331
+
332
+ # ------------------------------------------------------------------
333
+ # Read operations
334
+ # ------------------------------------------------------------------
335
+
336
+ def recall(
337
+ self,
338
+ query: str,
339
+ scope: str = "",
340
+ max_results: int = 10,
341
+ ) -> list[dict]:
342
+ """Retrieve relevant learnings by FTS + scope filtering.
343
+
344
+ Scope hierarchy: exact match > parent dir > global.
345
+ Results ranked by: FTS relevance x scope_proximity x confidence.
346
+ """
347
+ conn = self._get_conn()
348
+
349
+ # Apply slow confidence decay to all active learnings
350
+ self._apply_decay()
351
+
352
+ # Build scope candidates (exact → parents → global)
353
+ scope_candidates = self._build_scope_hierarchy(scope)
354
+
355
+ results: list[dict] = []
356
+ seen_ids: set[int] = set()
357
+
358
+ # First: FTS-matched results
359
+ if query.strip():
360
+ try:
361
+ cursor = conn.execute(
362
+ """SELECT l.id, l.type, l.description, l.details, l.scope,
363
+ l.confidence, l.apply_count, l.help_count,
364
+ l.unhelpful_count, l.created_at, l.updated_at,
365
+ l.anchor_blob_oid, rank
366
+ FROM learnings_fts f
367
+ JOIN learnings l ON l.id = f.rowid
368
+ WHERE learnings_fts MATCH ?
369
+ AND l.status = 'active'
370
+ ORDER BY rank DESC
371
+ LIMIT ?""",
372
+ (self._escape_fts_query(query), max_results * 3),
373
+ )
374
+ for row in cursor:
375
+ entry = self._row_to_dict(row[:12])
376
+ entry["_rank"] = row[12]
377
+ results.append(entry)
378
+ seen_ids.add(entry["id"])
379
+ except sqlite3.OperationalError:
380
+ pass # FTS syntax error — fall back to scope-only
381
+
382
+ # Second: scope-matched results (for when FTS doesn't match but scope does)
383
+ if len(results) < max_results and scope_candidates:
384
+ placeholders = ",".join("?" * len(scope_candidates))
385
+ cursor = conn.execute(
386
+ f"""SELECT id, type, description, details, scope,
387
+ confidence, apply_count, help_count,
388
+ unhelpful_count, created_at, updated_at,
389
+ anchor_blob_oid
390
+ FROM learnings
391
+ WHERE status = 'active'
392
+ AND scope IN ({placeholders})
393
+ ORDER BY confidence DESC
394
+ LIMIT ?""",
395
+ [*scope_candidates, max_results * 2],
396
+ )
397
+ for row in cursor:
398
+ entry = self._row_to_dict(row)
399
+ if entry["id"] not in seen_ids:
400
+ entry["_rank"] = 0.0 # No FTS rank
401
+ results.append(entry)
402
+ seen_ids.add(entry["id"])
403
+
404
+ # Score and sort
405
+ results = self._score_results(results, scope_candidates)
406
+ return results[:max_results]
407
+
408
+ def list_all(
409
+ self,
410
+ status: str = "active",
411
+ type: str | None = None, # noqa: A002
412
+ ) -> list[dict]:
413
+ """List all learnings, optionally filtered by status and type."""
414
+ conn = self._get_conn()
415
+
416
+ query = (
417
+ "SELECT id, type, description, details, scope, confidence,"
418
+ " apply_count, help_count, unhelpful_count, created_at, updated_at,"
419
+ " anchor_blob_oid"
420
+ " FROM learnings WHERE status = ?"
421
+ )
422
+ params: list[str] = [status]
423
+
424
+ if type:
425
+ query += " AND type = ?"
426
+ params.append(type)
427
+
428
+ query += " ORDER BY confidence DESC, updated_at DESC"
429
+
430
+ cursor = conn.execute(query, params)
431
+ return [self._row_to_dict(row) for row in cursor]
432
+
433
+ # ------------------------------------------------------------------
434
+ # Helpers
435
+ # ------------------------------------------------------------------
436
+
437
+ def _escape_fts_query(self, query: str) -> str:
438
+ """Escape FTS5 special characters by quoting each term."""
439
+ terms = query.split()
440
+ return " ".join(f'"{t}"' for t in terms if t)
441
+
442
+ def _row_to_dict(self, row: tuple) -> dict:
443
+ """Convert a row tuple to a dict.
444
+
445
+ Tolerant of the row length — if the caller's SELECT doesn't
446
+ include ``anchor_blob_oid`` (legacy code paths not yet updated),
447
+ the field defaults to an empty string.
448
+ """
449
+ result = {
450
+ "id": row[0],
451
+ "type": row[1],
452
+ "description": row[2],
453
+ "details": row[3],
454
+ "scope": row[4],
455
+ "confidence": row[5],
456
+ "apply_count": row[6],
457
+ "help_count": row[7],
458
+ "unhelpful_count": row[8],
459
+ "created_at": row[9],
460
+ "updated_at": row[10],
461
+ }
462
+ if len(row) > 11:
463
+ result["anchor_blob_oid"] = row[11]
464
+ else:
465
+ result["anchor_blob_oid"] = ""
466
+ return result
467
+
468
+ def _build_scope_hierarchy(self, scope: str) -> list[str]:
469
+ """Build scope candidates from most specific to global."""
470
+ if not scope:
471
+ return [""]
472
+
473
+ candidates = [scope]
474
+ parts = scope.rstrip("/").split("/")
475
+ for i in range(len(parts) - 1, 0, -1):
476
+ candidates.append("/".join(parts[:i]) + "/")
477
+ candidates.append("") # Global
478
+ return candidates
479
+
480
+ def _score_results(
481
+ self,
482
+ results: list[dict],
483
+ scope_candidates: list[str],
484
+ ) -> list[dict]:
485
+ """Score results by FTS relevance x scope proximity x confidence."""
486
+ scope_weights = {s: 1.0 - (i * 0.15) for i, s in enumerate(scope_candidates)}
487
+
488
+ for r in results:
489
+ fts_score = 1.0 / (1.0 + abs(r.get("_rank", 0.0)))
490
+ scope_weight = scope_weights.get(r["scope"], 0.3)
491
+ r["_score"] = fts_score * scope_weight * r["confidence"]
492
+
493
+ results.sort(key=lambda r: r["_score"], reverse=True)
494
+
495
+ # Clean up internal keys
496
+ for r in results:
497
+ r.pop("_rank", None)
498
+ r.pop("_score", None)
499
+
500
+ return results
501
+
502
+ def _apply_decay(self) -> None:
503
+ """Apply very slow confidence decay to all active learnings.
504
+
505
+ Time-gated to once per hour to prevent excessive decay in
506
+ swarm mode where multiple workers call recall() frequently.
507
+ """
508
+ now = time.time()
509
+ if now - self._last_decay_at < 3600:
510
+ return
511
+ self._last_decay_at = now
512
+
513
+ conn = self._get_conn()
514
+ conn.execute(
515
+ """UPDATE learnings
516
+ SET confidence = MAX(?, confidence - ?)
517
+ WHERE status = 'active'""",
518
+ (_CONFIDENCE_FLOOR, _RECALL_DECAY),
519
+ )
520
+ # Auto-archive any that fell below threshold
521
+ now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
522
+ conn.execute(
523
+ """UPDATE learnings
524
+ SET status = 'archived', updated_at = ?
525
+ WHERE status = 'active' AND confidence < ?""",
526
+ (now, _AUTO_ARCHIVE_THRESHOLD),
527
+ )
528
+ conn.commit()
529
+
530
+ # ------------------------------------------------------------------
531
+ # Deletion
532
+ # ------------------------------------------------------------------
533
+
534
+ def clear_all(self, status_filter: str = "") -> int:
535
+ """Hard-delete learnings.
536
+
537
+ Args:
538
+ status_filter: If non-empty, only delete learnings whose
539
+ ``status`` matches (e.g. ``"archived"``). Empty string
540
+ wipes every row.
541
+
542
+ Returns the count deleted. FTS rows are removed via the existing
543
+ AFTER DELETE trigger on the ``learnings`` table. MemoryStore
544
+ doesn't serialize writes with a lock elsewhere (SQLite's own
545
+ busy handler is the contention path), so we follow the same
546
+ pattern here.
547
+ """
548
+ conn = self._get_conn()
549
+ if status_filter:
550
+ cursor = conn.execute(
551
+ "DELETE FROM learnings WHERE status = ?",
552
+ (status_filter,),
553
+ )
554
+ else:
555
+ cursor = conn.execute("DELETE FROM learnings")
556
+ conn.commit()
557
+ return cursor.rowcount
558
+
559
+ def delete_by_id(self, learning_id: int) -> bool:
560
+ """Hard-delete one learning by id. Returns True on success."""
561
+ conn = self._get_conn()
562
+ cursor = conn.execute(
563
+ "DELETE FROM learnings WHERE id = ?", (learning_id,),
564
+ )
565
+ conn.commit()
566
+ return cursor.rowcount > 0
567
+
568
+ def archive_by_id(self, learning_id: int) -> bool:
569
+ """Soft-delete a learning by setting ``status='archived'``.
570
+
571
+ Preserves the row and bumps ``updated_at`` so consumers can audit
572
+ when the archival happened.
573
+
574
+ Returns True if a row was updated, False if no matching learning
575
+ was found.
576
+ """
577
+ conn = self._get_conn()
578
+ now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
579
+ cursor = conn.execute(
580
+ """UPDATE learnings
581
+ SET status = 'archived', updated_at = ?
582
+ WHERE id = ?""",
583
+ (now, learning_id),
584
+ )
585
+ conn.commit()
586
+ return cursor.rowcount > 0
587
+
588
+ # ------------------------------------------------------------------
589
+ # Lifecycle
590
+ # ------------------------------------------------------------------
591
+
592
+ def __enter__(self) -> MemoryStore:
593
+ return self
594
+
595
+ def __exit__(self, *exc: object) -> None:
596
+ self.close()
597
+
598
+ def close(self) -> None:
599
+ """Close the database connection."""
600
+ if self._conn:
601
+ self._conn.close()
602
+ self._conn = None
@@ -0,0 +1,52 @@
1
+ """Repository-local JavaScript/TypeScript project configuration.
2
+
3
+ No package installation or execution is needed to resolve local path aliases.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+
11
+
12
+ class FileIndex(dict):
13
+ def __init__(self, *args, **kwargs):
14
+ super().__init__(*args, **kwargs)
15
+ self.js_projects = []
16
+
17
+ def configure_javascript(self, root):
18
+ for name in list(self):
19
+ if Path(name).name != "tsconfig.json":
20
+ continue
21
+ try:
22
+ options = json.loads((Path(root) / name).read_text()).get("compilerOptions", {})
23
+ except (OSError, ValueError, AttributeError):
24
+ continue
25
+ if not isinstance(options, dict) or not isinstance(options.get("baseUrl", "."), str):
26
+ continue
27
+ directory = str(Path(name).parent)
28
+ base = os.path.normpath(str(Path(directory) / options.get("baseUrl", ".")))
29
+ paths = options.get("paths", {})
30
+ self.js_projects.append((directory, base, paths if isinstance(paths, dict) else {}))
31
+ self.js_projects.sort(key=lambda item: len(item[0]), reverse=True)
32
+
33
+ def javascript_candidates(self, module, source):
34
+ for directory, base, paths in self.js_projects:
35
+ if directory != "." and not source.startswith(directory + "/"):
36
+ continue
37
+ for pattern, targets in paths.items():
38
+ prefix, wildcard, suffix = pattern.partition("*")
39
+ if wildcard:
40
+ if not module.startswith(prefix) or not module.endswith(suffix):
41
+ continue
42
+ middle = module[len(prefix):len(module) - len(suffix) if suffix else None]
43
+ elif module == pattern:
44
+ middle = ""
45
+ else:
46
+ continue
47
+ if isinstance(targets, list):
48
+ for target in targets:
49
+ if isinstance(target, str):
50
+ yield os.path.normpath(str(Path(base) / target.replace("*", middle)))
51
+ yield os.path.normpath(str(Path(base) / module))
52
+ break