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,440 @@
1
+ # v1.3.0 - 2026-04-24 - Project.degree = SUM(weight) outgoing aggregate (was COUNT)
2
+ # v1.2.0 - 2026-04-24 - Q2 estesa con sat_items CTE: top-12 SatelliteItem clickable
3
+ # v1.1.0 - 2026-04-24 - SatelliteSummary: Q2 ritorna count + latest_at
4
+ # v1.0.0 - 2026-04-24 - Cosmo canvas data aggregator
5
+ """Service layer per GET /api/v1/graph/cosmo.
6
+
7
+ 3 query SQL (projects / satellites windowed / aggregated edges) dentro
8
+ una single transaction BEGIN DEFERRED → COMMIT per snapshot isolation vs
9
+ populator che gira in parallelo (M-BE-10 piano).
10
+
11
+ `program` NON e' nel metadata KG (populate_project_nodes.py non lo scrive):
12
+ lo joiniamo live da `programs.yaml` tramite _load_programs_map() con cache
13
+ mtime-based (M-BE-03 opzione A).
14
+
15
+ v1.2.0: Q2 estesa con CTE `sat_items` che ritorna top-12 artifact per
16
+ (project, kind) ordinati per latest_at DESC. Ogni item porta id/title/path/
17
+ latest_at/importance per file-dot cliccabili FE. Performance: subquery
18
+ `importance` O(N_items × edge_lookup) — assume `idx_graph_edges_target`
19
+ su `graph_edges.target_id` (verificato in produzione 2026-04-24). Cap teorico
20
+ payload: 71 project × 8 kind × 12 item ≈ 6816 row × ~150 byte ≈ ~1MB.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import logging
26
+ import re
27
+ import threading
28
+ from pathlib import Path
29
+
30
+ import aiosqlite
31
+ import yaml
32
+
33
+ from core.api.models import UserInfo
34
+ from core.api.models.graph_cosmo import (
35
+ Edge,
36
+ GraphCosmoOut,
37
+ Project,
38
+ SatelliteItem,
39
+ SatelliteSummary,
40
+ )
41
+ from core.api.visibility import get_visible_projects
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+ # Keep in sync con scripts/populate_project_nodes.py v1.3.0: qualsiasi char
46
+ # fuori da [a-zA-Z0-9_\-.] viene sostituito con `_` nel node_id del progetto.
47
+ # Serve per matchare `graph_nodes.project_id` (che puo' contenere `&`, es.
48
+ # cartelle `c&i-tool`) al `project:artifact:<slug>` super-nodo reale (`c_i-tool`).
49
+ _SLUG_SAFE_RE = re.compile(r"[^a-zA-Z0-9_\-.]")
50
+
51
+
52
+ def _safe_project_slug(raw: str) -> str:
53
+ """Sanitize project slug per costruire node_id stabili (c&i-tool → c_i-tool)."""
54
+ return _SLUG_SAFE_RE.sub("_", raw)
55
+
56
+ # Kind whitelist per satellites: derivato dal prefix del node_id (H-11 piano).
57
+ # Schema CHECK di `graph_nodes.type` esclude plan/brainstorm/audit/research,
58
+ # quindi la classificazione vera sta nel prefix `{kind}:artifact:...`.
59
+ ALLOWED_KINDS: tuple[str, ...] = (
60
+ "plan",
61
+ "brainstorm",
62
+ "solution",
63
+ "audit",
64
+ "research",
65
+ "handoff",
66
+ "task",
67
+ "learning",
68
+ )
69
+
70
+ # Generic program baseline shipped in core (tenant-agnostic). Tenant/customer
71
+ # program names are NOT hardcoded here: they come from programs.yaml (local,
72
+ # not mirrored) and are unioned into the valid set by _valid_programs().
73
+ _KNOWN_PROGRAMS_BASELINE: frozenset[str] = frozenset({"marvis", "personal"})
74
+
75
+ # --- programs.yaml cache (mtime-based, thread-safe) ---
76
+
77
+ _programs_map_cache: dict[str, str] | None = None
78
+ _programs_map_mtime: float = 0.0
79
+ _programs_map_lock = threading.Lock()
80
+ _PROGRAMS_YAML_PATH = Path.home() / "workspace" / "programs.yaml"
81
+
82
+
83
+ def _load_programs_map() -> dict[str, str]:
84
+ """Carica programs.yaml → {project_slug: program_name} con cache mtime.
85
+
86
+ Formato programs.yaml:
87
+ <program_name>:
88
+ projects:
89
+ - <slug1>
90
+ - <slug2>
91
+
92
+ Ritorna dict che mappa ogni slug al program di appartenenza. Slug non
93
+ listati in programs.yaml → non presenti nel dict (default 'personal'
94
+ applicato dal caller tramite _normalize_program).
95
+ """
96
+ global _programs_map_cache, _programs_map_mtime
97
+
98
+ try:
99
+ current_mtime = _PROGRAMS_YAML_PATH.stat().st_mtime
100
+ except FileNotFoundError:
101
+ logger.warning("cosmo: programs.yaml non trovato a %s", _PROGRAMS_YAML_PATH)
102
+ return {}
103
+
104
+ with _programs_map_lock:
105
+ if _programs_map_cache is not None and current_mtime == _programs_map_mtime:
106
+ return _programs_map_cache
107
+
108
+ try:
109
+ raw = yaml.safe_load(_PROGRAMS_YAML_PATH.read_text(encoding="utf-8")) or {}
110
+ except Exception: # noqa: BLE001 — yaml errori vari
111
+ logger.exception("cosmo: parse programs.yaml fallito")
112
+ return _programs_map_cache or {}
113
+
114
+ result: dict[str, str] = {}
115
+ for prog_name, prog_data in raw.items():
116
+ if not isinstance(prog_data, dict):
117
+ continue
118
+ projects = prog_data.get("projects") or []
119
+ for slug in projects:
120
+ if isinstance(slug, str):
121
+ result[slug] = prog_name
122
+
123
+ _programs_map_cache = result
124
+ _programs_map_mtime = current_mtime
125
+ return result
126
+
127
+
128
+ def _valid_programs() -> frozenset[str]:
129
+ """Insieme dei program validi: baseline generico + nomi da programs.yaml.
130
+
131
+ Il baseline (`marvis`, `personal`) e' tenant-agnostico e vive in core. Ogni
132
+ program definito in programs.yaml (locale, non mirrorato) e' considerato
133
+ valido — cosi' nessun nome tenant/cliente e' hardcoded in core ma il
134
+ comportamento live resta identico. La punteggiatura `&` viene normalizzata
135
+ (`c&i` → `ci`) come per _normalize_program.
136
+ """
137
+ extra = {_strip_program_punctuation(name) for name in _load_programs_map().values()}
138
+ return _KNOWN_PROGRAMS_BASELINE | extra
139
+
140
+
141
+ def _strip_program_punctuation(raw: str) -> str:
142
+ """Normalizza il nome program (punteggiatura): `c&i` → `ci`."""
143
+ return raw.replace("&", "")
144
+
145
+
146
+ def _normalize_program(raw: str | None) -> str:
147
+ """Normalizza program name a un valore valido (baseline + programs.yaml).
148
+
149
+ Mappa esplicita:
150
+ - 'c&i' → 'ci' (rimozione punteggiatura, coerente con i nomi mirror FE)
151
+ - None / non riconosciuto → 'personal' (con warning)
152
+ """
153
+ if not raw:
154
+ logger.warning("cosmo: project missing program, defaulting to 'personal'")
155
+ return "personal"
156
+ normalized = _strip_program_punctuation(raw)
157
+ if normalized not in _valid_programs():
158
+ logger.warning("cosmo: unknown program %r, defaulting to 'personal'", raw)
159
+ return "personal"
160
+ return normalized
161
+
162
+
163
+ async def fetch_cosmo_graph(
164
+ db: aiosqlite.Connection, user: UserInfo
165
+ ) -> GraphCosmoOut:
166
+ """Ritorna il bundle {projects, edges} per il canvas cosmo.
167
+
168
+ Snapshot isolation via BEGIN DEFERRED — evita che populator in corso
169
+ causi conteggi disallineati tra le 3 query.
170
+
171
+ RBAC: projects filtrati via get_visible_projects (None = tutti, set =
172
+ whitelist). Edges scartati se un endpoint non e' nel whitelist (post-filter
173
+ Python).
174
+ """
175
+ # Dedup row_factory: il caller (get_db) lo setta gia' ma per sicurezza.
176
+ db.row_factory = aiosqlite.Row
177
+
178
+ # Transazione DEFERRED: nessun lock prima del primo SELECT. Rilascia in
179
+ # finally per garantire COMMIT/ROLLBACK anche su eccezioni query.
180
+ await db.execute("BEGIN DEFERRED")
181
+ try:
182
+ # --- Query 1: project super-nodi + degree ---
183
+ # v1.4.0: degree = SUM(weight) INCOMING aggregate (chi e' citato/dipeso
184
+ # da molti = importante = bubble grande). v1.3.0 usava OUTGOING ma
185
+ # premiava i citatori (small project con depends_on forti emergono al
186
+ # top, marvisx scompare perche' la sua weight outgoing e' normalizzata
187
+ # per sqrt(2700 artifact)). v1.4.0 corregge: marvisx riceve mention da
188
+ # tutti -> incoming sum alto -> bubble grande. Tool-bolletta cita
189
+ # marvisx ma non e' citato da nessuno -> piccolo.
190
+ q_projects = """
191
+ SELECT gn.id AS node_id,
192
+ gn.name AS slug,
193
+ (SELECT COALESCE(SUM(CAST(json_extract(ge.metadata, '$.weight') AS REAL)), 0.0)
194
+ FROM graph_edges ge
195
+ WHERE ge.target_id = gn.id
196
+ AND ge.relation IN (
197
+ 'mentions','depends_on','refers_to','shares_tag','similar_to'
198
+ )
199
+ AND json_extract(ge.metadata, '$.aggregated') = 1
200
+ AND ge.valid_until IS NULL
201
+ ) AS degree
202
+ FROM graph_nodes gn
203
+ WHERE gn.id LIKE 'project:artifact:%'
204
+ AND gn.type = 'project'
205
+ AND gn.deprecated_at IS NULL
206
+ ORDER BY degree DESC, gn.id ASC
207
+ """
208
+ async with db.execute(q_projects) as cur:
209
+ project_rows: list[aiosqlite.Row] = await cur.fetchall()
210
+
211
+ # --- Query 2: satellites top-8 per project via graph_nodes.project_id ---
212
+ # Gli edge work-chain (produces/contains/describes/documents) non sono
213
+ # popolati in produzione (0 righe). I nodi artifact pero' hanno
214
+ # `project_id` valorizzato dal populator (migration 073 + ingest path),
215
+ # quindi deriviamo i satellites direttamente dalla colonna — senza
216
+ # passare per `graph_edges`. Una riga per (project_id, kind) con:
217
+ # - count = COUNT(*) artifact totali del kind nel project (per file-dot)
218
+ # - latest_at = MAX(last_seen_at, created_at) ISO recency
219
+ # ROW_NUMBER ordinato per latest_at DESC, kind ASC tiebreak. Cap a 8
220
+ # kind distinti per project. Il service ricostruira' il node_id
221
+ # `project:artifact:<slug>` in fase di aggregazione.
222
+ #
223
+ # v1.2.0: aggiunta CTE `sat_items` con top-12 artifact per (project,
224
+ # kind). Performance critica: la subquery `importance` (incoming edges)
225
+ # gira ~6800 volte. Mitigazione: indice `idx_graph_edges_target`
226
+ # esistente su `graph_edges.target_id`. Stima dev DB ~500ms; produzione
227
+ # da monitorare con SQLITE_TIMER se si osserva degrado.
228
+ kinds_placeholders = ",".join("?" * len(ALLOWED_KINDS))
229
+ q_satellites = f"""
230
+ WITH sat_counts AS (
231
+ SELECT gn.project_id AS project_slug,
232
+ gn.type AS kind,
233
+ COUNT(*) AS count,
234
+ MAX(COALESCE(gn.last_seen_at, gn.created_at)) AS latest_at
235
+ FROM graph_nodes gn
236
+ WHERE gn.project_id IS NOT NULL
237
+ AND gn.deprecated_at IS NULL
238
+ AND gn.type IN ({kinds_placeholders})
239
+ GROUP BY gn.project_id, gn.type
240
+ ),
241
+ sat_ranked AS (
242
+ SELECT project_slug, kind, count, latest_at,
243
+ ROW_NUMBER() OVER (
244
+ PARTITION BY project_slug
245
+ ORDER BY latest_at DESC, kind ASC
246
+ ) AS rn
247
+ FROM sat_counts
248
+ ),
249
+ sat_items AS (
250
+ SELECT gn.project_id AS project_slug,
251
+ gn.type AS kind,
252
+ gn.id AS item_id,
253
+ gn.name AS title,
254
+ gn.file_path AS path,
255
+ COALESCE(gn.last_seen_at, gn.created_at) AS latest_at,
256
+ (SELECT COUNT(*) FROM graph_edges ge
257
+ WHERE ge.target_id = gn.id
258
+ AND ge.relation IN (
259
+ 'cites','mentions','refers_to',
260
+ 'applies_to','similar_to'
261
+ )
262
+ AND ge.valid_until IS NULL
263
+ ) AS importance,
264
+ ROW_NUMBER() OVER (
265
+ PARTITION BY gn.project_id, gn.type
266
+ ORDER BY COALESCE(gn.last_seen_at, gn.created_at) DESC,
267
+ gn.id ASC
268
+ ) AS rn
269
+ FROM graph_nodes gn
270
+ WHERE gn.project_id IS NOT NULL
271
+ AND gn.deprecated_at IS NULL
272
+ AND gn.type IN ({kinds_placeholders})
273
+ )
274
+ SELECT sr.project_slug, sr.kind, sr.count, sr.latest_at,
275
+ (
276
+ SELECT json_group_array(json_object(
277
+ 'id', si.item_id,
278
+ 'title', si.title,
279
+ 'path', si.path,
280
+ 'latest_at', si.latest_at,
281
+ 'importance', si.importance
282
+ ) ORDER BY si.rn ASC)
283
+ FROM sat_items si
284
+ WHERE si.project_slug = sr.project_slug
285
+ AND si.kind = sr.kind
286
+ AND si.rn <= 12
287
+ ) AS items_json
288
+ FROM sat_ranked sr
289
+ WHERE sr.rn <= 8
290
+ """
291
+ # Doppio param-set: il placeholder appare 2 volte (sat_counts + sat_items)
292
+ sat_query_params: list[str] = list(ALLOWED_KINDS) + list(ALLOWED_KINDS)
293
+ async with db.execute(q_satellites, sat_query_params) as cur:
294
+ sat_rows: list[aiosqlite.Row] = await cur.fetchall()
295
+
296
+ # --- Query 3: edges cross-project aggregated ---
297
+ # JOIN graph_nodes per leggere `name` (raw slug, es. `c&i-master`) invece
298
+ # di `substr(source_id, 18)` che restituirebbe il safe slug del node_id
299
+ # (`c_i-master`). Q1 emette `Project.slug = gn.name` (raw): se gli edge
300
+ # usassero substr (safe), su FE `activeSlug = "c&i-master"` non
301
+ # matcherebbe mai `Edge.source = "c_i-master"` → no edge highlight su
302
+ # hover (bug Cosmo canvas, sessione 162). weight e' dentro metadata JSON.
303
+ # aggregated flag letto via json_extract (robust vs varianti di
304
+ # spacing nel JSON seriale: "aggregated":true vs "aggregated": true).
305
+ # weight: REAL (float) post v1.4 populator. Storicamente era int (count
306
+ # primitive), ora e' un float continuo (decay+normalize). Cast esplicito
307
+ # a REAL per evitare di perdere edge con peso < 1.0 (CAST AS INTEGER
308
+ # tronca 0.4 -> 0 -> filtrato dal `> 0`). FE Zod schema rilassato
309
+ # a `z.number().nonnegative()` di pari passo (consumer accettano float).
310
+ q_edges = """
311
+ SELECT src.name AS slug_a,
312
+ tgt.name AS slug_b,
313
+ ge.relation AS relation,
314
+ CAST(json_extract(ge.metadata, '$.weight') AS REAL) AS weight
315
+ FROM graph_edges ge
316
+ JOIN graph_nodes src ON src.id = ge.source_id
317
+ JOIN graph_nodes tgt ON tgt.id = ge.target_id
318
+ WHERE ge.source_id LIKE 'project:artifact:%'
319
+ AND ge.target_id LIKE 'project:artifact:%'
320
+ AND ge.relation IN ('mentions','depends_on')
321
+ AND json_extract(ge.metadata, '$.aggregated') = 1
322
+ AND ge.valid_until IS NULL
323
+ AND CAST(json_extract(ge.metadata, '$.weight') AS REAL) > 0.0
324
+ """
325
+ async with db.execute(q_edges) as cur:
326
+ edge_rows: list[aiosqlite.Row] = await cur.fetchall()
327
+
328
+ await db.commit()
329
+ except Exception:
330
+ await db.rollback()
331
+ raise
332
+
333
+ # --- Aggregazione satellites per project_id ricostruito ---
334
+ # Q2 restituisce `project_slug` (valore della colonna `gn.project_id`, es.
335
+ # 'marvisx'). Ricostruiamo la chiave completa `project:artifact:<slug>` per
336
+ # allinearla al `node_id` dei project super-nodi di Q1. Applichiamo la
337
+ # stessa `_safe_project_slug` del populator: altrimenti slug con `&` (es.
338
+ # `c&i-tool`) non matchano mai i super-nodi (`project:artifact:c_i-tool`)
339
+ # e i satelliti/edges dei progetti c&i-* restano orfani.
340
+ #
341
+ # v1.1.0: Q2 ritorna anche `count` (artifact totali del kind, per
342
+ # file-dot semantici N=min(count,6)) + `latest_at` (recency ISO, per
343
+ # accent burning su primo dot solo se < 7 giorni).
344
+ # v1.2.0: Q2 ritorna anche `items_json` (lista top-12 SatelliteItem
345
+ # serializzata via json_group_array). Deserializza con json.loads e
346
+ # popola SatelliteSummary.items.
347
+ satellites_by_pid: dict[str, list[SatelliteSummary]] = {}
348
+ for r in sat_rows:
349
+ slug_raw = r["project_slug"]
350
+ kind = r["kind"]
351
+ if kind not in ALLOWED_KINDS: # defense-in-depth vs schema drift
352
+ continue
353
+ pid = f"project:artifact:{_safe_project_slug(slug_raw)}"
354
+ items_raw = r["items_json"] or "[]"
355
+ try:
356
+ items_decoded = json.loads(items_raw)
357
+ except (TypeError, ValueError):
358
+ logger.warning("cosmo: items_json parse failed for pid=%s kind=%s", pid, kind)
359
+ items_decoded = []
360
+ items_out: list[SatelliteItem] = []
361
+ for it in items_decoded:
362
+ if not isinstance(it, dict):
363
+ continue
364
+ item_id = it.get("id")
365
+ title = it.get("title")
366
+ latest_at_item = it.get("latest_at")
367
+ if not isinstance(item_id, str) or not isinstance(title, str):
368
+ continue
369
+ if not isinstance(latest_at_item, str):
370
+ continue
371
+ path_val = it.get("path")
372
+ items_out.append(
373
+ SatelliteItem(
374
+ id=item_id,
375
+ title=title,
376
+ latest_at=latest_at_item,
377
+ importance=int(it.get("importance") or 0),
378
+ path=path_val if isinstance(path_val, str) else None,
379
+ )
380
+ )
381
+ satellites_by_pid.setdefault(pid, []).append(
382
+ SatelliteSummary(
383
+ kind=kind, # type: ignore[arg-type] # filtered above
384
+ count=int(r["count"] or 0),
385
+ latest_at=r["latest_at"],
386
+ items=items_out,
387
+ )
388
+ )
389
+
390
+ # --- Program join via programs.yaml ---
391
+ programs_map = _load_programs_map()
392
+
393
+ # --- Visibility filter ---
394
+ visible = await get_visible_projects(db, user)
395
+
396
+ projects_out: list[Project] = []
397
+ for r in project_rows:
398
+ slug: str = r["slug"]
399
+ if visible is not None and slug not in visible:
400
+ continue
401
+ program = _normalize_program(programs_map.get(slug))
402
+ sats = satellites_by_pid.get(r["node_id"], [])
403
+ projects_out.append(
404
+ Project(
405
+ slug=slug,
406
+ program=program, # type: ignore[arg-type] # Literal runtime-checked
407
+ degree=float(r["degree"] or 0.0),
408
+ satellites=sats,
409
+ )
410
+ )
411
+
412
+ # --- Edge filter + shape ---
413
+ visible_slugs = {p.slug for p in projects_out} if visible is not None else None
414
+ edges_out: list[Edge] = []
415
+ for r in edge_rows:
416
+ a: str = r["slug_a"]
417
+ b: str = r["slug_b"]
418
+ if visible_slugs is not None and (a not in visible_slugs or b not in visible_slugs):
419
+ continue
420
+ edges_out.append(
421
+ Edge(
422
+ source=a,
423
+ target=b,
424
+ relation=r["relation"], # type: ignore[arg-type] # CHECK in SQL
425
+ weight=float(r["weight"] or 0.0),
426
+ )
427
+ )
428
+
429
+ return GraphCosmoOut(projects=projects_out, edges=edges_out)
430
+
431
+
432
+ # --- Test helpers (invalidazione cache) ---
433
+
434
+
435
+ def _reset_programs_cache_for_tests() -> None:
436
+ """Reset programs.yaml cache — solo per unit test."""
437
+ global _programs_map_cache, _programs_map_mtime
438
+ with _programs_map_lock:
439
+ _programs_map_cache = None
440
+ _programs_map_mtime = 0.0