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,632 @@
1
+ # v1.0.0 - 2026-05-28 - S-Transmute RI-7 layer 2: in-place KG index of an arbitrary repo
2
+ """In-place KG indexing of a transmuted project's source code (RI-7 layer 2).
3
+
4
+ Layer 1 (``core/cli/_transmute.py``) scaffolds the Marvis metadata dir and writes
5
+ a ``.marvis-transmute.yaml`` manifest whose ``source_roots[]`` point — in-place,
6
+ never copied — at the real source code. This layer reads that manifest and feeds
7
+ the source into the existing tree-sitter populator (``scripts/ast_parser``) so a
8
+ *foreign* repo gets the same calls/imports/defines graph the marvisx monorepo has,
9
+ plus a chunk-per-symbol code embedding.
10
+
11
+ Two hard guarantees carried over from D3 (regola ferrea, non-distruttiva):
12
+
13
+ 1. **READ-ONLY on the source.** Nothing in this module ever opens a source file
14
+ for writing. The only writes are KG rows in the Marvis DB. We re-hash the
15
+ source roots before and after the index pass and abort if a single byte moved.
16
+ 2. **File-discovery security gate** (RI-7): a secrets blocklist (always on, never
17
+ trusts only ``.gitignore``), ``follow_symlinks=False``, ``.gitignore`` respect
18
+ with a ``.marvisindex`` override, binary skip (extension + null-byte sniff),
19
+ noise-dir skip, a per-file size cap and a file-count cap (both env-overridable)
20
+ so a 100k-file repo can't exhaust RAM at embed time.
21
+
22
+ The id scheme is inherited from the populator: ``{py|ts}:{kind}:{qualified_name}``
23
+ tagged with ``project_id=<slug>``. Ids are content/qualified-name addressed, NOT
24
+ absolute paths, so moving a file inside the repo does not orphan its nodes — the
25
+ qualified name (module path) is the same and ``project_id`` keeps the scope.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import hashlib
30
+ import logging
31
+ import os
32
+ import sqlite3
33
+ import struct
34
+ import subprocess
35
+ from dataclasses import dataclass, field
36
+ from pathlib import Path
37
+ from typing import Any, Callable, Iterable, Iterator, Sequence
38
+
39
+ logger = logging.getLogger("marvisx.index_source")
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Security gate — file discovery
43
+ # ---------------------------------------------------------------------------
44
+
45
+ # Secrets blocklist: matched by name (case-insensitive) and a few glob-ish
46
+ # suffixes. ALWAYS active — we never rely on `.gitignore` alone to keep a key
47
+ # out of the index (the OSS leak post-mortem, learning fa749d38: allowlist green
48
+ # while real leaks merely exempted).
49
+ _SECRET_EXACT = frozenset(
50
+ {
51
+ ".env",
52
+ ".env.local",
53
+ ".env.production",
54
+ ".env.development",
55
+ ".netrc",
56
+ ".npmrc",
57
+ ".pypirc",
58
+ ".htpasswd",
59
+ "credentials",
60
+ "credentials.json",
61
+ "secrets.yaml",
62
+ "secrets.yml",
63
+ "id_rsa",
64
+ "id_ed25519",
65
+ "id_ecdsa",
66
+ "id_dsa",
67
+ }
68
+ )
69
+ _SECRET_PREFIXES = ("id_", "credentials", ".env", "secret")
70
+ _SECRET_SUFFIXES = (
71
+ ".pem",
72
+ ".key",
73
+ ".p12",
74
+ ".pfx",
75
+ ".keystore",
76
+ ".jks",
77
+ ".crt",
78
+ ".cer",
79
+ ".der",
80
+ ".asc",
81
+ ".gpg",
82
+ )
83
+
84
+ # Noise directories never walked (build artifacts, caches, deps, VCS).
85
+ _SKIP_DIRS = frozenset(
86
+ {
87
+ ".git",
88
+ ".hg",
89
+ ".svn",
90
+ "node_modules",
91
+ "__pycache__",
92
+ ".venv",
93
+ "venv",
94
+ "env",
95
+ "dist",
96
+ "build",
97
+ "target",
98
+ ".mypy_cache",
99
+ ".pytest_cache",
100
+ ".ruff_cache",
101
+ ".tox",
102
+ ".idea",
103
+ ".vscode",
104
+ ".next",
105
+ ".cache",
106
+ "coverage",
107
+ ".gradle",
108
+ }
109
+ )
110
+
111
+ # Source extensions we currently parse (tree-sitter Python + TypeScript).
112
+ _SOURCE_SUFFIXES = (".py", ".ts", ".tsx")
113
+
114
+ # Binary extensions skipped outright (the null-byte sniff is the real backstop).
115
+ _BINARY_SUFFIXES = frozenset(
116
+ {
117
+ ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".webp", ".svg",
118
+ ".pdf", ".zip", ".gz", ".tar", ".tgz", ".bz2", ".xz", ".7z", ".rar",
119
+ ".so", ".dylib", ".dll", ".a", ".o", ".obj", ".class", ".pyc", ".pyo",
120
+ ".wasm", ".bin", ".exe", ".woff", ".woff2", ".ttf", ".eot", ".otf",
121
+ ".mp3", ".mp4", ".mov", ".avi", ".wav", ".flac", ".ogg",
122
+ ".db", ".sqlite", ".sqlite3",
123
+ }
124
+ )
125
+
126
+ _DEFAULT_MAX_FILE_BYTES = 1_000_000 # ~1MB per-file cap (RI-7)
127
+ _DEFAULT_MAX_FILES = 20_000 # file-count cap; a huge repo can't blow up RAM
128
+ _NULL_SNIFF_BYTES = 8192 # first 8KB scanned for a NUL byte
129
+
130
+
131
+ def _max_file_bytes() -> int:
132
+ return _int_env("MARVIS_INDEX_MAX_FILE_BYTES", _DEFAULT_MAX_FILE_BYTES)
133
+
134
+
135
+ def _max_files() -> int:
136
+ return _int_env("MARVIS_INDEX_MAX_FILES", _DEFAULT_MAX_FILES)
137
+
138
+
139
+ def _int_env(name: str, default: int) -> int:
140
+ raw = os.environ.get(name)
141
+ if not raw:
142
+ return default
143
+ try:
144
+ return int(raw)
145
+ except (TypeError, ValueError):
146
+ return default
147
+
148
+
149
+ def is_secret_name(name: str) -> bool:
150
+ """True if a filename looks like a secret (blocklist, case-insensitive)."""
151
+ lower = name.lower()
152
+ if lower in _SECRET_EXACT:
153
+ return True
154
+ if any(lower.endswith(suf) for suf in _SECRET_SUFFIXES):
155
+ return True
156
+ if any(lower.startswith(pre) for pre in _SECRET_PREFIXES):
157
+ return True
158
+ return False
159
+
160
+
161
+ def _looks_binary(path: Path) -> bool:
162
+ """Extension check first, then a null-byte sniff of the first 8KB."""
163
+ if path.suffix.lower() in _BINARY_SUFFIXES:
164
+ return True
165
+ try:
166
+ with path.open("rb") as fh:
167
+ return b"\x00" in fh.read(_NULL_SNIFF_BYTES)
168
+ except OSError:
169
+ return True # unreadable → treat as binary (skip), never crash
170
+
171
+
172
+ def _gitignored(root: Path) -> set[str]:
173
+ """Return repo-relative POSIX paths git would ignore under ``root``.
174
+
175
+ Best-effort: if ``root`` is not a git repo (or git is absent) we return an
176
+ empty set — the secrets blocklist + noise-dir skip still apply, so we never
177
+ *fail open* on the things that matter. ``.marvisindex`` (if present, listing
178
+ one repo-relative path per line) overrides the ignore for generated code that
179
+ is genuinely useful to index.
180
+ """
181
+ if not (root / ".git").exists():
182
+ return set()
183
+ try:
184
+ out = subprocess.check_output(
185
+ ["git", "ls-files", "--others", "--ignored", "--exclude-standard", "-z"],
186
+ cwd=root,
187
+ text=True,
188
+ stderr=subprocess.DEVNULL,
189
+ )
190
+ except (subprocess.CalledProcessError, FileNotFoundError, OSError):
191
+ return set()
192
+ ignored = {p for p in out.split("\0") if p}
193
+ override = root / ".marvisindex"
194
+ if override.is_file():
195
+ try:
196
+ for line in override.read_text(encoding="utf-8").splitlines():
197
+ rel = line.strip()
198
+ if rel and not rel.startswith("#"):
199
+ ignored.discard(rel)
200
+ except OSError:
201
+ pass
202
+ return ignored
203
+
204
+
205
+ @dataclass(slots=True)
206
+ class DiscoveryStats:
207
+ """Counters describing why files were kept or dropped (observability)."""
208
+
209
+ walked: int = 0
210
+ kept: int = 0
211
+ skipped_secret: int = 0
212
+ skipped_binary: int = 0
213
+ skipped_symlink: int = 0
214
+ skipped_too_large: int = 0
215
+ skipped_not_source: int = 0
216
+ skipped_gitignored: int = 0
217
+ hit_file_cap: bool = False
218
+
219
+
220
+ def iter_source_files(
221
+ root: Path,
222
+ *,
223
+ exclude: Sequence[str] = (),
224
+ stats: DiscoveryStats | None = None,
225
+ ) -> Iterator[Path]:
226
+ """Yield in-scope source files under ``root`` (the RI-7 security gate).
227
+
228
+ Read-only: only ``stat``/``open('rb')`` for sniffing. ``follow_symlinks`` is
229
+ False everywhere — a symlink (file OR dir) is never traversed, so a link
230
+ escaping the root can't smuggle outside files into the index. Yields absolute
231
+ paths in a deterministic (sorted) order.
232
+ """
233
+ root = root.resolve()
234
+ st = stats if stats is not None else DiscoveryStats()
235
+ extra_skip = {e.strip("/").split("/")[-1] for e in exclude if e}
236
+ skip_dirs = _SKIP_DIRS | extra_skip
237
+ ignored = _gitignored(root)
238
+ max_bytes = _max_file_bytes()
239
+ cap = _max_files()
240
+
241
+ stack: list[Path] = [root]
242
+ while stack:
243
+ current = stack.pop()
244
+ try:
245
+ entries = sorted(current.iterdir(), key=lambda p: p.name, reverse=True)
246
+ except (PermissionError, OSError):
247
+ continue
248
+ for entry in entries:
249
+ # follow_symlinks=False: a symlink is dropped, dir or file.
250
+ if entry.is_symlink():
251
+ st.skipped_symlink += 1
252
+ continue
253
+ if entry.is_dir():
254
+ if entry.name in skip_dirs:
255
+ continue
256
+ stack.append(entry)
257
+ continue
258
+ if not entry.is_file():
259
+ continue
260
+ st.walked += 1
261
+ name = entry.name
262
+ if is_secret_name(name):
263
+ st.skipped_secret += 1
264
+ continue
265
+ if entry.suffix.lower() not in _SOURCE_SUFFIXES:
266
+ st.skipped_not_source += 1
267
+ continue
268
+ try:
269
+ rel = entry.resolve().relative_to(root).as_posix()
270
+ except ValueError:
271
+ rel = name
272
+ if rel in ignored:
273
+ st.skipped_gitignored += 1
274
+ continue
275
+ try:
276
+ size = entry.stat().st_size
277
+ except OSError:
278
+ continue
279
+ if size > max_bytes:
280
+ st.skipped_too_large += 1
281
+ continue
282
+ if _looks_binary(entry):
283
+ st.skipped_binary += 1
284
+ continue
285
+ if st.kept >= cap:
286
+ st.hit_file_cap = True
287
+ logger.warning(
288
+ "file-count cap (%d) reached under %s; remaining files skipped "
289
+ "(raise MARVIS_INDEX_MAX_FILES to index more).",
290
+ cap,
291
+ root,
292
+ )
293
+ return
294
+ st.kept += 1
295
+ yield entry
296
+
297
+
298
+ # ---------------------------------------------------------------------------
299
+ # Per-symbol code embeddings (chunk-per-symbol, AST boundaries)
300
+ # ---------------------------------------------------------------------------
301
+
302
+
303
+ def _read_lines(path: Path) -> list[str]:
304
+ try:
305
+ return path.read_text(encoding="utf-8", errors="replace").splitlines()
306
+ except OSError:
307
+ return []
308
+
309
+
310
+ def build_symbol_text(node: dict, lines: Sequence[str], *, max_lines: int = 200) -> str:
311
+ """Assemble the embedding text for one function/method node.
312
+
313
+ Chunk-per-symbol (NOT a blind sliding window): the populator already gives us
314
+ the symbol's start line; we take the body up to the next symbol or
315
+ ``max_lines`` (cAST-style truncation budget). The qualified name + signature
316
+ line are PREPENDED as context enrichment, which lifts small-model retrieval.
317
+ """
318
+ qn = node.get("qualified_name") or node.get("name") or ""
319
+ start = (node.get("line_number") or 1) - 1
320
+ end = min(len(lines), start + max_lines)
321
+ body = "\n".join(lines[start:end]) if 0 <= start < len(lines) else ""
322
+ header = f"# symbol: {qn}"
323
+ return f"{header}\n{body}".strip()
324
+
325
+
326
+ # An embedder is any callable: list[str] -> list[list[float]]. The default uses
327
+ # the in-process Granite client; tests inject a fake to avoid a model load.
328
+ Embedder = Callable[[list[str]], list[list[float]]]
329
+
330
+
331
+ def _default_embedder() -> Embedder:
332
+ """Lazily build a Granite-backed embedder (never loaded at import time)."""
333
+ from core.api.services.embedding_internal import GraniteEmbeddingClient
334
+
335
+ client = GraniteEmbeddingClient()
336
+
337
+ def _embed(texts: list[str]) -> list[list[float]]:
338
+ return client.embed_texts(texts, input_type="document")
339
+
340
+ return _embed
341
+
342
+
343
+ def _pack_vector(vec: Sequence[float]) -> bytes:
344
+ return struct.pack(f"<{len(vec)}f", *(float(x) for x in vec))
345
+
346
+
347
+ def _content_hash(text: str) -> str:
348
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
349
+
350
+
351
+ # ---------------------------------------------------------------------------
352
+ # Orchestrator
353
+ # ---------------------------------------------------------------------------
354
+
355
+
356
+ @dataclass(slots=True)
357
+ class IndexResult:
358
+ slug: str
359
+ db_path: str
360
+ roots: list[str] = field(default_factory=list)
361
+ files_indexed: int = 0
362
+ n_nodes: int = 0
363
+ n_edges: int = 0
364
+ n_embeddings: int = 0
365
+ discovery: dict[str, Any] = field(default_factory=dict)
366
+ source_verified_untouched: bool = True
367
+
368
+ def as_dict(self) -> dict[str, Any]:
369
+ return {
370
+ "slug": self.slug,
371
+ "db_path": self.db_path,
372
+ "roots": self.roots,
373
+ "files_indexed": self.files_indexed,
374
+ "n_nodes": self.n_nodes,
375
+ "n_edges": self.n_edges,
376
+ "n_embeddings": self.n_embeddings,
377
+ "discovery": self.discovery,
378
+ "source_verified_untouched": self.source_verified_untouched,
379
+ }
380
+
381
+
382
+ class IndexSourceError(RuntimeError):
383
+ """A manifest/source invariant was violated (read-only, missing roots, ...)."""
384
+
385
+
386
+ def _load_manifest_roots(metadata_dir: Path) -> list[dict[str, Any]]:
387
+ """Read ``source_roots[]`` from the project's ``.marvis-transmute.yaml``."""
388
+ from core.cli import _transmute as tx
389
+
390
+ manifest = tx.load_manifest(metadata_dir)
391
+ if manifest is None:
392
+ raise IndexSourceError(
393
+ f"no {tx.MANIFEST_NAME} in {metadata_dir} — run "
394
+ f"`marvis project import <path> --scaffold` (layer 1) first."
395
+ )
396
+ roots = manifest.get("source_roots") or []
397
+ if not roots:
398
+ raise IndexSourceError(f"{tx.MANIFEST_NAME} has empty source_roots[].")
399
+ return roots
400
+
401
+
402
+ def _hash_root(root: Path) -> dict[str, str]:
403
+ """SHA-256 inventory of a source root (read-only proof-of-untouched)."""
404
+ from core.cli import _transmute as tx
405
+
406
+ return tx.hash_inventory(root)
407
+
408
+
409
+ def index_project_source(
410
+ metadata_dir: Path,
411
+ *,
412
+ slug: str,
413
+ db_path: str,
414
+ embedder: Embedder | None = None,
415
+ embed: bool = True,
416
+ ) -> IndexResult:
417
+ """Index every ``source_roots[]`` of a transmuted project into the KG.
418
+
419
+ Reuses ``scripts/ast_parser.populate_graph_chunked`` parametrized on the root
420
+ + ``project_id=slug`` (content-addressed ids, NOT absolute paths), then writes
421
+ chunk-per-symbol code embeddings. READ-ONLY on the source: every source root
422
+ is hashed before and after; a single changed byte aborts (D3).
423
+ """
424
+ from core.scripts.ast_parser import populate_graph_chunked
425
+
426
+ roots = _load_manifest_roots(metadata_dir)
427
+ result = IndexResult(slug=slug, db_path=db_path)
428
+ agg_stats = DiscoveryStats()
429
+
430
+ for entry in roots:
431
+ raw_path = entry.get("path")
432
+ if not raw_path:
433
+ continue
434
+ root = Path(raw_path).expanduser().resolve()
435
+ if not root.is_dir():
436
+ logger.warning("source_root %s is not a directory; skipping.", root)
437
+ continue
438
+ result.roots.append(str(root))
439
+ exclude = entry.get("exclude") or []
440
+
441
+ before = _hash_root(root)
442
+
443
+ files = list(iter_source_files(root, exclude=exclude, stats=agg_stats))
444
+ result.files_indexed += len(files)
445
+
446
+ if files:
447
+ pop = populate_graph_chunked(
448
+ db_path=db_path,
449
+ python_workers=0, # in-process so the repo_root global override holds
450
+ files=files,
451
+ repo_root=root,
452
+ project_id=slug,
453
+ )
454
+ result.n_nodes += int(pop.get("n_nodes", 0))
455
+ result.n_edges += int(pop.get("n_edges", 0))
456
+
457
+ if embed:
458
+ result.n_embeddings += _embed_symbols(
459
+ db_path=db_path,
460
+ slug=slug,
461
+ root=root,
462
+ files=files,
463
+ embedder=embedder,
464
+ )
465
+
466
+ after = _hash_root(root)
467
+ if after != before:
468
+ result.source_verified_untouched = False
469
+ raise IndexSourceError(
470
+ f"non-destructive invariant violated: source changed under {root}"
471
+ )
472
+
473
+ result.discovery = {
474
+ "walked": agg_stats.walked,
475
+ "kept": agg_stats.kept,
476
+ "skipped_secret": agg_stats.skipped_secret,
477
+ "skipped_binary": agg_stats.skipped_binary,
478
+ "skipped_symlink": agg_stats.skipped_symlink,
479
+ "skipped_too_large": agg_stats.skipped_too_large,
480
+ "skipped_not_source": agg_stats.skipped_not_source,
481
+ "skipped_gitignored": agg_stats.skipped_gitignored,
482
+ "hit_file_cap": agg_stats.hit_file_cap,
483
+ }
484
+ return result
485
+
486
+
487
+ def _function_nodes_for_files(
488
+ files: Sequence[Path], root: Path, slug: str
489
+ ) -> dict[str, list[dict]]:
490
+ """Parse files (no DB) and group function nodes by their source file.
491
+
492
+ Reuses the populator's own parse functions so symbol boundaries / ids match
493
+ exactly what was written to the graph — single source of truth for ids.
494
+ """
495
+ from core.scripts import ast_parser as ap
496
+
497
+ # The parse functions read REPO_ROOT to compute rel_path / ids; set it for
498
+ # this pass and restore it (the populator already does the same internally).
499
+ saved_root, saved_pid = ap.REPO_ROOT, ap.PROJECT_ID
500
+ ap.REPO_ROOT, ap.PROJECT_ID = root.resolve(), slug
501
+ grouped: dict[str, list[dict]] = {}
502
+ try:
503
+ for f in files:
504
+ suffix = f.suffix.lower()
505
+ if suffix == ".py":
506
+ nodes, _ = ap.parse_python_file(str(f))
507
+ elif suffix in (".ts", ".tsx"):
508
+ nodes, _ = ap.parse_typescript_file(str(f))
509
+ else:
510
+ continue
511
+ for n in nodes:
512
+ if n.get("type") != "function" or n.get("metadata", {}).get("stub"):
513
+ continue
514
+ fp = n.get("file_path")
515
+ if not fp:
516
+ continue
517
+ grouped.setdefault(fp, []).append(n)
518
+ finally:
519
+ ap.REPO_ROOT, ap.PROJECT_ID = saved_root, saved_pid
520
+ return grouped
521
+
522
+
523
+ def _embed_symbols(
524
+ *,
525
+ db_path: str,
526
+ slug: str,
527
+ root: Path,
528
+ files: Sequence[Path],
529
+ embedder: Embedder | None,
530
+ ) -> int:
531
+ """Compute + persist chunk-per-symbol code embeddings (DELETE+INSERT per file).
532
+
533
+ Incremental: every source file's prior embeddings are deleted before the
534
+ fresh batch is inserted, so a re-index after an edit never leaves stale
535
+ vectors. Skips re-embedding a symbol whose body hash is unchanged.
536
+ """
537
+ grouped = _function_nodes_for_files(files, root, slug)
538
+ if not grouped:
539
+ return 0
540
+
541
+ embed_fn = embedder or _default_embedder()
542
+ model_id = os.environ.get("MARVIS_EMBED_MODEL", "granite-embedding-97m")
543
+
544
+ conn = sqlite3.connect(db_path)
545
+ written = 0
546
+ try:
547
+ conn.execute("PRAGMA journal_mode=WAL")
548
+ conn.execute("PRAGMA foreign_keys=ON")
549
+ conn.execute("PRAGMA busy_timeout=15000")
550
+ for f in files:
551
+ rel = _rel_for(f, root)
552
+ nodes = grouped.get(rel) or []
553
+ lines = _read_lines(f) if nodes else []
554
+
555
+ # Existing hashes for this file → skip unchanged symbols.
556
+ cur = conn.execute(
557
+ "SELECT node_id, content_hash FROM graph_node_code_embeddings "
558
+ "WHERE source_file = ? AND project_id = ?",
559
+ (rel, slug),
560
+ )
561
+ existing = {row[0]: row[1] for row in cur.fetchall()}
562
+
563
+ current_ids: set[str] = set()
564
+ pending: list[tuple[str, str, str]] = [] # (node_id, text, hash)
565
+ for n in nodes:
566
+ text = build_symbol_text(n, lines)
567
+ if not text:
568
+ continue
569
+ current_ids.add(n["id"])
570
+ h = _content_hash(text)
571
+ if existing.get(n["id"]) == h:
572
+ continue # body unchanged → keep the stored vector (hash-skip)
573
+ pending.append((n["id"], text, h))
574
+
575
+ # Incremental: delete only the symbols that vanished from this file
576
+ # (renamed/removed) so unchanged ones keep their vector; then upsert
577
+ # the changed/new ones. A pure DELETE-all would drop unchanged rows
578
+ # and re-embedding-skip would leave the file empty on a no-op re-run.
579
+ stale_ids = [nid for nid in existing if nid not in current_ids]
580
+
581
+ conn.execute("BEGIN IMMEDIATE")
582
+ try:
583
+ if stale_ids:
584
+ placeholders = ",".join("?" * len(stale_ids))
585
+ conn.execute(
586
+ f"DELETE FROM graph_node_code_embeddings "
587
+ f"WHERE node_id IN ({placeholders})",
588
+ stale_ids,
589
+ )
590
+ if pending:
591
+ vectors = embed_fn([t for _, t, _ in pending])
592
+ rows = []
593
+ for (node_id, _t, h), vec in zip(pending, vectors):
594
+ rows.append(
595
+ (
596
+ node_id,
597
+ slug,
598
+ rel,
599
+ len(vec),
600
+ _pack_vector(vec),
601
+ h,
602
+ model_id,
603
+ )
604
+ )
605
+ conn.executemany(
606
+ "INSERT INTO graph_node_code_embeddings "
607
+ "(node_id, project_id, source_file, dim, vector, content_hash, "
608
+ " model, updated_at) "
609
+ "VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now')) "
610
+ "ON CONFLICT(node_id) DO UPDATE SET "
611
+ " project_id=excluded.project_id, "
612
+ " source_file=excluded.source_file, "
613
+ " dim=excluded.dim, vector=excluded.vector, "
614
+ " content_hash=excluded.content_hash, model=excluded.model, "
615
+ " updated_at=datetime('now')",
616
+ rows,
617
+ )
618
+ written += len(rows)
619
+ conn.commit()
620
+ except Exception:
621
+ conn.rollback()
622
+ raise
623
+ finally:
624
+ conn.close()
625
+ return written
626
+
627
+
628
+ def _rel_for(path: Path, root: Path) -> str:
629
+ try:
630
+ return path.resolve().relative_to(root.resolve()).as_posix()
631
+ except ValueError:
632
+ return path.name