marvisx-cli 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 (587) hide show
  1. core/api/__init__.py +0 -0
  2. core/api/agents/__init__.py +0 -0
  3. core/api/agents/session_health.py +59 -0
  4. core/api/agents/session_manager.py +206 -0
  5. core/api/bin/marvisx-state-hook.py +182 -0
  6. core/api/config.py +533 -0
  7. core/api/db.py +1516 -0
  8. core/api/dependencies/__init__.py +0 -0
  9. core/api/dependencies/tenant.py +34 -0
  10. core/api/main.py +1641 -0
  11. core/api/mcp/__init__.py +8 -0
  12. core/api/mcp/_adapter.py +184 -0
  13. core/api/mcp/server.py +58 -0
  14. core/api/mcp/tools/__init__.py +59 -0
  15. core/api/mcp/tools/brain.py +599 -0
  16. core/api/mcp/tools/graph.py +380 -0
  17. core/api/mcp/tools/handoffs.py +112 -0
  18. core/api/mcp/tools/ingest.py +326 -0
  19. core/api/mcp/tools/learnings.py +144 -0
  20. core/api/mcp/tools/projects.py +99 -0
  21. core/api/mcp/tools/pull_requests.py +173 -0
  22. core/api/mcp/tools/safety.py +111 -0
  23. core/api/mcp/tools/search.py +79 -0
  24. core/api/mcp/tools/tasks.py +258 -0
  25. core/api/middleware/__init__.py +0 -0
  26. core/api/middleware/tool_call_audit.py +111 -0
  27. core/api/models/__init__.py +346 -0
  28. core/api/models/auth.py +48 -0
  29. core/api/models/brain.py +1006 -0
  30. core/api/models/common.py +76 -0
  31. core/api/models/costs.py +91 -0
  32. core/api/models/graph.py +66 -0
  33. core/api/models/graph_cosmo.py +125 -0
  34. core/api/models/graph_pr_impact.py +257 -0
  35. core/api/models/graph_ux.py +141 -0
  36. core/api/models/inbox.py +230 -0
  37. core/api/models/ingest_keys.py +108 -0
  38. core/api/models/kg.py +41 -0
  39. core/api/models/llm_config.py +56 -0
  40. core/api/models/monitoring.py +234 -0
  41. core/api/models/projects.py +161 -0
  42. core/api/models/search.py +42 -0
  43. core/api/models/sessions.py +322 -0
  44. core/api/models/tasks.py +184 -0
  45. core/api/models/teams.py +63 -0
  46. core/api/models/users.py +184 -0
  47. core/api/observability/__init__.py +0 -0
  48. core/api/observability/tracing.py +92 -0
  49. core/api/paths.py +26 -0
  50. core/api/rate_limit.py +24 -0
  51. core/api/rbac.py +112 -0
  52. core/api/routers/__init__.py +0 -0
  53. core/api/routers/_adapter.py +24 -0
  54. core/api/routers/admin_pr_impact.py +230 -0
  55. core/api/routers/admin_settings.py +147 -0
  56. core/api/routers/agent.py +1079 -0
  57. core/api/routers/agent_tokens.py +276 -0
  58. core/api/routers/app_settings.py +112 -0
  59. core/api/routers/audit.py +89 -0
  60. core/api/routers/auth.py +586 -0
  61. core/api/routers/bench.py +161 -0
  62. core/api/routers/brain.py +881 -0
  63. core/api/routers/brain_directions.py +527 -0
  64. core/api/routers/ci_checks.py +140 -0
  65. core/api/routers/comments.py +273 -0
  66. core/api/routers/costs.py +148 -0
  67. core/api/routers/docs_coverage.py +217 -0
  68. core/api/routers/docs_governance.py +63 -0
  69. core/api/routers/documents.py +318 -0
  70. core/api/routers/files.py +163 -0
  71. core/api/routers/finder.py +987 -0
  72. core/api/routers/graph.py +836 -0
  73. core/api/routers/handoffs.py +156 -0
  74. core/api/routers/inbox.py +496 -0
  75. core/api/routers/ingest_api_keys.py +205 -0
  76. core/api/routers/ingest_triage.py +1227 -0
  77. core/api/routers/judge.py +306 -0
  78. core/api/routers/kg.py +336 -0
  79. core/api/routers/learnings.py +253 -0
  80. core/api/routers/llm_config.py +130 -0
  81. core/api/routers/monitoring.py +347 -0
  82. core/api/routers/notifications.py +125 -0
  83. core/api/routers/pr_impact.py +315 -0
  84. core/api/routers/projects.py +1061 -0
  85. core/api/routers/pull_requests.py +312 -0
  86. core/api/routers/push.py +67 -0
  87. core/api/routers/raci.py +228 -0
  88. core/api/routers/search.py +125 -0
  89. core/api/routers/sessions.py +3100 -0
  90. core/api/routers/settings.py +90 -0
  91. core/api/routers/share_repo.py +68 -0
  92. core/api/routers/status_updates.py +96 -0
  93. core/api/routers/tags.py +45 -0
  94. core/api/routers/tasks.py +526 -0
  95. core/api/routers/teams.py +425 -0
  96. core/api/routers/terminal.py +105 -0
  97. core/api/routers/users.py +331 -0
  98. core/api/routers/webhooks.py +330 -0
  99. core/api/runtime_settings.py +84 -0
  100. core/api/security.py +652 -0
  101. core/api/services/__init__.py +0 -0
  102. core/api/services/audit.py +58 -0
  103. core/api/services/auto_approval.py +82 -0
  104. core/api/services/brain/__init__.py +52 -0
  105. core/api/services/brain/baseline.py +230 -0
  106. core/api/services/brain/capabilities.py +75 -0
  107. core/api/services/brain/cascade_rollup.py +388 -0
  108. core/api/services/brain/compound_bridge.py +215 -0
  109. core/api/services/brain/cycle.py +1242 -0
  110. core/api/services/brain/cycle_snapshot.py +371 -0
  111. core/api/services/brain/digest_collector.py +147 -0
  112. core/api/services/brain/direction.py +421 -0
  113. core/api/services/brain/drift.py +356 -0
  114. core/api/services/brain/drift_router.py +409 -0
  115. core/api/services/brain/edge_metrics.py +79 -0
  116. core/api/services/brain/events_reader.py +222 -0
  117. core/api/services/brain/findings.py +1379 -0
  118. core/api/services/brain/findings_reader.py +1006 -0
  119. core/api/services/brain/jobs.py +733 -0
  120. core/api/services/brain/journal.py +206 -0
  121. core/api/services/brain/knowledge_forms.py +92 -0
  122. core/api/services/brain/llm/__init__.py +37 -0
  123. core/api/services/brain/llm/_runner.py +62 -0
  124. core/api/services/brain/llm/base.py +70 -0
  125. core/api/services/brain/llm/cache.py +99 -0
  126. core/api/services/brain/llm/constants.py +46 -0
  127. core/api/services/brain/llm/direction_alignment.py +289 -0
  128. core/api/services/brain/llm/factory.py +132 -0
  129. core/api/services/brain/llm/finding_reasoning.py +98 -0
  130. core/api/services/brain/llm/finding_summary.py +92 -0
  131. core/api/services/brain/llm/grounding.py +46 -0
  132. core/api/services/brain/llm/journal_polish.py +96 -0
  133. core/api/services/brain/llm/local_gateway.py +426 -0
  134. core/api/services/brain/llm/parsers.py +71 -0
  135. core/api/services/brain/llm/router_glue.py +422 -0
  136. core/api/services/brain/memory_ops.py +1677 -0
  137. core/api/services/brain/models.py +140 -0
  138. core/api/services/brain/owner_hint.py +211 -0
  139. core/api/services/brain/recap.py +307 -0
  140. core/api/services/brain/rules/__init__.py +65 -0
  141. core/api/services/brain/rules/_signals.py +205 -0
  142. core/api/services/brain/rules/dr1_activity_without_status.py +108 -0
  143. core/api/services/brain/rules/dr2_decision_without_adr.py +110 -0
  144. core/api/services/brain/rules/dr3_stale_open_loop.py +127 -0
  145. core/api/services/brain/rules/dr4_docs_governance_drift.py +79 -0
  146. core/api/services/brain/rules/dr5_playbook_changed.py +99 -0
  147. core/api/services/brain/rules/dr6_external_update_unpropagated.py +100 -0
  148. core/api/services/brain/rules/dr7_claimed_decision_gap.py +123 -0
  149. core/api/services/brain/rules/dr8_direction_misalignment.py +230 -0
  150. core/api/services/brain/runs_reader.py +485 -0
  151. core/api/services/brain/scope.py +79 -0
  152. core/api/services/brain/sources/__init__.py +46 -0
  153. core/api/services/brain/sources/base.py +86 -0
  154. core/api/services/brain/sources/git_kg.py +393 -0
  155. core/api/services/brain/sources/handoffs.py +157 -0
  156. core/api/services/brain/sources/ingestor.py +130 -0
  157. core/api/services/brain/sources/learnings.py +121 -0
  158. core/api/services/brain/sources/pir_tasks.py +245 -0
  159. core/api/services/brain/watermarks.py +147 -0
  160. core/api/services/brain/ws_emitter.py +170 -0
  161. core/api/services/cc_tasks_reader.py +76 -0
  162. core/api/services/ci_service.py +263 -0
  163. core/api/services/claude_metrics.py +796 -0
  164. core/api/services/codex_metrics.py +364 -0
  165. core/api/services/conversation_reader.py +102 -0
  166. core/api/services/cost_service.py +243 -0
  167. core/api/services/crypto.py +147 -0
  168. core/api/services/docs_governance/__init__.py +1 -0
  169. core/api/services/docs_governance/confidence.py +230 -0
  170. core/api/services/docs_governance/config.py +87 -0
  171. core/api/services/docs_governance/enrichment.py +65 -0
  172. core/api/services/docs_governance/frontmatter_validator.py +83 -0
  173. core/api/services/docs_governance/hard_gates.py +221 -0
  174. core/api/services/docs_governance/triage_orchestrator.py +98 -0
  175. core/api/services/embedding_internal.py +395 -0
  176. core/api/services/embedding_service.py +832 -0
  177. core/api/services/event_dispatcher.py +167 -0
  178. core/api/services/events.py +70 -0
  179. core/api/services/git_ops.py +621 -0
  180. core/api/services/graph_cosmo_service.py +440 -0
  181. core/api/services/graph_ranker.py +306 -0
  182. core/api/services/graph_service.py +1589 -0
  183. core/api/services/inbox.py +800 -0
  184. core/api/services/inbox_digest.py +221 -0
  185. core/api/services/inbox_digest_deep_research.py +80 -0
  186. core/api/services/inbox_digest_jobs.py +595 -0
  187. core/api/services/inbox_gmail_sync.py +167 -0
  188. core/api/services/inbox_llm_classifier.py +906 -0
  189. core/api/services/inbox_source_identity.py +116 -0
  190. core/api/services/inbox_sources.py +456 -0
  191. core/api/services/inbox_taxonomy.py +195 -0
  192. core/api/services/inbox_tldr.py +1079 -0
  193. core/api/services/inbox_triage.py +899 -0
  194. core/api/services/ingest/__init__.py +13 -0
  195. core/api/services/ingest/api_key_auth.py +136 -0
  196. core/api/services/ingest/auto_approve.py +120 -0
  197. core/api/services/ingest/classifier.py +138 -0
  198. core/api/services/ingest/confidence.py +173 -0
  199. core/api/services/ingest/dispatch.py +88 -0
  200. core/api/services/ingest/embedding_router.py +272 -0
  201. core/api/services/ingest/events.py +33 -0
  202. core/api/services/ingest/ignore_patterns.py +79 -0
  203. core/api/services/ingest/image_probe.py +218 -0
  204. core/api/services/ingest/ingress.py +263 -0
  205. core/api/services/ingest/insert_saga.py +793 -0
  206. core/api/services/ingest/llm/__init__.py +13 -0
  207. core/api/services/ingest/llm/anthropic_haiku.py +23 -0
  208. core/api/services/ingest/llm/base.py +59 -0
  209. core/api/services/ingest/llm/byok_provider.py +130 -0
  210. core/api/services/ingest/llm/classification_context.py +301 -0
  211. core/api/services/ingest/llm/config_store.py +246 -0
  212. core/api/services/ingest/llm/factory.py +24 -0
  213. core/api/services/ingest/llm/kg_enricher.py +306 -0
  214. core/api/services/ingest/llm/local_gateway.py +821 -0
  215. core/api/services/ingest/llm/local_vllm.py +23 -0
  216. core/api/services/ingest/llm/openai_nano.py +349 -0
  217. core/api/services/ingest/lock_advisory.py +57 -0
  218. core/api/services/ingest/parser_router.py +1756 -0
  219. core/api/services/ingest/parsers/__init__.py +1 -0
  220. core/api/services/ingest/parsers/docling_parser.py +142 -0
  221. core/api/services/ingest/parsers/docparse_gateway.py +178 -0
  222. core/api/services/ingest/parsers/docx_parser.py +127 -0
  223. core/api/services/ingest/parsers/folder_unpacker.py +85 -0
  224. core/api/services/ingest/parsers/gateway_aux.py +147 -0
  225. core/api/services/ingest/parsers/image_parser.py +251 -0
  226. core/api/services/ingest/parsers/internal_markdown.py +89 -0
  227. core/api/services/ingest/parsers/ocr_gateway.py +117 -0
  228. core/api/services/ingest/parsers/ocr_pdf_parser.py +112 -0
  229. core/api/services/ingest/parsers/pdf_types.py +13 -0
  230. core/api/services/ingest/parsers/transcript_parser.py +445 -0
  231. core/api/services/ingest/parsers/vision_gateway.py +186 -0
  232. core/api/services/ingest/parsers/xlsx_parser.py +91 -0
  233. core/api/services/ingest/parsers/zip_unpacker.py +126 -0
  234. core/api/services/ingest/preflight.py +393 -0
  235. core/api/services/ingest/retry_voyage.py +88 -0
  236. core/api/services/ingest/routing_policy.py +307 -0
  237. core/api/services/ingest/serializers/__init__.py +1 -0
  238. core/api/services/ingest/serializers/xlsx_to_markdown.py +80 -0
  239. core/api/services/ingest/skip_log.py +74 -0
  240. core/api/services/ingest/watcher.py +637 -0
  241. core/api/services/kg/__init__.py +0 -0
  242. core/api/services/kg/audit.py +49 -0
  243. core/api/services/kg/hybrid_search.py +691 -0
  244. core/api/services/kg/lens.py +339 -0
  245. core/api/services/kg/pr_impact.py +770 -0
  246. core/api/services/kg/queries.py +152 -0
  247. core/api/services/kg/ranking.py +89 -0
  248. core/api/services/kg/rrf.py +143 -0
  249. core/api/services/kg_watcher_control.py +161 -0
  250. core/api/services/local_llm/__init__.py +19 -0
  251. core/api/services/local_llm/async_client.py +385 -0
  252. core/api/services/local_llm/client.py +173 -0
  253. core/api/services/local_llm/url_validator.py +44 -0
  254. core/api/services/metrics_collector.py +646 -0
  255. core/api/services/metrics_providers.py +65 -0
  256. core/api/services/model_registry.py +266 -0
  257. core/api/services/model_router.py +137 -0
  258. core/api/services/n8n_client.py +77 -0
  259. core/api/services/newsletter_llm_gateway.py +66 -0
  260. core/api/services/notification_service.py +134 -0
  261. core/api/services/openai_responses.py +55 -0
  262. core/api/services/opencode_metrics.py +375 -0
  263. core/api/services/opencode_sessions.py +173 -0
  264. core/api/services/pii_redactor.py +138 -0
  265. core/api/services/pr_impact_pipeline/__init__.py +21 -0
  266. core/api/services/pr_impact_pipeline/differ.py +421 -0
  267. core/api/services/pr_impact_pipeline/dispatcher.py +415 -0
  268. core/api/services/pr_impact_pipeline/gc.py +93 -0
  269. core/api/services/pr_impact_pipeline/languages.py +192 -0
  270. core/api/services/pr_impact_pipeline/parser.py +178 -0
  271. core/api/services/pr_impact_pipeline/writer.py +394 -0
  272. core/api/services/pr_service.py +1393 -0
  273. core/api/services/project_paths.py +70 -0
  274. core/api/services/project_status_updates.py +265 -0
  275. core/api/services/providers.py +276 -0
  276. core/api/services/push_service.py +170 -0
  277. core/api/services/reminder_service.py +89 -0
  278. core/api/services/runas.py +41 -0
  279. core/api/services/salience_service.py +69 -0
  280. core/api/services/security_collector.py +281 -0
  281. core/api/services/session_catalog.py +385 -0
  282. core/api/services/session_metrics_service.py +301 -0
  283. core/api/services/session_ops.py +272 -0
  284. core/api/services/session_state.py +173 -0
  285. core/api/services/share_links.py +222 -0
  286. core/api/services/task_transitions.py +146 -0
  287. core/api/services/terminal_metrics.py +462 -0
  288. core/api/services/terminal_metrics_dump.py +203 -0
  289. core/api/services/tmux.py +1205 -0
  290. core/api/services/webhook_service.py +422 -0
  291. core/api/services/workspace_sync.py +164 -0
  292. core/api/templates/__init__.py +1 -0
  293. core/api/templates/markdown_share.py +164 -0
  294. core/api/terminal.py +1031 -0
  295. core/api/tests/__init__.py +0 -0
  296. core/api/tests/test_agent_facing_auth_dependencies.py +132 -0
  297. core/api/tests/test_audit_permissions.py +133 -0
  298. core/api/tests/test_backfill_session_conversations.py +90 -0
  299. core/api/tests/test_backfill_working_seconds_msg.py +129 -0
  300. core/api/tests/test_claude_metrics.py +326 -0
  301. core/api/tests/test_codex_metrics.py +189 -0
  302. core/api/tests/test_finder_paths.py +74 -0
  303. core/api/tests/test_git_ops_merge.py +155 -0
  304. core/api/tests/test_learnings_check_search.py +81 -0
  305. core/api/tests/test_metrics_providers.py +133 -0
  306. core/api/tests/test_migration_087.py +164 -0
  307. core/api/tests/test_migration_088.py +94 -0
  308. core/api/tests/test_migration_089.py +116 -0
  309. core/api/tests/test_openai_responses.py +24 -0
  310. core/api/tests/test_opencode_metrics.py +740 -0
  311. core/api/tests/test_opencode_sessions.py +321 -0
  312. core/api/tests/test_pr_workflow_e2e.py +457 -0
  313. core/api/tests/test_projects_handoffs.py +31 -0
  314. core/api/tests/test_providers.py +138 -0
  315. core/api/tests/test_safety_bridge.py +347 -0
  316. core/api/tests/test_session_catalog.py +142 -0
  317. core/api/tests/test_session_conversations.py +512 -0
  318. core/api/tests/test_session_metrics_service.py +270 -0
  319. core/api/tests/test_session_resume_paths.py +548 -0
  320. core/api/tests/test_session_theme_mode_migration.py +56 -0
  321. core/api/tests/test_sessions_rbac.py +131 -0
  322. core/api/tests/test_share_edit.py +398 -0
  323. core/api/tests/test_share_repo.py +200 -0
  324. core/api/tests/test_terminal_session_manager.py +98 -0
  325. core/api/tests/test_terminal_upload.py +34 -0
  326. core/api/tests/test_tmux.py +272 -0
  327. core/api/tests/test_workspace_sync.py +186 -0
  328. core/api/tests/test_ws_ticket_in_memory.py +73 -0
  329. core/api/use_cases/__init__.py +11 -0
  330. core/api/use_cases/_context.py +89 -0
  331. core/api/use_cases/_errors.py +62 -0
  332. core/api/use_cases/_roles.py +16 -0
  333. core/api/use_cases/audit.py +171 -0
  334. core/api/use_cases/brain.py +1232 -0
  335. core/api/use_cases/costs.py +249 -0
  336. core/api/use_cases/graph.py +1153 -0
  337. core/api/use_cases/handoffs.py +506 -0
  338. core/api/use_cases/ingest_triage.py +1229 -0
  339. core/api/use_cases/learnings.py +538 -0
  340. core/api/use_cases/projects.py +705 -0
  341. core/api/use_cases/pull_requests.py +415 -0
  342. core/api/use_cases/search.py +926 -0
  343. core/api/use_cases/tasks.py +1495 -0
  344. core/api/visibility.py +141 -0
  345. core/cli/__init__.py +5 -0
  346. core/cli/_index_source.py +632 -0
  347. core/cli/_runtime_ctx.py +160 -0
  348. core/cli/_transmute.py +241 -0
  349. core/cli/marvis_doctor.py +704 -0
  350. core/cli/marvis_feedback.py +396 -0
  351. core/cli/marvis_governance.py +315 -0
  352. core/cli/marvis_hooks.py +515 -0
  353. core/cli/marvis_init.py +757 -0
  354. core/cli/marvis_mcp.py +401 -0
  355. core/cli/marvis_runtime.py +855 -0
  356. core/cli/marvis_telemetry.py +228 -0
  357. core/scripts/_drift_check.py +716 -0
  358. core/scripts/_frontmatter.py +66 -0
  359. core/scripts/_graph_writer.py +189 -0
  360. core/scripts/ast_parser.py +1553 -0
  361. core/scripts/install_hooks/__init__.py +1 -0
  362. core/scripts/install_hooks/_config.sh +109 -0
  363. core/scripts/install_hooks/block-dangerous-bash.sh +23 -0
  364. core/scripts/install_hooks/block-db-direct-write.sh +23 -0
  365. core/scripts/install_hooks/block-push-no-task.sh +23 -0
  366. core/scripts/install_hooks/block-staging-to-prod.sh +23 -0
  367. core/scripts/install_hooks/block-subtree-push.sh +23 -0
  368. core/scripts/install_hooks/config.json +53 -0
  369. core/scripts/install_hooks/enforce-no-merge-main.sh +23 -0
  370. core/scripts/install_hooks/enforce-worktree.sh +23 -0
  371. core/scripts/install_hooks/quality-gate.sh +170 -0
  372. core/scripts/install_hooks/safety_bridge.py +968 -0
  373. core/scripts/install_hooks/secret-scan.sh +23 -0
  374. core/scripts/migrate_spike_node_ids.py +122 -0
  375. core/scripts/populate_artifacts.py +2198 -0
  376. core/scripts/populate_cross_project.py +2457 -0
  377. core/scripts/populate_inbox_nodes.py +357 -0
  378. core/scripts/populate_pr_impact.py +267 -0
  379. core/scripts/populate_project_nodes.py +603 -0
  380. core/scripts/populate_touch_counter.py +337 -0
  381. core/scripts/reparse_failed.py +57 -0
  382. core/scripts/safety_bridge.py +968 -0
  383. core/telemetry/__init__.py +9 -0
  384. core/telemetry/client.py +405 -0
  385. core/telemetry/schema.py +122 -0
  386. core/wizard/__init__.py +65 -0
  387. core/wizard/byok_vault.py +147 -0
  388. core/wizard/defaults.py +58 -0
  389. core/wizard/state.py +117 -0
  390. core/wizard/steps.py +70 -0
  391. core/wizard/validation.py +136 -0
  392. marvisx_cli-0.1.0.dist-info/METADATA +201 -0
  393. marvisx_cli-0.1.0.dist-info/RECORD +587 -0
  394. marvisx_cli-0.1.0.dist-info/WHEEL +5 -0
  395. marvisx_cli-0.1.0.dist-info/entry_points.txt +3 -0
  396. marvisx_cli-0.1.0.dist-info/licenses/LICENSE +98 -0
  397. marvisx_cli-0.1.0.dist-info/top_level.txt +3 -0
  398. migrations/001_initial.sql +33 -0
  399. migrations/002_tasks.sql +30 -0
  400. migrations/003_session_management.sql +7 -0
  401. migrations/004_projects_comments.sql +65 -0
  402. migrations/005_session_intelligence.sql +15 -0
  403. migrations/006_settings.sql +12 -0
  404. migrations/007_task_scoring.sql +12 -0
  405. migrations/008_cost_tracking.sql +31 -0
  406. migrations/009_session_card_metrics.sql +3 -0
  407. migrations/010_monitoring.sql +55 -0
  408. migrations/012_agent_api.sql +21 -0
  409. migrations/013_session_complete.sql +8 -0
  410. migrations/015_pull_requests.sql +43 -0
  411. migrations/015_pull_requests_down.sql +5 -0
  412. migrations/016_users_raci.sql +116 -0
  413. migrations/017_task_cost_entries.sql +87 -0
  414. migrations/018_agents.sql +73 -0
  415. migrations/018_agents_down.sql +13 -0
  416. migrations/019_review_feedback.sql +18 -0
  417. migrations/020_pr_commit_sha.sql +4 -0
  418. migrations/021_webhook_events.sql +18 -0
  419. migrations/022_devx_agent_managed.sql +11 -0
  420. migrations/022_devx_agent_managed_down.sql +6 -0
  421. migrations/023_devx_p1_gate.sql +7 -0
  422. migrations/023_devx_p1_gate_down.sql +3 -0
  423. migrations/024_chat_messages.sql +16 -0
  424. migrations/024_pr_conversation_id.sql +8 -0
  425. migrations/024_task_indexes.sql +21 -0
  426. migrations/024_task_indexes_down.sql +7 -0
  427. migrations/025_audit_log.sql +17 -0
  428. migrations/026_agent_tokens.sql +20 -0
  429. migrations/027_teams_auth_phase_b.sql +35 -0
  430. migrations/028_learnings.sql +23 -0
  431. migrations/029_team_roles.sql +14 -0
  432. migrations/030_finder_pins.sql +10 -0
  433. migrations/031_pr_deploy_status.sql +9 -0
  434. migrations/032_task_reminders.sql +7 -0
  435. migrations/033_events_retry_count.sql +6 -0
  436. migrations/033_session_owner.sql +9 -0
  437. migrations/034_notifications.sql +38 -0
  438. migrations/035_shared_links.sql +15 -0
  439. migrations/036_session_index_upgrade.sql +29 -0
  440. migrations/037_pr_approval.sql +15 -0
  441. migrations/038_pr_submitted_by.sql +6 -0
  442. migrations/039_push_subscriptions.sql +17 -0
  443. migrations/040_semantic_search.sql +16 -0
  444. migrations/041_workspaces.sql +63 -0
  445. migrations/042_oidc_providers.sql +24 -0
  446. migrations/043_ci_checks.sql +31 -0
  447. migrations/044_agent_metrics.sql +30 -0
  448. migrations/045_documents_doc_type.sql +5 -0
  449. migrations/046_salience.sql +13 -0
  450. migrations/047_seed_missing_agents.sql +6 -0
  451. migrations/048_fix_agent_paths_roles.sql +5 -0
  452. migrations/049_agent_role_and_learnings_schema.sql +3 -0
  453. migrations/050_session_provider.sql +2 -0
  454. migrations/051_session_launch_profile.sql +4 -0
  455. migrations/052_session_theme_mode.sql +2 -0
  456. migrations/052_task_kind.sql +4 -0
  457. migrations/053_inbox_items.sql +31 -0
  458. migrations/054_inbox_triage_contract.sql +30 -0
  459. migrations/055_inbox_topic_treatment.sql +12 -0
  460. migrations/056_inbox_treatment_read_save.sql +57 -0
  461. migrations/057_session_theme_mode_backfill.sql +4 -0
  462. migrations/058_inbox_item_status_lifecycle.sql +13 -0
  463. migrations/059_inbox_tldr_and_source_scores.sql +18 -0
  464. migrations/060_newsletter.sql +16 -0
  465. migrations/061_inbox_redesign.sql +69 -0
  466. migrations/062_fix_inbox_sources_backfill.sql +37 -0
  467. migrations/063_task_completion_mode.sql +23 -0
  468. migrations/064_judge_mode_setting.sql +4 -0
  469. migrations/065_knowledge_graph_spike.sql +40 -0
  470. migrations/066_digest_ranking_inputs.sql +10 -0
  471. migrations/066_kg_artifact_nodes.sql +129 -0
  472. migrations/067_inbox_digest_selections.sql +28 -0
  473. migrations/067_kg_temporal.sql +53 -0
  474. migrations/068_inbox_digest_app_settings.sql +9 -0
  475. migrations/068_kg_touch_counter.sql +52 -0
  476. migrations/069_kg_doc_types.sql +117 -0
  477. migrations/070_digest_ranking_inputs_recovery.sql +3 -0
  478. migrations/071_inbox_digest_selections_recovery.sql +3 -0
  479. migrations/072_inbox_digest_app_settings_recovery.sql +3 -0
  480. migrations/073_kg_cross_project.sql +216 -0
  481. migrations/073_kg_cross_project_down.sql +77 -0
  482. migrations/074_kg_infra_types.sql +208 -0
  483. migrations/074_kg_infra_types_down.sql +80 -0
  484. migrations/075_kg_file_state_recovery.sql +35 -0
  485. migrations/075_kg_file_state_recovery_down.sql +5 -0
  486. migrations/076_kg_watcher_state.sql +33 -0
  487. migrations/076_kg_watcher_state_down.sql +3 -0
  488. migrations/077_kg_doc_types_extend.sql +226 -0
  489. migrations/077_kg_doc_types_extend_down.sql +80 -0
  490. migrations/078_kg_fts5.sql +102 -0
  491. migrations/078_kg_fts5_down.sql +14 -0
  492. migrations/079_kg_missing_indexes.sql +31 -0
  493. migrations/079_kg_missing_indexes_down.sql +10 -0
  494. migrations/080_kg_fts5_extended.sql +232 -0
  495. migrations/080_kg_fts5_extended_down.sql +25 -0
  496. migrations/081_kg_lens_indexes.sql +9 -0
  497. migrations/081_kg_lens_indexes_down.sql +3 -0
  498. migrations/082_kg_pins.sql +26 -0
  499. migrations/082_kg_pins_down.sql +14 -0
  500. migrations/083_kg_graph_nodes_degree.sql +20 -0
  501. migrations/083_kg_graph_nodes_degree_down.sql +15 -0
  502. migrations/084_drop_legacy_scheduler_tables.sql +58 -0
  503. migrations/084_drop_legacy_scheduler_tables_down.sql +112 -0
  504. migrations/085_kg_edge_resolves_to.sql +142 -0
  505. migrations/085_kg_edge_resolves_to_down.sql +66 -0
  506. migrations/086_project_status_updates_feed.sql +20 -0
  507. migrations/086_project_status_updates_feed_down.sql +36 -0
  508. migrations/087_session_metrics_dual.sql +50 -0
  509. migrations/087_session_metrics_dual_down.sql +21 -0
  510. migrations/088_rename_context_pct_legacy.sql +23 -0
  511. migrations/088_rename_context_pct_legacy_down.sql +8 -0
  512. migrations/089_session_metrics_equivalent_cost.sql +26 -0
  513. migrations/089_session_metrics_equivalent_cost_down.sql +11 -0
  514. migrations/090_kg_inbox_node_type.sql +26 -0
  515. migrations/090_kg_inbox_node_type_down.sql +20 -0
  516. migrations/091_kg_inbox_node_type_check.sql +265 -0
  517. migrations/091_kg_inbox_node_type_check_down.sql +129 -0
  518. migrations/092_sessions_activity_state_ts.sql +29 -0
  519. migrations/092_sessions_activity_state_ts_down.sql +14 -0
  520. migrations/093_sessions_activity_state_column.sql +29 -0
  521. migrations/093_sessions_activity_state_column_down.sql +10 -0
  522. migrations/094_ingest_pending.sql +55 -0
  523. migrations/094_ingest_pending_down.sql +15 -0
  524. migrations/095_kg_intent_first.sql +77 -0
  525. migrations/095_kg_intent_first_down.sql +25 -0
  526. migrations/096_kg_xlsx_artifact_prefix.sql +17 -0
  527. migrations/096_kg_xlsx_artifact_prefix_down.sql +11 -0
  528. migrations/097_ingest_change_history.sql +37 -0
  529. migrations/097_ingest_change_history_down.sql +13 -0
  530. migrations/098_kg_node_type_business.sql +254 -0
  531. migrations/098_kg_node_type_business_down.sql +195 -0
  532. migrations/099_kg_edges_restore_weight.sql +58 -0
  533. migrations/099_kg_edges_restore_weight_down.sql +12 -0
  534. migrations/100_kg_enriched_at.sql +25 -0
  535. migrations/100_kg_enriched_at_down.sql +12 -0
  536. migrations/101_local_llm_shadow_comparisons.sql +66 -0
  537. migrations/101_local_llm_shadow_comparisons_down.sql +15 -0
  538. migrations/102_promote_llm_costs.sql +69 -0
  539. migrations/102_promote_llm_costs_down.sql +19 -0
  540. migrations/103_ingest_skipped_log.sql +46 -0
  541. migrations/103_ingest_skipped_log_down.sql +15 -0
  542. migrations/120_docs_governance.sql +50 -0
  543. migrations/120_docs_governance_down.sql +11 -0
  544. migrations/121_notification_event_fk_cleanup.sql +21 -0
  545. migrations/121_notification_event_fk_cleanup_down.sql +10 -0
  546. migrations/122_docs_drift_history.sql +34 -0
  547. migrations/122_docs_drift_history_down.sql +15 -0
  548. migrations/123_ingest_parser_waiting_status.sql +69 -0
  549. migrations/123_ingest_parser_waiting_status_down.sql +69 -0
  550. migrations/124_heypocket_recordings.sql +63 -0
  551. migrations/124_heypocket_recordings_down.sql +13 -0
  552. migrations/125_kg_node_type_record.sql +219 -0
  553. migrations/125_kg_node_type_record_down.sql +205 -0
  554. migrations/126_ingest_terminal_upload_source_kind.sql +69 -0
  555. migrations/126_ingest_terminal_upload_source_kind_down.sql +69 -0
  556. migrations/127_brain_v1_substrate.sql +200 -0
  557. migrations/127_brain_v1_substrate_down.sql +32 -0
  558. migrations/128_brain_drift_signals.sql +157 -0
  559. migrations/128_brain_drift_signals_down.sql +23 -0
  560. migrations/129_brain_memory_operations.sql +232 -0
  561. migrations/129_brain_memory_operations_down.sql +27 -0
  562. migrations/130_brain_findings.sql +258 -0
  563. migrations/130_brain_findings_down.sql +29 -0
  564. migrations/132_kg_pr_modifies.sql +242 -0
  565. migrations/132_kg_pr_modifies_down.sql +99 -0
  566. migrations/133_brain_v1_2_direction_schema.sql +476 -0
  567. migrations/133_brain_v1_2_direction_schema_down.sql +273 -0
  568. migrations/134_brain_journal_narrative_polished.sql +8 -0
  569. migrations/134_brain_journal_narrative_polished_down.sql +6 -0
  570. migrations/135_kg_edges_provider.sql +21 -0
  571. migrations/136_documents_fts.sql +56 -0
  572. migrations/137_promote_llm_costs.sql +59 -0
  573. migrations/137_promote_llm_costs_down.sql +19 -0
  574. migrations/138_ingest_api_keys.sql +39 -0
  575. migrations/138_ingest_api_keys_down.sql +8 -0
  576. migrations/139_ingest_pending_ingress.sql +91 -0
  577. migrations/139_ingest_pending_ingress_down.sql +73 -0
  578. migrations/140_ingest_idempotency_quota.sql +45 -0
  579. migrations/140_ingest_idempotency_quota_down.sql +9 -0
  580. migrations/141_ingest_pending_metadata.sql +16 -0
  581. migrations/141_ingest_pending_metadata_down.sql +7 -0
  582. migrations/142_llm_function_config.sql +36 -0
  583. migrations/142_llm_function_config_down.sql +8 -0
  584. migrations/143_kg_code_embeddings.sql +25 -0
  585. migrations/143_kg_code_embeddings_down.sql +5 -0
  586. migrations/__init__.py +4 -0
  587. projects/_template/project.yaml +46 -0
