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,409 @@
1
+ # Brain v1 — Drift router service (sub-02 C5, §11.1).
2
+ # List / fetch / patch backing for the HTTP router. Visibility uses
3
+ # api/visibility.py — ALL-or-redacted per §4.3.
4
+ from __future__ import annotations
5
+
6
+ import base64
7
+ import binascii
8
+ import json
9
+ import logging
10
+ from datetime import datetime, timezone
11
+ from typing import Any
12
+
13
+ import aiosqlite
14
+
15
+ from core.api.db import acquire_db, write_db
16
+ from core.api.models import UserInfo
17
+ from core.api.models.brain import (
18
+ DirectionSource,
19
+ DriftAxis,
20
+ DriftListResponse,
21
+ DriftPatchAction,
22
+ DriftSignal,
23
+ DriftSignalRedacted,
24
+ KnowledgeForm,
25
+ RuleId,
26
+ ScopeType,
27
+ Severity,
28
+ SignalState,
29
+ SignalType,
30
+ )
31
+ from core.api.visibility import get_visible_projects
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ DEFAULT_DRIFT_LIMIT = 50
36
+ MAX_DRIFT_LIMIT = 200
37
+
38
+ _SEVERITY_RANK: dict[Severity, int] = {
39
+ "low": 1,
40
+ "medium": 2,
41
+ "high": 3,
42
+ "critical": 4,
43
+ }
44
+
45
+ _VALID_AXES: frozenset[DriftAxis] = frozenset({"intent", "context", "both"})
46
+
47
+
48
+ def _encode_cursor(severity: Severity, confidence: float, detected_at: str, signal_id: str) -> str:
49
+ payload = json.dumps(
50
+ {
51
+ "s": _SEVERITY_RANK.get(severity, 0),
52
+ "c": round(confidence, 6),
53
+ "d": detected_at,
54
+ "i": signal_id,
55
+ },
56
+ separators=(",", ":"),
57
+ )
58
+ return base64.urlsafe_b64encode(payload.encode("utf-8")).decode("ascii")
59
+
60
+
61
+ def _decode_cursor(cursor: str) -> tuple[int, float, str, str] | None:
62
+ try:
63
+ raw = base64.urlsafe_b64decode(cursor.encode("ascii"))
64
+ payload = json.loads(raw)
65
+ return (
66
+ int(payload["s"]),
67
+ float(payload["c"]),
68
+ str(payload["d"]),
69
+ str(payload["i"]),
70
+ )
71
+ except (KeyError, ValueError, binascii.Error, json.JSONDecodeError, UnicodeDecodeError):
72
+ return None
73
+
74
+
75
+ def _parse_iso(value: str | None) -> datetime | None:
76
+ if value is None:
77
+ return None
78
+ try:
79
+ dt = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
80
+ except ValueError:
81
+ return None
82
+ if dt.tzinfo is None:
83
+ dt = dt.replace(tzinfo=timezone.utc)
84
+ return dt
85
+
86
+
87
+ def _split_axes(values: list[str] | None) -> list[DriftAxis] | None:
88
+ if not values:
89
+ return None
90
+ out: list[DriftAxis] = []
91
+ for v in values:
92
+ for token in str(v).split(","):
93
+ token = token.strip()
94
+ if token in _VALID_AXES:
95
+ out.append(token) # type: ignore[arg-type]
96
+ return out or None
97
+
98
+
99
+ async def _resolve_run(
100
+ db: aiosqlite.Connection,
101
+ *,
102
+ cycle_key: str | None,
103
+ run_id: str | None,
104
+ workspace_id: str,
105
+ ) -> dict[str, Any] | None:
106
+ if run_id:
107
+ row = await (
108
+ await db.execute(
109
+ "SELECT run_id, cycle_key FROM brain_runs WHERE run_id = ?",
110
+ (run_id,),
111
+ )
112
+ ).fetchone()
113
+ if row is None:
114
+ return None
115
+ return {"run_id": row[0], "cycle_key": row[1]}
116
+ if not cycle_key or cycle_key == "latest":
117
+ row = await (
118
+ await db.execute(
119
+ "SELECT run_id, cycle_key FROM brain_runs "
120
+ "WHERE workspace_id = ? AND status IN ('succeeded','partial') "
121
+ "AND superseded_by_run_id IS NULL "
122
+ "ORDER BY cycle_key DESC, started_at DESC LIMIT 1",
123
+ (workspace_id,),
124
+ )
125
+ ).fetchone()
126
+ if row is None:
127
+ return None
128
+ return {"run_id": row[0], "cycle_key": row[1]}
129
+ row = await (
130
+ await db.execute(
131
+ "SELECT run_id, cycle_key FROM brain_runs "
132
+ "WHERE workspace_id = ? AND cycle_key = ? "
133
+ "AND status IN ('succeeded','partial') "
134
+ "AND superseded_by_run_id IS NULL "
135
+ "ORDER BY started_at DESC LIMIT 1",
136
+ (workspace_id, cycle_key),
137
+ )
138
+ ).fetchone()
139
+ if row is None:
140
+ return None
141
+ return {"run_id": row[0], "cycle_key": row[1]}
142
+
143
+
144
+ def _row_to_signal(row: aiosqlite.Row) -> DriftSignal:
145
+ return DriftSignal(
146
+ signal_id=row["signal_id"],
147
+ run_id=row["run_id"],
148
+ cycle_key=row["cycle_key"],
149
+ detected_at=_parse_iso(row["detected_at"]) or datetime.now(timezone.utc),
150
+ rule_id=row["rule_id"],
151
+ schema_version=row["schema_version"],
152
+ scope_type=row["scope_type"],
153
+ scope_key=row["scope_key"],
154
+ program_key=row["program_key"],
155
+ signal_type=row["signal_type"],
156
+ knowledge_form=row["knowledge_form"],
157
+ classifier_version=row["classifier_version"],
158
+ expected_direction_source=row["expected_direction_source"],
159
+ expected_direction_ref=row["expected_direction_ref"],
160
+ observed_direction_ref=row["observed_direction_ref"],
161
+ observed_delta=row["observed_delta"],
162
+ evidence=json.loads(row["evidence_json"] or "[]"),
163
+ evidence_hash=row["evidence_hash"],
164
+ severity=row["severity"],
165
+ confidence=row["confidence"],
166
+ recurrence_key=row["recurrence_key"],
167
+ involved_projects=json.loads(row["involved_projects_json"] or "[]"),
168
+ state=row["state"],
169
+ superseded_by_signal_id=row["superseded_by_signal_id"],
170
+ resolved_at=_parse_iso(row["resolved_at"]),
171
+ dismissed_at=_parse_iso(row["dismissed_at"]),
172
+ dismissed_by=row["dismissed_by"],
173
+ dismiss_reason=row["dismiss_reason"],
174
+ drift_axis=row["drift_axis"],
175
+ )
176
+
177
+
178
+ def _redact(sig: DriftSignal) -> DriftSignalRedacted:
179
+ return DriftSignalRedacted(
180
+ signal_id=sig.signal_id,
181
+ cycle_key=sig.cycle_key,
182
+ signal_type=sig.signal_type,
183
+ severity=sig.severity,
184
+ )
185
+
186
+
187
+ def _is_visible(visible: set[str] | None, involved_projects: list[str]) -> bool:
188
+ if visible is None:
189
+ return True
190
+ if not involved_projects:
191
+ return True
192
+ return set(involved_projects).issubset(visible)
193
+
194
+
195
+ async def list_drift_signals(
196
+ *,
197
+ cycle_key: str | None = None,
198
+ run_id: str | None = None,
199
+ scope_type: str | None = None,
200
+ scope_key: str | None = None,
201
+ signal_types: list[SignalType] | None = None,
202
+ knowledge_forms: list[KnowledgeForm] | None = None,
203
+ severity_min: Severity = "low",
204
+ confidence_min: float = 0.0,
205
+ states: list[SignalState] | None = None,
206
+ drift_axes: list[str] | None = None,
207
+ rule_ids: list[str] | None = None,
208
+ cursor: str | None = None,
209
+ limit: int = DEFAULT_DRIFT_LIMIT,
210
+ user: UserInfo | None = None,
211
+ workspace_id: str = "ws_default",
212
+ ) -> DriftListResponse:
213
+ limit = max(1, min(MAX_DRIFT_LIMIT, int(limit)))
214
+ over_fetch = limit + 1
215
+ states_to_apply = states or ["open"]
216
+ axes = _split_axes(drift_axes)
217
+
218
+ async with acquire_db() as db:
219
+ db.row_factory = aiosqlite.Row
220
+ run = await _resolve_run(
221
+ db, cycle_key=cycle_key, run_id=run_id, workspace_id=workspace_id
222
+ )
223
+ if run is None:
224
+ return DriftListResponse(items=[], total_returned=0)
225
+
226
+ visible = await get_visible_projects(db, user, workspace_id) if user else None
227
+
228
+ where = ["s.run_id = ?"]
229
+ params: list[Any] = [run["run_id"]]
230
+ if scope_type:
231
+ where.append("s.scope_type = ?")
232
+ params.append(scope_type)
233
+ if scope_key:
234
+ where.append("s.scope_key = ?")
235
+ params.append(scope_key)
236
+ if signal_types:
237
+ placeholders = ",".join("?" for _ in signal_types)
238
+ where.append(f"s.signal_type IN ({placeholders})")
239
+ params.extend(signal_types)
240
+ if knowledge_forms:
241
+ placeholders = ",".join("?" for _ in knowledge_forms)
242
+ where.append(f"s.knowledge_form IN ({placeholders})")
243
+ params.extend(knowledge_forms)
244
+ if rule_ids:
245
+ placeholders = ",".join("?" for _ in rule_ids)
246
+ where.append(f"s.rule_id IN ({placeholders})")
247
+ params.extend(rule_ids)
248
+ if axes:
249
+ placeholders = ",".join("?" for _ in axes)
250
+ where.append(f"s.drift_axis IN ({placeholders})")
251
+ params.extend(axes)
252
+ if states_to_apply:
253
+ placeholders = ",".join("?" for _ in states_to_apply)
254
+ where.append(f"s.state IN ({placeholders})")
255
+ params.extend(states_to_apply)
256
+ min_rank = _SEVERITY_RANK.get(severity_min, 1)
257
+ where.append(
258
+ "CASE s.severity WHEN 'low' THEN 1 WHEN 'medium' THEN 2 WHEN 'high' THEN 3 WHEN 'critical' THEN 4 END >= ?"
259
+ )
260
+ params.append(min_rank)
261
+ where.append("s.confidence >= ?")
262
+ params.append(confidence_min)
263
+
264
+ if cursor:
265
+ decoded = _decode_cursor(cursor)
266
+ if decoded:
267
+ cur_sev, cur_conf, cur_dt, cur_id = decoded
268
+ where.append(
269
+ "(CASE s.severity WHEN 'low' THEN 1 WHEN 'medium' THEN 2 WHEN 'high' THEN 3 WHEN 'critical' THEN 4 END, "
270
+ "s.confidence, s.detected_at, s.signal_id) < (?, ?, ?, ?)"
271
+ )
272
+ params.extend([cur_sev, cur_conf, cur_dt, cur_id])
273
+
274
+ query = (
275
+ "SELECT s.* FROM brain_drift_signals s "
276
+ f"WHERE {' AND '.join(where)} "
277
+ "ORDER BY "
278
+ " CASE s.severity WHEN 'low' THEN 1 WHEN 'medium' THEN 2 WHEN 'high' THEN 3 WHEN 'critical' THEN 4 END DESC, "
279
+ " s.confidence DESC, s.detected_at DESC, s.signal_id ASC "
280
+ "LIMIT ?"
281
+ )
282
+ params.append(over_fetch)
283
+ rows = await (await db.execute(query, params)).fetchall()
284
+
285
+ items: list[DriftSignal | DriftSignalRedacted] = []
286
+ next_cursor: str | None = None
287
+ redacted_count = 0
288
+ page_rows = rows[:limit]
289
+ if len(rows) > limit:
290
+ last = page_rows[-1]
291
+ next_cursor = _encode_cursor(
292
+ last["severity"],
293
+ last["confidence"],
294
+ last["detected_at"],
295
+ last["signal_id"],
296
+ )
297
+ for row in page_rows:
298
+ sig = _row_to_signal(row)
299
+ if not _is_visible(visible, sig.involved_projects):
300
+ redacted_count += 1
301
+ items.append(_redact(sig))
302
+ continue
303
+ items.append(sig)
304
+
305
+ return DriftListResponse(
306
+ items=items,
307
+ next_cursor=next_cursor,
308
+ cycle_key=run["cycle_key"],
309
+ run_id=run["run_id"],
310
+ redacted_count=redacted_count,
311
+ total_returned=len(items),
312
+ )
313
+
314
+
315
+ async def fetch_single_drift_signal(
316
+ *,
317
+ signal_id: str,
318
+ user: UserInfo | None,
319
+ workspace_id: str = "ws_default",
320
+ ) -> DriftSignal | None:
321
+ async with acquire_db() as db:
322
+ db.row_factory = aiosqlite.Row
323
+ row = await (
324
+ await db.execute(
325
+ "SELECT * FROM brain_drift_signals WHERE signal_id = ?",
326
+ (signal_id,),
327
+ )
328
+ ).fetchone()
329
+ if row is None:
330
+ return None
331
+ visible = await get_visible_projects(db, user, workspace_id) if user else None
332
+ sig = _row_to_signal(row)
333
+ if not _is_visible(visible, sig.involved_projects):
334
+ # 404 — never reveal existence to redacted callers.
335
+ return None
336
+ return sig
337
+
338
+
339
+ async def apply_drift_patch(
340
+ *,
341
+ signal_id: str,
342
+ action: DriftPatchAction,
343
+ reason: str | None,
344
+ user: UserInfo,
345
+ workspace_id: str = "ws_default",
346
+ now: datetime,
347
+ ) -> DriftSignal | None:
348
+ iso_now = now.astimezone(timezone.utc).isoformat()
349
+ # First load + visibility gate.
350
+ existing = await fetch_single_drift_signal(
351
+ signal_id=signal_id, user=user, workspace_id=workspace_id
352
+ )
353
+ if existing is None:
354
+ return None
355
+
356
+ new_state: SignalState = existing.state
357
+ resolved_at = existing.resolved_at.astimezone(timezone.utc).isoformat() if existing.resolved_at else None
358
+ dismissed_at = existing.dismissed_at.astimezone(timezone.utc).isoformat() if existing.dismissed_at else None
359
+ dismissed_by = existing.dismissed_by
360
+ dismiss_reason = existing.dismiss_reason
361
+
362
+ if action == "dismiss":
363
+ new_state = "dismissed"
364
+ dismissed_at = iso_now
365
+ dismissed_by = user.user_id
366
+ dismiss_reason = reason
367
+ elif action == "acknowledge":
368
+ # Acknowledge keeps state=open but stores reason as dismiss_reason
369
+ # surrogate (no separate column in v1 schema).
370
+ new_state = "open"
371
+ dismiss_reason = reason
372
+ elif action == "resolve":
373
+ new_state = "resolved"
374
+ resolved_at = iso_now
375
+ elif action == "reopen":
376
+ new_state = "open"
377
+ resolved_at = None
378
+ dismissed_at = None
379
+ dismissed_by = None
380
+ dismiss_reason = None
381
+
382
+ async with write_db() as db:
383
+ await db.execute(
384
+ "UPDATE brain_drift_signals SET "
385
+ " state = ?, resolved_at = ?, dismissed_at = ?, dismissed_by = ?,"
386
+ " dismiss_reason = ?"
387
+ " WHERE signal_id = ?",
388
+ (
389
+ new_state,
390
+ resolved_at,
391
+ dismissed_at,
392
+ dismissed_by,
393
+ dismiss_reason,
394
+ signal_id,
395
+ ),
396
+ )
397
+ refreshed = await fetch_single_drift_signal(
398
+ signal_id=signal_id, user=user, workspace_id=workspace_id
399
+ )
400
+ return refreshed
401
+
402
+
403
+ __all__ = [
404
+ "DEFAULT_DRIFT_LIMIT",
405
+ "MAX_DRIFT_LIMIT",
406
+ "apply_drift_patch",
407
+ "fetch_single_drift_signal",
408
+ "list_drift_signals",
409
+ ]
@@ -0,0 +1,79 @@
1
+ # Brain v1 — M3 Edge Metrics compatibility shim (sub-03 §6).
2
+ #
3
+ # Translates `reinforce` operations into a future-compatible `kg_edge_metric`
4
+ # proposal. v1 stores the proposal as evidence only — NO mutation of
5
+ # `kg_edges` weight. v2 may write to `graph_edge_metrics` directly.
6
+ #
7
+ # Layering invariant:
8
+ # * No SQL on kg_edges/kg_nodes here.
9
+ # * Manual pin boost reuses sub-03 §4.6 score derivation (deterministic).
10
+ from __future__ import annotations
11
+
12
+ from core.api.models.brain import ProposedWriteKGEdgeMetric
13
+
14
+
15
+ SCORE_BASELINE = 0.5
16
+ SCORE_PER_EVIDENCE = 0.1
17
+ SCORE_EVIDENCE_CAP = 0.4
18
+ SCORE_DRIFT_BOOST = 0.1
19
+ SCORE_RECURRENCE_BOOST = 0.1
20
+ SCORE_PIN_BOOST = 0.2
21
+ SCORE_MULTI_CYCLE_BOOST = 0.1
22
+
23
+
24
+ def compute_reinforce_score(
25
+ *,
26
+ evidence: list[str],
27
+ drift_refs_present: bool = False,
28
+ recurrence_count: int = 1,
29
+ pins_hit: bool = False,
30
+ multi_cycle: bool = False,
31
+ ) -> float:
32
+ """Deterministic evidence-density score per sub-03 §4.6."""
33
+ score = SCORE_BASELINE
34
+ score += min(SCORE_EVIDENCE_CAP, SCORE_PER_EVIDENCE * len(evidence))
35
+ if drift_refs_present:
36
+ score += SCORE_DRIFT_BOOST
37
+ if recurrence_count >= 3:
38
+ score += SCORE_RECURRENCE_BOOST
39
+ if pins_hit:
40
+ score += SCORE_PIN_BOOST
41
+ if multi_cycle:
42
+ score += SCORE_MULTI_CYCLE_BOOST
43
+ if score < 0.0:
44
+ score = 0.0
45
+ if score > 1.0:
46
+ score = 1.0
47
+ return round(score, 4)
48
+
49
+
50
+ def build_kg_edge_metric_payload(
51
+ *,
52
+ edge_id: str,
53
+ score: float,
54
+ metric_kind: str = "reinforce_score",
55
+ ) -> ProposedWriteKGEdgeMetric:
56
+ """Construct a ProposedWriteKGEdgeMetric payload.
57
+
58
+ `delta` is the increment relative to the existing metric — v1 carries the
59
+ full score as delta from baseline (clamped to [-1.0, 1.0]).
60
+ """
61
+ delta = max(-1.0, min(1.0, score - SCORE_BASELINE))
62
+ return ProposedWriteKGEdgeMetric(
63
+ edge_id=edge_id,
64
+ metric_kind=metric_kind, # type: ignore[arg-type]
65
+ delta=round(delta, 4),
66
+ )
67
+
68
+
69
+ __all__ = [
70
+ "SCORE_BASELINE",
71
+ "SCORE_DRIFT_BOOST",
72
+ "SCORE_EVIDENCE_CAP",
73
+ "SCORE_MULTI_CYCLE_BOOST",
74
+ "SCORE_PER_EVIDENCE",
75
+ "SCORE_PIN_BOOST",
76
+ "SCORE_RECURRENCE_BOOST",
77
+ "build_kg_edge_metric_payload",
78
+ "compute_reinforce_score",
79
+ ]
@@ -0,0 +1,222 @@
1
+ # Brain v1 — Events read API (sub-01 D6, NEW 2026-05-16).
2
+ # Consumed by HTTP GET /api/v1/brain/events, the Console PipelineSubbar Digest
3
+ # station, and the EvidenceDrawer drill-down. Stable cursor sort
4
+ # (observed_at DESC, event_id) per parent §9.8.
5
+ from __future__ import annotations
6
+
7
+ import base64
8
+ import binascii
9
+ import json
10
+ import logging
11
+ from datetime import datetime, timezone
12
+ from typing import Any
13
+
14
+ import aiosqlite
15
+
16
+ from core.api.db import acquire_db
17
+ from core.api.models import UserInfo
18
+ from core.api.models.brain import (
19
+ DigestEvent,
20
+ DigestEventRedacted,
21
+ EventsListResponse,
22
+ EventType,
23
+ )
24
+ from core.api.visibility import get_visible_projects
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ MAX_LIMIT = 200
29
+ DEFAULT_LIMIT = 50
30
+
31
+
32
+ def _encode_cursor(observed_at: str, event_id: str) -> str:
33
+ payload = json.dumps({"o": observed_at, "e": event_id}, separators=(",", ":"))
34
+ return base64.urlsafe_b64encode(payload.encode("utf-8")).decode("ascii")
35
+
36
+
37
+ def _decode_cursor(cursor: str) -> tuple[str, str] | None:
38
+ try:
39
+ raw = base64.urlsafe_b64decode(cursor.encode("ascii"))
40
+ payload = json.loads(raw)
41
+ return payload["o"], payload["e"]
42
+ except (KeyError, ValueError, binascii.Error, json.JSONDecodeError, UnicodeDecodeError):
43
+ return None
44
+
45
+
46
+ def _parse_iso(value: str) -> datetime:
47
+ dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
48
+ if dt.tzinfo is None:
49
+ dt = dt.replace(tzinfo=timezone.utc)
50
+ return dt
51
+
52
+
53
+ def _is_visible(
54
+ visible: set[str] | None, source_project: str | None, target_project: str | None
55
+ ) -> bool:
56
+ if visible is None:
57
+ return True
58
+ projects = {p for p in (source_project, target_project) if p}
59
+ if not projects:
60
+ return True
61
+ return projects.issubset(visible)
62
+
63
+
64
+ async def _resolve_run(
65
+ db: aiosqlite.Connection,
66
+ *,
67
+ cycle_key: str | None,
68
+ run_id: str | None,
69
+ workspace_id: str,
70
+ ) -> dict[str, Any] | None:
71
+ if run_id:
72
+ row = await (
73
+ await db.execute(
74
+ "SELECT run_id, cycle_key FROM brain_runs WHERE run_id = ?",
75
+ (run_id,),
76
+ )
77
+ ).fetchone()
78
+ if row is None:
79
+ return None
80
+ return {"run_id": row[0], "cycle_key": row[1]}
81
+
82
+ if not cycle_key or cycle_key == "latest":
83
+ row = await (
84
+ await db.execute(
85
+ "SELECT run_id, cycle_key FROM brain_runs "
86
+ "WHERE workspace_id = ? AND status IN ('succeeded', 'partial') "
87
+ "AND superseded_by_run_id IS NULL "
88
+ "ORDER BY cycle_key DESC, started_at DESC LIMIT 1",
89
+ (workspace_id,),
90
+ )
91
+ ).fetchone()
92
+ if row is None:
93
+ return None
94
+ return {"run_id": row[0], "cycle_key": row[1]}
95
+
96
+ row = await (
97
+ await db.execute(
98
+ "SELECT run_id, cycle_key FROM brain_runs "
99
+ "WHERE workspace_id = ? AND cycle_key = ? "
100
+ "AND status IN ('succeeded', 'partial') "
101
+ "AND superseded_by_run_id IS NULL "
102
+ "ORDER BY started_at DESC LIMIT 1",
103
+ (workspace_id, cycle_key),
104
+ )
105
+ ).fetchone()
106
+ if row is None:
107
+ return None
108
+ return {"run_id": row[0], "cycle_key": row[1]}
109
+
110
+
111
+ async def list_events_for_cycle(
112
+ *,
113
+ cycle_key: str | None = None,
114
+ run_id: str | None = None,
115
+ event_type: list[EventType] | None = None,
116
+ source_project: str | None = None,
117
+ cursor: str | None = None,
118
+ limit: int = DEFAULT_LIMIT,
119
+ user: UserInfo | None = None,
120
+ workspace_id: str = "ws_default",
121
+ ) -> EventsListResponse:
122
+ """Return paginated events with visibility filter applied."""
123
+ limit = max(1, min(MAX_LIMIT, int(limit)))
124
+ over_fetch = limit + 1
125
+
126
+ async with acquire_db() as db:
127
+ db.row_factory = aiosqlite.Row
128
+ run = await _resolve_run(
129
+ db, cycle_key=cycle_key, run_id=run_id, workspace_id=workspace_id
130
+ )
131
+ if run is None:
132
+ return EventsListResponse(items=[], next_cursor=None, total_returned=0)
133
+
134
+ visible = await get_visible_projects(db, user, workspace_id) if user else None
135
+
136
+ where = ["e.run_id = ?"]
137
+ params: list[Any] = [run["run_id"]]
138
+
139
+ if event_type:
140
+ placeholders = ",".join("?" for _ in event_type)
141
+ where.append(f"e.event_type IN ({placeholders})")
142
+ params.extend(event_type)
143
+
144
+ if source_project:
145
+ where.append("(e.source_project = ? OR e.target_project = ?)")
146
+ params.extend([source_project, source_project])
147
+
148
+ if cursor:
149
+ decoded = _decode_cursor(cursor)
150
+ if decoded:
151
+ observed_after, last_event_id = decoded
152
+ where.append("(e.observed_at < ? OR (e.observed_at = ? AND e.event_id > ?))")
153
+ params.extend([observed_after, observed_after, last_event_id])
154
+
155
+ query = (
156
+ "SELECT event_id, run_id, cycle_key, observed_at, derived_from_state_at, "
157
+ " event_type, schema_version, source_system, source_project, "
158
+ " target_project, program_key, source_ref, title, summary, "
159
+ " evidence_json, evidence_hash "
160
+ "FROM brain_digest_events e "
161
+ f"WHERE {' AND '.join(where)} "
162
+ "ORDER BY e.observed_at DESC, e.event_id ASC LIMIT ?"
163
+ )
164
+ params.append(over_fetch)
165
+
166
+ rows = await (await db.execute(query, params)).fetchall()
167
+
168
+ items: list[DigestEvent | DigestEventRedacted] = []
169
+ next_cursor: str | None = None
170
+ redacted_count = 0
171
+ page_rows = rows[:limit]
172
+ if len(rows) > limit:
173
+ last = page_rows[-1]
174
+ next_cursor = _encode_cursor(last["observed_at"], last["event_id"])
175
+
176
+ for row in page_rows:
177
+ if not _is_visible(visible, row["source_project"], row["target_project"]):
178
+ redacted_count += 1
179
+ items.append(
180
+ DigestEventRedacted(
181
+ event_id=row["event_id"],
182
+ cycle_key=row["cycle_key"],
183
+ event_type=row["event_type"],
184
+ )
185
+ )
186
+ continue
187
+ try:
188
+ evidence = json.loads(row["evidence_json"] or "{}")
189
+ except json.JSONDecodeError:
190
+ evidence = {}
191
+ items.append(
192
+ DigestEvent(
193
+ event_id=row["event_id"],
194
+ run_id=row["run_id"],
195
+ cycle_key=row["cycle_key"],
196
+ observed_at=_parse_iso(row["observed_at"]),
197
+ derived_from_state_at=_parse_iso(row["derived_from_state_at"]),
198
+ event_type=row["event_type"],
199
+ schema_version=row["schema_version"],
200
+ source_system=row["source_system"],
201
+ source_project=row["source_project"],
202
+ target_project=row["target_project"],
203
+ program_key=row["program_key"],
204
+ source_ref=row["source_ref"],
205
+ title=row["title"],
206
+ summary=row["summary"] or "",
207
+ evidence=evidence,
208
+ evidence_hash=row["evidence_hash"],
209
+ )
210
+ )
211
+
212
+ return EventsListResponse(
213
+ items=items,
214
+ next_cursor=next_cursor,
215
+ cycle_key=run["cycle_key"],
216
+ run_id=run["run_id"],
217
+ redacted_count=redacted_count,
218
+ total_returned=len(items),
219
+ )
220
+
221
+
222
+ __all__ = ["list_events_for_cycle", "MAX_LIMIT", "DEFAULT_LIMIT"]