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,423 @@
1
+ """Blob-OID helpers — stable content-addressed identifiers for source files.
2
+
3
+ Every code-intel artifact that used to key on a file path (learnings,
4
+ ADRs, frecency) should really key on *the content of that file* — so a
5
+ rename doesn't orphan it, a vendored copy dedups across projects, and
6
+ git's own object store is the source of truth when available.
7
+
8
+ This module gives the rest of the codebase a uniform interface for
9
+ computing those identifiers, regardless of whether the project is a git
10
+ repo or a plain directory:
11
+
12
+ - ``"git:<sha1>"`` inside a git repo (computed via
13
+ ``git hash-object``, which matches what
14
+ ``git ls-files --stage`` would show)
15
+ - ``"sha256:<hex>"`` for non-git projects or for stdin-typed
16
+ content that has no on-disk path
17
+
18
+ The module also exposes:
19
+
20
+ - :func:`compute_blob_oids_batch` — fast bulk mode using
21
+ ``git hash-object --stdin-paths``
22
+ - :func:`check_reachability` — given a set of git blob OIDs,
23
+ returns the subset that are still reachable from any ref
24
+ - :func:`is_git_repo` — cheap test
25
+ - A small SQLite ``blob_oid_cache`` keyed by ``(path, mtime)`` so
26
+ repeat calls on unchanged files are O(1)
27
+
28
+ All git subprocess calls are best-effort: if git isn't installed or the
29
+ repo is broken, we silently fall back to SHA-256.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import hashlib
35
+ import logging
36
+ import os
37
+ import sqlite3
38
+ import subprocess
39
+ import threading
40
+ from dataclasses import dataclass, field
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Constants
47
+ # ---------------------------------------------------------------------------
48
+
49
+ BLOB_OID_CACHE_DIR = os.path.join(".attocode", "cache")
50
+ BLOB_OID_CACHE_FILE = "blob_oid_cache.db"
51
+ BLOB_OID_SCHEMA_VERSION = 1
52
+
53
+ # Subprocess timeouts (seconds). Git operations are expected to be fast
54
+ # but we don't want a stuck subprocess to freeze the whole MCP server.
55
+ _GIT_TIMEOUT_SECONDS = 10.0
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Cache
60
+ # ---------------------------------------------------------------------------
61
+
62
+
63
+ @dataclass(slots=True)
64
+ class BlobOidCache:
65
+ """SQLite-backed (path, mtime) → blob_oid cache.
66
+
67
+ Path is stored relative to the project directory so the same cache
68
+ file remains valid if the project is moved.
69
+ """
70
+
71
+ project_dir: str
72
+ db_path: str = field(default="", repr=False)
73
+ _conn: sqlite3.Connection | None = field(default=None, repr=False)
74
+ _lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
75
+
76
+ def __post_init__(self) -> None:
77
+ if not self.db_path:
78
+ cache_dir = os.path.join(self.project_dir, BLOB_OID_CACHE_DIR)
79
+ os.makedirs(cache_dir, exist_ok=True)
80
+ self.db_path = os.path.join(cache_dir, BLOB_OID_CACHE_FILE)
81
+ self._conn = sqlite3.connect(self.db_path, check_same_thread=False)
82
+ self._conn.execute("PRAGMA journal_mode=WAL")
83
+ self._create_tables()
84
+
85
+ def _create_tables(self) -> None:
86
+ assert self._conn is not None
87
+ self._conn.executescript(
88
+ """
89
+ CREATE TABLE IF NOT EXISTS meta (
90
+ key TEXT PRIMARY KEY,
91
+ value TEXT NOT NULL
92
+ );
93
+ CREATE TABLE IF NOT EXISTS entries (
94
+ rel_path TEXT PRIMARY KEY,
95
+ mtime REAL NOT NULL,
96
+ size INTEGER NOT NULL,
97
+ blob_oid TEXT NOT NULL,
98
+ updated_at REAL NOT NULL
99
+ );
100
+ CREATE INDEX IF NOT EXISTS ix_entries_oid ON entries(blob_oid);
101
+ """
102
+ )
103
+ self._conn.execute(
104
+ "INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)",
105
+ (str(BLOB_OID_SCHEMA_VERSION),),
106
+ )
107
+ self._conn.commit()
108
+
109
+ def get(self, rel_path: str, mtime: float, size: int) -> str | None:
110
+ assert self._conn is not None
111
+ with self._lock:
112
+ row = self._conn.execute(
113
+ "SELECT blob_oid FROM entries WHERE rel_path = ? AND mtime = ? AND size = ?",
114
+ (rel_path, mtime, size),
115
+ ).fetchone()
116
+ return row[0] if row else None
117
+
118
+ def put(self, rel_path: str, mtime: float, size: int, blob_oid: str) -> None:
119
+ import time as _time
120
+ assert self._conn is not None
121
+ with self._lock:
122
+ self._conn.execute(
123
+ """INSERT OR REPLACE INTO entries
124
+ (rel_path, mtime, size, blob_oid, updated_at)
125
+ VALUES (?, ?, ?, ?, ?)""",
126
+ (rel_path, mtime, size, blob_oid, _time.time()),
127
+ )
128
+ self._conn.commit()
129
+
130
+ def clear(self) -> int:
131
+ assert self._conn is not None
132
+ with self._lock:
133
+ cur = self._conn.execute("DELETE FROM entries")
134
+ self._conn.commit()
135
+ return cur.rowcount
136
+
137
+ def close(self) -> None:
138
+ if self._conn is not None:
139
+ self._conn.close()
140
+ self._conn = None
141
+
142
+
143
+ # ---------------------------------------------------------------------------
144
+ # Git detection
145
+ # ---------------------------------------------------------------------------
146
+
147
+
148
+ def is_git_repo(project_dir: str) -> bool:
149
+ """True if ``project_dir`` (or any parent) is inside a git work-tree.
150
+
151
+ Uses ``git rev-parse --is-inside-work-tree`` rather than checking for
152
+ a literal ``.git`` directory so submodules, worktrees, and ``.git``
153
+ file redirections all resolve correctly.
154
+ """
155
+ try:
156
+ result = subprocess.run(
157
+ ["git", "rev-parse", "--is-inside-work-tree"],
158
+ cwd=project_dir,
159
+ capture_output=True,
160
+ text=True,
161
+ timeout=_GIT_TIMEOUT_SECONDS,
162
+ check=False,
163
+ )
164
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
165
+ return False
166
+ return result.returncode == 0 and result.stdout.strip() == "true"
167
+
168
+
169
+ # ---------------------------------------------------------------------------
170
+ # Single / batch compute
171
+ # ---------------------------------------------------------------------------
172
+
173
+
174
+ def compute_blob_oid(
175
+ path: str,
176
+ project_dir: str,
177
+ *,
178
+ cache: BlobOidCache | None = None,
179
+ ) -> str:
180
+ """Return the blob OID for one file.
181
+
182
+ Args:
183
+ path: Absolute or project-relative path.
184
+ project_dir: Project root. Used to normalize ``path`` for caching
185
+ and as the ``cwd`` for git subprocess calls.
186
+ cache: Optional :class:`BlobOidCache` for memoization. If given,
187
+ hits/misses are consulted and populated.
188
+
189
+ Returns:
190
+ ``"git:<sha1>"`` if ``project_dir`` is a git repo and the file is
191
+ readable; ``"sha256:<hex>"`` otherwise. Returns
192
+ ``"sha256:missing:<path>"`` if the file can't be opened — callers
193
+ can treat that as a sentinel rather than crashing the index walk.
194
+ """
195
+ # Normalize to absolute + relative forms.
196
+ if os.path.isabs(path):
197
+ abs_path = path
198
+ try:
199
+ rel_path = os.path.relpath(path, project_dir)
200
+ except ValueError:
201
+ rel_path = path
202
+ else:
203
+ rel_path = path
204
+ abs_path = os.path.join(project_dir, path)
205
+
206
+ # Missing file → sentinel.
207
+ try:
208
+ stat = os.stat(abs_path)
209
+ except OSError:
210
+ return f"sha256:missing:{rel_path}"
211
+
212
+ mtime = stat.st_mtime
213
+ size = stat.st_size
214
+
215
+ # Cache hit?
216
+ if cache is not None:
217
+ cached = cache.get(rel_path, mtime, size)
218
+ if cached is not None:
219
+ return cached
220
+
221
+ oid = _compute_blob_oid_uncached(abs_path, project_dir)
222
+
223
+ if cache is not None:
224
+ cache.put(rel_path, mtime, size, oid)
225
+ return oid
226
+
227
+
228
+ def _compute_blob_oid_uncached(abs_path: str, project_dir: str) -> str:
229
+ """Single-file blob OID without cache consultation."""
230
+ if is_git_repo(project_dir):
231
+ try:
232
+ result = subprocess.run(
233
+ ["git", "hash-object", "--", abs_path],
234
+ cwd=project_dir,
235
+ capture_output=True,
236
+ text=True,
237
+ timeout=_GIT_TIMEOUT_SECONDS,
238
+ check=False,
239
+ )
240
+ if result.returncode == 0:
241
+ sha = result.stdout.strip()
242
+ if sha:
243
+ return f"git:{sha}"
244
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError) as exc:
245
+ logger.debug("git hash-object failed for %s: %s", abs_path, exc)
246
+
247
+ # Fallback — SHA-256 the raw bytes.
248
+ try:
249
+ with open(abs_path, "rb") as f:
250
+ h = hashlib.sha256()
251
+ for chunk in iter(lambda: f.read(65536), b""):
252
+ h.update(chunk)
253
+ return f"sha256:{h.hexdigest()}"
254
+ except OSError:
255
+ return "sha256:unreadable"
256
+
257
+
258
+ def compute_blob_oids_batch(
259
+ paths: list[str],
260
+ project_dir: str,
261
+ *,
262
+ cache: BlobOidCache | None = None,
263
+ ) -> dict[str, str]:
264
+ """Compute blob OIDs for many files in one shot.
265
+
266
+ If ``project_dir`` is a git repo, dispatches a single
267
+ ``git hash-object --stdin-paths`` subprocess call — ~100x faster than
268
+ calling :func:`compute_blob_oid` in a loop. Cache hits are resolved
269
+ locally before the subprocess so unchanged files don't cost anything.
270
+
271
+ Returns a mapping from the input path (as given, not necessarily
272
+ canonicalized) to the computed OID.
273
+ """
274
+ results: dict[str, str] = {}
275
+ uncached: list[tuple[str, str, float, int]] = [] # (input_path, abs, mtime, size)
276
+
277
+ for path in paths:
278
+ if os.path.isabs(path):
279
+ abs_path = path
280
+ try:
281
+ rel_path = os.path.relpath(path, project_dir)
282
+ except ValueError:
283
+ rel_path = path
284
+ else:
285
+ rel_path = path
286
+ abs_path = os.path.join(project_dir, path)
287
+ try:
288
+ stat = os.stat(abs_path)
289
+ except OSError:
290
+ results[path] = f"sha256:missing:{rel_path}"
291
+ continue
292
+ if cache is not None:
293
+ cached = cache.get(rel_path, stat.st_mtime, stat.st_size)
294
+ if cached is not None:
295
+ results[path] = cached
296
+ continue
297
+ uncached.append((path, abs_path, stat.st_mtime, stat.st_size))
298
+
299
+ if not uncached:
300
+ return results
301
+
302
+ # Git batch mode if available.
303
+ if is_git_repo(project_dir):
304
+ try:
305
+ stdin_text = "\n".join(abs_p for _, abs_p, _, _ in uncached) + "\n"
306
+ result = subprocess.run(
307
+ ["git", "hash-object", "--stdin-paths"],
308
+ cwd=project_dir,
309
+ input=stdin_text,
310
+ capture_output=True,
311
+ text=True,
312
+ timeout=max(_GIT_TIMEOUT_SECONDS, len(uncached) * 0.1),
313
+ check=False,
314
+ )
315
+ if result.returncode == 0:
316
+ out_lines = result.stdout.splitlines()
317
+ if len(out_lines) == len(uncached):
318
+ for (input_path, abs_path, mtime, size), sha in zip(
319
+ uncached, out_lines, strict=True,
320
+ ):
321
+ oid = f"git:{sha.strip()}"
322
+ results[input_path] = oid
323
+ if cache is not None:
324
+ try:
325
+ rel = os.path.relpath(abs_path, project_dir)
326
+ except ValueError:
327
+ rel = input_path
328
+ cache.put(rel, mtime, size, oid)
329
+ return results
330
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError) as exc:
331
+ logger.debug("git hash-object --stdin-paths failed: %s", exc)
332
+
333
+ # Non-git fallback — compute sha256 per file.
334
+ for input_path, abs_path, mtime, size in uncached:
335
+ try:
336
+ with open(abs_path, "rb") as f:
337
+ h = hashlib.sha256()
338
+ for chunk in iter(lambda: f.read(65536), b""):
339
+ h.update(chunk)
340
+ oid = f"sha256:{h.hexdigest()}"
341
+ except OSError:
342
+ oid = "sha256:unreadable"
343
+ results[input_path] = oid
344
+ if cache is not None:
345
+ try:
346
+ rel = os.path.relpath(abs_path, project_dir)
347
+ except ValueError:
348
+ rel = input_path
349
+ cache.put(rel, mtime, size, oid)
350
+
351
+ return results
352
+
353
+
354
+ # ---------------------------------------------------------------------------
355
+ # Reachability
356
+ # ---------------------------------------------------------------------------
357
+
358
+
359
+ def check_reachability(
360
+ blob_oids: set[str],
361
+ project_dir: str,
362
+ ) -> set[str]:
363
+ """Return the subset of ``blob_oids`` that are still reachable in git.
364
+
365
+ Non-git-prefixed OIDs (``sha256:...``, ``sha256:missing:...``, etc.)
366
+ are passed through as "always reachable" since we can't prove they're
367
+ orphaned without the git object database.
368
+
369
+ Implementation: uses ``git cat-file --batch-check`` which checks all
370
+ objects, not just reachable ones — but for orphan detection we also
371
+ filter out dangling blobs using ``git fsck --unreachable``. In
372
+ practice, for code-intel anchors, "object exists" is a close-enough
373
+ proxy for "reachable" because git's GC eventually prunes unreachable
374
+ objects and the cache_manifest will catch up on the next scan.
375
+
376
+ Empty input returns empty set.
377
+ """
378
+ if not blob_oids:
379
+ return set()
380
+
381
+ # Partition git vs non-git keys.
382
+ git_oids: set[str] = set()
383
+ non_git: set[str] = set()
384
+ for oid in blob_oids:
385
+ if oid.startswith("git:"):
386
+ git_oids.add(oid)
387
+ else:
388
+ non_git.add(oid)
389
+
390
+ if not git_oids or not is_git_repo(project_dir):
391
+ # Nothing to probe in git → pass-through.
392
+ return set(blob_oids)
393
+
394
+ # Prepare stdin: one SHA per line (strip the "git:" prefix).
395
+ stdin_text = "\n".join(oid[4:] for oid in git_oids) + "\n"
396
+ try:
397
+ result = subprocess.run(
398
+ ["git", "cat-file", "--batch-check"],
399
+ cwd=project_dir,
400
+ input=stdin_text,
401
+ capture_output=True,
402
+ text=True,
403
+ timeout=max(_GIT_TIMEOUT_SECONDS, len(git_oids) * 0.01),
404
+ check=False,
405
+ )
406
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError) as exc:
407
+ logger.debug("git cat-file --batch-check failed: %s", exc)
408
+ return set(blob_oids)
409
+
410
+ if result.returncode != 0:
411
+ return set(blob_oids)
412
+
413
+ # Each line is either "<sha> <type> <size>" (exists) or
414
+ # "<sha> missing" (doesn't exist).
415
+ reachable: set[str] = set(non_git)
416
+ for line in result.stdout.splitlines():
417
+ parts = line.split()
418
+ if len(parts) >= 2:
419
+ sha = parts[0]
420
+ status = parts[1]
421
+ if status != "missing":
422
+ reachable.add(f"git:{sha}")
423
+ return reachable
@@ -0,0 +1,221 @@
1
+ """Per-project cache manifest — central schema-version registry for .attocode/.
2
+
3
+ A single ``.attocode/cache_manifest.json`` records the current schema version
4
+ of every local store (symbols, embeddings, kw_index, trigrams, learnings,
5
+ ADRs, frecency, query_history, CAS). Stores consult the manifest on open
6
+ and trigger their own migration hooks if they're behind.
7
+
8
+ This replaces the status-quo "half the DBs have a schema_version row, half
9
+ don't" by giving them one place to declare what version they're at and one
10
+ way to ask "do I need to migrate?".
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import logging
17
+ import os
18
+ import tempfile
19
+ import time
20
+ from dataclasses import asdict, dataclass, field
21
+ from typing import TYPE_CHECKING, Any
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Mapping
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ MANIFEST_SCHEMA_VERSION = 1
29
+
30
+
31
+ @dataclass(slots=True)
32
+ class StoreEntry:
33
+ """One row in the cache manifest, describing one store."""
34
+
35
+ path: str
36
+ schema_version: int
37
+ last_migrated_at: str = ""
38
+ extra: dict[str, Any] = field(default_factory=dict)
39
+
40
+
41
+ @dataclass(slots=True)
42
+ class CacheManifest:
43
+ """Read-write wrapper around ``<project>/.attocode/cache_manifest.json``.
44
+
45
+ Usage::
46
+
47
+ m = CacheManifest.load(project_dir="/path/to/project")
48
+ if m.needs_migration("embeddings", target=2):
49
+ ... # run your store's migration hook
50
+ m.bump("embeddings", new_schema_version=2)
51
+ m.save()
52
+ """
53
+
54
+ project_dir: str
55
+ manifest_version: int = MANIFEST_SCHEMA_VERSION
56
+ stores: dict[str, StoreEntry] = field(default_factory=dict)
57
+ cas_root: str = ""
58
+ active_overlay: str = "main"
59
+ last_full_verify_at: str = ""
60
+
61
+ # ------------------------------------------------------------------
62
+ # Paths
63
+ # ------------------------------------------------------------------
64
+
65
+ @property
66
+ def manifest_path(self) -> str:
67
+ return os.path.join(self.project_dir, ".attocode", "cache_manifest.json")
68
+
69
+ # ------------------------------------------------------------------
70
+ # Load / Save
71
+ # ------------------------------------------------------------------
72
+
73
+ @classmethod
74
+ def load(cls, project_dir: str) -> CacheManifest:
75
+ """Load the manifest, or return a fresh empty one if missing.
76
+
77
+ Never raises on a missing file — callers should treat "missing" and
78
+ "present but empty" as identical, because a brand-new ``.attocode/``
79
+ directory is indistinguishable from "we forgot to write the manifest".
80
+ """
81
+ path = os.path.join(project_dir, ".attocode", "cache_manifest.json")
82
+ if not os.path.exists(path):
83
+ return cls(project_dir=project_dir)
84
+ try:
85
+ with open(path, "rb") as f:
86
+ raw = json.loads(f.read())
87
+ except (OSError, json.JSONDecodeError) as exc:
88
+ logger.warning(
89
+ "cache_manifest: failed to parse %s (%s); starting fresh",
90
+ path, exc,
91
+ )
92
+ return cls(project_dir=project_dir)
93
+
94
+ stores_raw = raw.get("stores", {})
95
+ stores = {
96
+ name: StoreEntry(
97
+ path=str(entry.get("path", "")),
98
+ schema_version=int(entry.get("schema_version", 0)),
99
+ last_migrated_at=str(entry.get("last_migrated_at", "")),
100
+ extra={k: v for k, v in entry.items()
101
+ if k not in {"path", "schema_version", "last_migrated_at"}},
102
+ )
103
+ for name, entry in stores_raw.items()
104
+ }
105
+ return cls(
106
+ project_dir=project_dir,
107
+ manifest_version=int(raw.get("manifest_version", MANIFEST_SCHEMA_VERSION)),
108
+ stores=stores,
109
+ cas_root=str(raw.get("cas_root", "")),
110
+ active_overlay=str(raw.get("active_overlay", "main")),
111
+ last_full_verify_at=str(raw.get("last_full_verify_at", "")),
112
+ )
113
+
114
+ def save(self) -> None:
115
+ """Atomically write the manifest to disk via tempfile + rename."""
116
+ os.makedirs(os.path.dirname(self.manifest_path), exist_ok=True)
117
+ payload = self.to_dict()
118
+ body = json.dumps(payload, indent=2, sort_keys=True).encode("utf-8")
119
+ fd, tmp = tempfile.mkstemp(
120
+ prefix="cache_manifest_",
121
+ suffix=".json.tmp",
122
+ dir=os.path.dirname(self.manifest_path),
123
+ )
124
+ try:
125
+ with os.fdopen(fd, "wb") as f:
126
+ f.write(body)
127
+ os.replace(tmp, self.manifest_path)
128
+ except Exception:
129
+ try:
130
+ os.unlink(tmp)
131
+ except OSError:
132
+ pass
133
+ raise
134
+
135
+ def to_dict(self) -> dict[str, Any]:
136
+ return {
137
+ "manifest_version": self.manifest_version,
138
+ "project_dir": self.project_dir,
139
+ "cas_root": self.cas_root,
140
+ "active_overlay": self.active_overlay,
141
+ "last_full_verify_at": self.last_full_verify_at,
142
+ "stores": {
143
+ name: {
144
+ "path": entry.path,
145
+ "schema_version": entry.schema_version,
146
+ "last_migrated_at": entry.last_migrated_at,
147
+ **entry.extra,
148
+ }
149
+ for name, entry in sorted(self.stores.items())
150
+ },
151
+ }
152
+
153
+ # ------------------------------------------------------------------
154
+ # Store registration + migration hooks
155
+ # ------------------------------------------------------------------
156
+
157
+ def register(
158
+ self,
159
+ name: str,
160
+ *,
161
+ path: str,
162
+ schema_version: int,
163
+ extra: Mapping[str, Any] | None = None,
164
+ ) -> StoreEntry:
165
+ """Idempotently register a store.
166
+
167
+ If the store is already present, update its recorded path/version
168
+ but do NOT reset last_migrated_at (that's a migration event).
169
+ """
170
+ existing = self.stores.get(name)
171
+ if existing is None:
172
+ entry = StoreEntry(
173
+ path=path,
174
+ schema_version=schema_version,
175
+ last_migrated_at="",
176
+ extra=dict(extra or {}),
177
+ )
178
+ else:
179
+ existing.path = path
180
+ existing.schema_version = schema_version
181
+ if extra:
182
+ existing.extra.update(extra)
183
+ entry = existing
184
+ self.stores[name] = entry
185
+ return entry
186
+
187
+ def get_store(self, name: str) -> StoreEntry | None:
188
+ return self.stores.get(name)
189
+
190
+ def needs_migration(self, name: str, *, target: int) -> bool:
191
+ """Return True if the named store is missing OR below ``target`` schema."""
192
+ entry = self.stores.get(name)
193
+ if entry is None:
194
+ return True
195
+ return entry.schema_version < target
196
+
197
+ def bump(self, name: str, *, new_schema_version: int) -> None:
198
+ """Record that a store migrated to ``new_schema_version``."""
199
+ entry = self.stores.get(name)
200
+ if entry is None:
201
+ raise KeyError(f"store {name!r} not registered; call register() first")
202
+ entry.schema_version = new_schema_version
203
+ entry.last_migrated_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
204
+
205
+ # ------------------------------------------------------------------
206
+ # Summary helpers
207
+ # ------------------------------------------------------------------
208
+
209
+ def summary(self) -> dict[str, Any]:
210
+ """Return a compact dict useful for `cache_status_all`."""
211
+ return {
212
+ "manifest_version": self.manifest_version,
213
+ "project_dir": self.project_dir,
214
+ "cas_root": self.cas_root,
215
+ "active_overlay": self.active_overlay,
216
+ "last_full_verify_at": self.last_full_verify_at,
217
+ "stores": {
218
+ name: asdict(entry)
219
+ for name, entry in sorted(self.stores.items())
220
+ },
221
+ }