@@ -0,0 +1,371 @@
1
+ # Brain v1 — CycleSnapshot loader (sub-02 C0).
2
+ # Single-pass projection of L2 (digest events + journal entries) for the seven
3
+ # drift rules. Rules MUST consume the snapshot — direct SELECT on
4
+ # brain_digest_events / brain_journal_entries is forbidden (layering invariant).
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import logging
9
+ import re
10
+ from collections import defaultdict
11
+ from dataclasses import dataclass, field
12
+ from datetime import datetime, timedelta, timezone
13
+ from typing import Any
14
+
15
+ import aiosqlite
16
+
17
+ from core.api.db import acquire_db
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ # Compile-once: DR5 procedure-change regex shared with the rule.
23
+ PROCEDURE_RE = re.compile(
24
+ r"\b(?:procedura cambiata|nuovo processo|d'ora in poi|da oggi in poi|"
25
+ r"nuova procedura|new process|from now on|procedure changed|updated workflow)\b",
26
+ re.IGNORECASE | re.UNICODE,
27
+ )
28
+
29
+
30
+ @dataclass(slots=True, frozen=True)
31
+ class DigestEventRow:
32
+ """In-memory digest event projection (per-cycle snapshot)."""
33
+
34
+ event_id: str
35
+ event_type: str
36
+ source_system: str
37
+ source_project: str | None
38
+ target_project: str | None
39
+ program_key: str | None
40
+ source_ref: str
41
+ title: str
42
+ summary: str
43
+ observed_at: datetime
44
+ evidence: dict[str, Any]
45
+
46
+
47
+ @dataclass(slots=True, frozen=True)
48
+ class JournalEntryRow:
49
+ """In-memory journal entry projection (per-cycle snapshot)."""
50
+
51
+ entry_id: str
52
+ cycle_key: str
53
+ scope_type: str
54
+ scope_key: str
55
+ program_key: str | None
56
+ body: dict[str, Any]
57
+ is_empty: bool
58
+ published_at: datetime
59
+
60
+
61
+ @dataclass(slots=True, frozen=True)
62
+ class CycleSnapshot:
63
+ """Read-only L2 projection consumed by all DR rules.
64
+
65
+ Rules MUST NOT issue raw SQL — every read goes through this snapshot.
66
+ """
67
+
68
+ cycle_key: str
69
+ run_id: str
70
+ as_of: datetime
71
+ workspace_id: str
72
+ lookback_cycles: int
73
+
74
+ events: tuple[DigestEventRow, ...]
75
+ by_event_type: dict[str, list[DigestEventRow]]
76
+ by_scope: dict[tuple[str, str], list[DigestEventRow]]
77
+ by_source_ref: dict[str, DigestEventRow]
78
+ decision_marker_events: tuple[DigestEventRow, ...]
79
+ procedure_keyword_hits: tuple[DigestEventRow, ...]
80
+ external_update_events: tuple[DigestEventRow, ...]
81
+
82
+ journal_entries: dict[tuple[str, str], JournalEntryRow]
83
+ prior_journal_entries: tuple[JournalEntryRow, ...]
84
+
85
+ project_program: dict[str, str | None] = field(default_factory=dict)
86
+
87
+
88
+ def _parse_iso(value: str) -> datetime:
89
+ dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
90
+ if dt.tzinfo is None:
91
+ dt = dt.replace(tzinfo=timezone.utc)
92
+ return dt
93
+
94
+
95
+ def _parse_evidence(raw: str | None) -> dict[str, Any]:
96
+ if not raw:
97
+ return {}
98
+ try:
99
+ decoded = json.loads(raw)
100
+ except (TypeError, json.JSONDecodeError):
101
+ return {}
102
+ return decoded if isinstance(decoded, dict) else {}
103
+
104
+
105
+ async def _fetch_events(
106
+ db: aiosqlite.Connection, *, run_id: str
107
+ ) -> list[DigestEventRow]:
108
+ rows = await (
109
+ await db.execute(
110
+ "SELECT event_id, event_type, source_system, source_project, "
111
+ " target_project, program_key, source_ref, title, summary, "
112
+ " observed_at, evidence_json "
113
+ "FROM brain_digest_events WHERE run_id = ?",
114
+ (run_id,),
115
+ )
116
+ ).fetchall()
117
+ events: list[DigestEventRow] = []
118
+ for r in rows:
119
+ get = (lambda key: r[key]) if hasattr(r, "keys") else None
120
+ if get is None:
121
+ (
122
+ event_id,
123
+ event_type,
124
+ source_system,
125
+ source_project,
126
+ target_project,
127
+ program_key,
128
+ source_ref,
129
+ title,
130
+ summary,
131
+ observed_at,
132
+ evidence_json,
133
+ ) = r
134
+ else:
135
+ event_id = get("event_id")
136
+ event_type = get("event_type")
137
+ source_system = get("source_system")
138
+ source_project = get("source_project")
139
+ target_project = get("target_project")
140
+ program_key = get("program_key")
141
+ source_ref = get("source_ref")
142
+ title = get("title")
143
+ summary = get("summary")
144
+ observed_at = get("observed_at")
145
+ evidence_json = get("evidence_json")
146
+ events.append(
147
+ DigestEventRow(
148
+ event_id=event_id,
149
+ event_type=event_type,
150
+ source_system=source_system,
151
+ source_project=source_project,
152
+ target_project=target_project,
153
+ program_key=program_key,
154
+ source_ref=source_ref,
155
+ title=title,
156
+ summary=summary or "",
157
+ observed_at=_parse_iso(observed_at),
158
+ evidence=_parse_evidence(evidence_json),
159
+ )
160
+ )
161
+ return events
162
+
163
+
164
+ async def _fetch_journal_for_run(
165
+ db: aiosqlite.Connection, *, run_id: str
166
+ ) -> list[JournalEntryRow]:
167
+ rows = await (
168
+ await db.execute(
169
+ "SELECT entry_id, cycle_key, scope_type, scope_key, program_key, "
170
+ " body_json, is_empty, published_at "
171
+ "FROM brain_journal_entries WHERE run_id = ?",
172
+ (run_id,),
173
+ )
174
+ ).fetchall()
175
+ return [_journal_row(r) for r in rows]
176
+
177
+
178
+ async def _fetch_prior_journal(
179
+ db: aiosqlite.Connection,
180
+ *,
181
+ workspace_id: str,
182
+ cycle_key: str,
183
+ lookback_cycles: int,
184
+ ) -> list[JournalEntryRow]:
185
+ if lookback_cycles <= 0:
186
+ return []
187
+ try:
188
+ ck_date = datetime.fromisoformat(cycle_key).date()
189
+ except ValueError:
190
+ return []
191
+ since = (ck_date - timedelta(days=lookback_cycles)).isoformat()
192
+ rows = await (
193
+ await db.execute(
194
+ "SELECT j.entry_id, j.cycle_key, j.scope_type, j.scope_key, "
195
+ " j.program_key, j.body_json, j.is_empty, j.published_at "
196
+ "FROM brain_journal_entries j "
197
+ "JOIN brain_runs r ON r.run_id = j.run_id "
198
+ "WHERE j.workspace_id = ? AND j.cycle_key >= ? AND j.cycle_key < ? "
199
+ "AND r.status IN ('succeeded','partial') "
200
+ "AND r.superseded_by_run_id IS NULL "
201
+ "ORDER BY j.cycle_key DESC",
202
+ (workspace_id, since, cycle_key),
203
+ )
204
+ ).fetchall()
205
+ return [_journal_row(r) for r in rows]
206
+
207
+
208
+ def _journal_row(r: Any) -> JournalEntryRow:
209
+ if hasattr(r, "keys"):
210
+ body_raw = r["body_json"]
211
+ return JournalEntryRow(
212
+ entry_id=r["entry_id"],
213
+ cycle_key=r["cycle_key"],
214
+ scope_type=r["scope_type"],
215
+ scope_key=r["scope_key"],
216
+ program_key=r["program_key"],
217
+ body=_parse_body(body_raw),
218
+ is_empty=bool(r["is_empty"]),
219
+ published_at=_parse_iso(r["published_at"]),
220
+ )
221
+ (
222
+ entry_id,
223
+ cycle_key,
224
+ scope_type,
225
+ scope_key,
226
+ program_key,
227
+ body_raw,
228
+ is_empty,
229
+ published_at,
230
+ ) = r
231
+ return JournalEntryRow(
232
+ entry_id=entry_id,
233
+ cycle_key=cycle_key,
234
+ scope_type=scope_type,
235
+ scope_key=scope_key,
236
+ program_key=program_key,
237
+ body=_parse_body(body_raw),
238
+ is_empty=bool(is_empty),
239
+ published_at=_parse_iso(published_at),
240
+ )
241
+
242
+
243
+ def _parse_body(raw: str | None) -> dict[str, Any]:
244
+ if not raw:
245
+ return {}
246
+ try:
247
+ decoded = json.loads(raw)
248
+ except (TypeError, json.JSONDecodeError):
249
+ return {}
250
+ return decoded if isinstance(decoded, dict) else {}
251
+
252
+
253
+ def _index_events(
254
+ events: list[DigestEventRow],
255
+ ) -> tuple[
256
+ dict[str, list[DigestEventRow]],
257
+ dict[tuple[str, str], list[DigestEventRow]],
258
+ dict[str, DigestEventRow],
259
+ list[DigestEventRow],
260
+ list[DigestEventRow],
261
+ list[DigestEventRow],
262
+ dict[str, str | None],
263
+ ]:
264
+ by_event_type: dict[str, list[DigestEventRow]] = defaultdict(list)
265
+ by_scope: dict[tuple[str, str], list[DigestEventRow]] = defaultdict(list)
266
+ by_source_ref: dict[str, DigestEventRow] = {}
267
+ decision_marker_events: list[DigestEventRow] = []
268
+ procedure_keyword_hits: list[DigestEventRow] = []
269
+ external_update_events: list[DigestEventRow] = []
270
+ project_program: dict[str, str | None] = {}
271
+
272
+ for ev in events:
273
+ by_event_type[ev.event_type].append(ev)
274
+ by_source_ref.setdefault(ev.source_ref, ev)
275
+
276
+ scopes: set[tuple[str, str]] = set()
277
+ for slug in (ev.source_project, ev.target_project):
278
+ if slug:
279
+ scopes.add(("project", slug))
280
+ project_program.setdefault(slug, ev.program_key)
281
+ if ev.program_key:
282
+ scopes.add(("program", ev.program_key))
283
+ scopes.add(("company", "__company__"))
284
+ for key in scopes:
285
+ by_scope[key].append(ev)
286
+
287
+ marker = ev.evidence.get("decision_marker")
288
+ if isinstance(marker, str) and marker:
289
+ decision_marker_events.append(ev)
290
+
291
+ if ev.summary and PROCEDURE_RE.search(ev.summary):
292
+ procedure_keyword_hits.append(ev)
293
+
294
+ if ev.event_type == "external_update_seen":
295
+ external_update_events.append(ev)
296
+
297
+ return (
298
+ dict(by_event_type),
299
+ dict(by_scope),
300
+ by_source_ref,
301
+ decision_marker_events,
302
+ procedure_keyword_hits,
303
+ external_update_events,
304
+ project_program,
305
+ )
306
+
307
+
308
+ async def build_snapshot(
309
+ cycle_key: str,
310
+ *,
311
+ run_id: str,
312
+ workspace_id: str = "ws_default",
313
+ lookback_cycles: int = 7,
314
+ as_of: datetime | None = None,
315
+ ) -> CycleSnapshot:
316
+ """Build a single-pass projection of L2 outputs for the given run.
317
+
318
+ Pure read-only — never writes to substrate or sibling Brain tables.
319
+ """
320
+ now = as_of or datetime.now(timezone.utc)
321
+ async with acquire_db() as db:
322
+ db.row_factory = aiosqlite.Row
323
+ events = await _fetch_events(db, run_id=run_id)
324
+ current_journal = await _fetch_journal_for_run(db, run_id=run_id)
325
+ prior_journal = await _fetch_prior_journal(
326
+ db,
327
+ workspace_id=workspace_id,
328
+ cycle_key=cycle_key,
329
+ lookback_cycles=lookback_cycles,
330
+ )
331
+
332
+ (
333
+ by_event_type,
334
+ by_scope,
335
+ by_source_ref,
336
+ decision_marker_events,
337
+ procedure_keyword_hits,
338
+ external_update_events,
339
+ project_program,
340
+ ) = _index_events(events)
341
+
342
+ journal_entries: dict[tuple[str, str], JournalEntryRow] = {
343
+ (e.scope_type, e.scope_key): e for e in current_journal
344
+ }
345
+
346
+ return CycleSnapshot(
347
+ cycle_key=cycle_key,
348
+ run_id=run_id,
349
+ as_of=now,
350
+ workspace_id=workspace_id,
351
+ lookback_cycles=lookback_cycles,
352
+ events=tuple(events),
353
+ by_event_type=by_event_type,
354
+ by_scope=by_scope,
355
+ by_source_ref=by_source_ref,
356
+ decision_marker_events=tuple(decision_marker_events),
357
+ procedure_keyword_hits=tuple(procedure_keyword_hits),
358
+ external_update_events=tuple(external_update_events),
359
+ journal_entries=journal_entries,
360
+ prior_journal_entries=tuple(prior_journal),
361
+ project_program=project_program,
362
+ )
363
+
364
+
365
+ __all__ = [
366
+ "CycleSnapshot",
367
+ "DigestEventRow",
368
+ "JournalEntryRow",
369
+ "PROCEDURE_RE",
370
+ "build_snapshot",
371
+ ]
@@ -0,0 +1,147 @@
1
+ # Brain v1 — Digest source collectors (sub-01 D1).
2
+ # Each collector is mechanical: reads substrate ≤ cutoff_at, yields EventDraft objects.
3
+ # No LLM, no business state mutation, no auto-task creation.
4
+ from __future__ import annotations
5
+
6
+ import dataclasses
7
+ import logging
8
+ from collections.abc import AsyncIterator, Awaitable, Callable
9
+ from dataclasses import dataclass
10
+
11
+ from core.api.services.brain.models import EventDraft, SourceCollectorContext
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ CollectorFn = Callable[[SourceCollectorContext], AsyncIterator[EventDraft]]
17
+
18
+ # Wave 3.1 gap 5: orchestrator-side pagination upper bound. A pathological
19
+ # source that never advances its watermark must not loop forever; this caps
20
+ # total iterations per (source, cycle) so backfill stays bounded even when a
21
+ # collector mis-orders observed_at. With cap=1000 the effective ceiling is
22
+ # 50_000 events per source per cycle — well above any realistic substrate.
23
+ MAX_COLLECT_ITERATIONS = 50
24
+
25
+
26
+ @dataclass(slots=True, frozen=True)
27
+ class SourceCollector:
28
+ """Minimal collector descriptor. Concrete implementations live alongside
29
+ the substrate they read — Brain just sequences them per cycle.
30
+
31
+ For v1 the implementations are stubs registered explicitly from jobs.py.
32
+ Sub-02/03/04 will register additional collectors as phases extend.
33
+ """
34
+
35
+ source_system: str
36
+ collect: CollectorFn
37
+
38
+
39
+ _REGISTERED: list[SourceCollector] = []
40
+
41
+
42
+ def register_collector(collector: SourceCollector) -> None:
43
+ """Idempotent registration keyed by source_system."""
44
+ for existing in _REGISTERED:
45
+ if existing.source_system == collector.source_system:
46
+ return
47
+ _REGISTERED.append(collector)
48
+
49
+
50
+ def registered_collectors() -> list[SourceCollector]:
51
+ return list(_REGISTERED)
52
+
53
+
54
+ def clear_registered_collectors() -> None:
55
+ """Test-only helper."""
56
+ _REGISTERED.clear()
57
+
58
+
59
+ async def empty_collector(_ctx: SourceCollectorContext) -> AsyncIterator[EventDraft]:
60
+ """Default no-op collector used as a placeholder for sources without a producer."""
61
+ if False: # pragma: no cover — keeps the async generator shape.
62
+ yield None # type: ignore[misc]
63
+
64
+
65
+ async def collect_from_source(
66
+ collector: SourceCollector, ctx: SourceCollectorContext
67
+ ) -> list[EventDraft]:
68
+ """Materialize a collector with orchestrator-side pagination (Wave 3.1 gap 5).
69
+
70
+ Pattern:
71
+ * Pull up to `ctx.per_source_event_cap` drafts in one pass.
72
+ * If the batch hits the cap, advance `ctx.since` to the max observed_at
73
+ seen so far and re-call the collector. Repeat until a partial batch
74
+ comes back or the bound `MAX_COLLECT_ITERATIONS` is reached.
75
+ * No synthetic `external_update_seen` overflow event — the orchestrator
76
+ now drains the source completely and the cycle reports `succeeded`
77
+ instead of `partial`.
78
+
79
+ Guards:
80
+ * Bail when no watermark progress (last_observed_at <= ctx.since).
81
+ * Bail at MAX_COLLECT_ITERATIONS with a WARNING — keeps a pathological
82
+ source from blocking the cycle.
83
+ """
84
+ all_drafts: list[EventDraft] = []
85
+ current_ctx = ctx
86
+ iterations = 0
87
+
88
+ while True:
89
+ batch: list[EventDraft] = []
90
+ cap = current_ctx.per_source_event_cap
91
+ async for draft in collector.collect(current_ctx):
92
+ if len(batch) >= cap:
93
+ # Stop reading from this batch — the collector should not
94
+ # return more than cap, but we defend regardless.
95
+ break
96
+ batch.append(draft)
97
+ all_drafts.extend(batch)
98
+ iterations += 1
99
+
100
+ if len(batch) < cap:
101
+ # Batch is below cap → source is fully drained for this window.
102
+ break
103
+
104
+ if iterations >= MAX_COLLECT_ITERATIONS:
105
+ logger.warning(
106
+ "collect_from_source: max %d iterations reached for %s "
107
+ "(cycle=%s) — stopping pagination",
108
+ MAX_COLLECT_ITERATIONS,
109
+ collector.source_system,
110
+ current_ctx.cycle_key,
111
+ )
112
+ break
113
+
114
+ # Advance the watermark on the iteration ctx so the next call returns
115
+ # the subsequent slice. We use the max observed_at to keep ordering
116
+ # robust even when collectors yield slightly out-of-order events.
117
+ try:
118
+ last_observed = max(d.observed_at for d in batch)
119
+ except ValueError:
120
+ break
121
+
122
+ if last_observed <= current_ctx.since_watermark:
123
+ logger.warning(
124
+ "collect_from_source: no watermark advance for %s (cycle=%s, "
125
+ "since=%s, last_observed=%s) — stopping to avoid infinite loop",
126
+ collector.source_system,
127
+ current_ctx.cycle_key,
128
+ current_ctx.since_watermark.isoformat() if current_ctx.since_watermark else "",
129
+ last_observed.isoformat() if last_observed else "",
130
+ )
131
+ break
132
+
133
+ current_ctx = dataclasses.replace(current_ctx, since_watermark=last_observed)
134
+
135
+ return all_drafts
136
+
137
+
138
+ __all__ = [
139
+ "CollectorFn",
140
+ "MAX_COLLECT_ITERATIONS",
141
+ "SourceCollector",
142
+ "clear_registered_collectors",
143
+ "collect_from_source",
144
+ "empty_collector",
145
+ "register_collector",
146
+ "registered_collectors",
147
+ ]