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.
- core/api/__init__.py +0 -0
- core/api/agents/__init__.py +0 -0
- core/api/agents/session_health.py +59 -0
- core/api/agents/session_manager.py +206 -0
- core/api/bin/marvisx-state-hook.py +182 -0
- core/api/config.py +533 -0
- core/api/db.py +1516 -0
- core/api/dependencies/__init__.py +0 -0
- core/api/dependencies/tenant.py +34 -0
- core/api/main.py +1641 -0
- core/api/mcp/__init__.py +8 -0
- core/api/mcp/_adapter.py +184 -0
- core/api/mcp/server.py +58 -0
- core/api/mcp/tools/__init__.py +59 -0
- core/api/mcp/tools/brain.py +599 -0
- core/api/mcp/tools/graph.py +380 -0
- core/api/mcp/tools/handoffs.py +112 -0
- core/api/mcp/tools/ingest.py +326 -0
- core/api/mcp/tools/learnings.py +144 -0
- core/api/mcp/tools/projects.py +99 -0
- core/api/mcp/tools/pull_requests.py +173 -0
- core/api/mcp/tools/safety.py +111 -0
- core/api/mcp/tools/search.py +79 -0
- core/api/mcp/tools/tasks.py +258 -0
- core/api/middleware/__init__.py +0 -0
- core/api/middleware/tool_call_audit.py +111 -0
- core/api/models/__init__.py +346 -0
- core/api/models/auth.py +48 -0
- core/api/models/brain.py +1006 -0
- core/api/models/common.py +76 -0
- core/api/models/costs.py +91 -0
- core/api/models/graph.py +66 -0
- core/api/models/graph_cosmo.py +125 -0
- core/api/models/graph_pr_impact.py +257 -0
- core/api/models/graph_ux.py +141 -0
- core/api/models/inbox.py +230 -0
- core/api/models/ingest_keys.py +108 -0
- core/api/models/kg.py +41 -0
- core/api/models/llm_config.py +56 -0
- core/api/models/monitoring.py +234 -0
- core/api/models/projects.py +161 -0
- core/api/models/search.py +42 -0
- core/api/models/sessions.py +322 -0
- core/api/models/tasks.py +184 -0
- core/api/models/teams.py +63 -0
- core/api/models/users.py +184 -0
- core/api/observability/__init__.py +0 -0
- core/api/observability/tracing.py +92 -0
- core/api/paths.py +26 -0
- core/api/rate_limit.py +24 -0
- core/api/rbac.py +112 -0
- core/api/routers/__init__.py +0 -0
- core/api/routers/_adapter.py +24 -0
- core/api/routers/admin_pr_impact.py +230 -0
- core/api/routers/admin_settings.py +147 -0
- core/api/routers/agent.py +1079 -0
- core/api/routers/agent_tokens.py +276 -0
- core/api/routers/app_settings.py +112 -0
- core/api/routers/audit.py +89 -0
- core/api/routers/auth.py +586 -0
- core/api/routers/bench.py +161 -0
- core/api/routers/brain.py +881 -0
- core/api/routers/brain_directions.py +527 -0
- core/api/routers/ci_checks.py +140 -0
- core/api/routers/comments.py +273 -0
- core/api/routers/costs.py +148 -0
- core/api/routers/docs_coverage.py +217 -0
- core/api/routers/docs_governance.py +63 -0
- core/api/routers/documents.py +318 -0
- core/api/routers/files.py +163 -0
- core/api/routers/finder.py +987 -0
- core/api/routers/graph.py +836 -0
- core/api/routers/handoffs.py +156 -0
- core/api/routers/inbox.py +496 -0
- core/api/routers/ingest_api_keys.py +205 -0
- core/api/routers/ingest_triage.py +1227 -0
- core/api/routers/judge.py +306 -0
- core/api/routers/kg.py +336 -0
- core/api/routers/learnings.py +253 -0
- core/api/routers/llm_config.py +130 -0
- core/api/routers/monitoring.py +347 -0
- core/api/routers/notifications.py +125 -0
- core/api/routers/pr_impact.py +315 -0
- core/api/routers/projects.py +1061 -0
- core/api/routers/pull_requests.py +312 -0
- core/api/routers/push.py +67 -0
- core/api/routers/raci.py +228 -0
- core/api/routers/search.py +125 -0
- core/api/routers/sessions.py +3100 -0
- core/api/routers/settings.py +90 -0
- core/api/routers/share_repo.py +68 -0
- core/api/routers/status_updates.py +96 -0
- core/api/routers/tags.py +45 -0
- core/api/routers/tasks.py +526 -0
- core/api/routers/teams.py +425 -0
- core/api/routers/terminal.py +105 -0
- core/api/routers/users.py +331 -0
- core/api/routers/webhooks.py +330 -0
- core/api/runtime_settings.py +84 -0
- core/api/security.py +652 -0
- core/api/services/__init__.py +0 -0
- core/api/services/audit.py +58 -0
- core/api/services/auto_approval.py +82 -0
- core/api/services/brain/__init__.py +52 -0
- core/api/services/brain/baseline.py +230 -0
- core/api/services/brain/capabilities.py +75 -0
- core/api/services/brain/cascade_rollup.py +388 -0
- core/api/services/brain/compound_bridge.py +215 -0
- core/api/services/brain/cycle.py +1242 -0
- core/api/services/brain/cycle_snapshot.py +371 -0
- core/api/services/brain/digest_collector.py +147 -0
- core/api/services/brain/direction.py +421 -0
- core/api/services/brain/drift.py +356 -0
- core/api/services/brain/drift_router.py +409 -0
- core/api/services/brain/edge_metrics.py +79 -0
- core/api/services/brain/events_reader.py +222 -0
- core/api/services/brain/findings.py +1379 -0
- core/api/services/brain/findings_reader.py +1006 -0
- core/api/services/brain/jobs.py +733 -0
- core/api/services/brain/journal.py +206 -0
- core/api/services/brain/knowledge_forms.py +92 -0
- core/api/services/brain/llm/__init__.py +37 -0
- core/api/services/brain/llm/_runner.py +62 -0
- core/api/services/brain/llm/base.py +70 -0
- core/api/services/brain/llm/cache.py +99 -0
- core/api/services/brain/llm/constants.py +46 -0
- core/api/services/brain/llm/direction_alignment.py +289 -0
- core/api/services/brain/llm/factory.py +132 -0
- core/api/services/brain/llm/finding_reasoning.py +98 -0
- core/api/services/brain/llm/finding_summary.py +92 -0
- core/api/services/brain/llm/grounding.py +46 -0
- core/api/services/brain/llm/journal_polish.py +96 -0
- core/api/services/brain/llm/local_gateway.py +426 -0
- core/api/services/brain/llm/parsers.py +71 -0
- core/api/services/brain/llm/router_glue.py +422 -0
- core/api/services/brain/memory_ops.py +1677 -0
- core/api/services/brain/models.py +140 -0
- core/api/services/brain/owner_hint.py +211 -0
- core/api/services/brain/recap.py +307 -0
- core/api/services/brain/rules/__init__.py +65 -0
- core/api/services/brain/rules/_signals.py +205 -0
- core/api/services/brain/rules/dr1_activity_without_status.py +108 -0
- core/api/services/brain/rules/dr2_decision_without_adr.py +110 -0
- core/api/services/brain/rules/dr3_stale_open_loop.py +127 -0
- core/api/services/brain/rules/dr4_docs_governance_drift.py +79 -0
- core/api/services/brain/rules/dr5_playbook_changed.py +99 -0
- core/api/services/brain/rules/dr6_external_update_unpropagated.py +100 -0
- core/api/services/brain/rules/dr7_claimed_decision_gap.py +123 -0
- core/api/services/brain/rules/dr8_direction_misalignment.py +230 -0
- core/api/services/brain/runs_reader.py +485 -0
- core/api/services/brain/scope.py +79 -0
- core/api/services/brain/sources/__init__.py +46 -0
- core/api/services/brain/sources/base.py +86 -0
- core/api/services/brain/sources/git_kg.py +393 -0
- core/api/services/brain/sources/handoffs.py +157 -0
- core/api/services/brain/sources/ingestor.py +130 -0
- core/api/services/brain/sources/learnings.py +121 -0
- core/api/services/brain/sources/pir_tasks.py +245 -0
- core/api/services/brain/watermarks.py +147 -0
- core/api/services/brain/ws_emitter.py +170 -0
- core/api/services/cc_tasks_reader.py +76 -0
- core/api/services/ci_service.py +263 -0
- core/api/services/claude_metrics.py +796 -0
- core/api/services/codex_metrics.py +364 -0
- core/api/services/conversation_reader.py +102 -0
- core/api/services/cost_service.py +243 -0
- core/api/services/crypto.py +147 -0
- core/api/services/docs_governance/__init__.py +1 -0
- core/api/services/docs_governance/confidence.py +230 -0
- core/api/services/docs_governance/config.py +87 -0
- core/api/services/docs_governance/enrichment.py +65 -0
- core/api/services/docs_governance/frontmatter_validator.py +83 -0
- core/api/services/docs_governance/hard_gates.py +221 -0
- core/api/services/docs_governance/triage_orchestrator.py +98 -0
- core/api/services/embedding_internal.py +395 -0
- core/api/services/embedding_service.py +832 -0
- core/api/services/event_dispatcher.py +167 -0
- core/api/services/events.py +70 -0
- core/api/services/git_ops.py +621 -0
- core/api/services/graph_cosmo_service.py +440 -0
- core/api/services/graph_ranker.py +306 -0
- core/api/services/graph_service.py +1589 -0
- core/api/services/inbox.py +800 -0
- core/api/services/inbox_digest.py +221 -0
- core/api/services/inbox_digest_deep_research.py +80 -0
- core/api/services/inbox_digest_jobs.py +595 -0
- core/api/services/inbox_gmail_sync.py +167 -0
- core/api/services/inbox_llm_classifier.py +906 -0
- core/api/services/inbox_source_identity.py +116 -0
- core/api/services/inbox_sources.py +456 -0
- core/api/services/inbox_taxonomy.py +195 -0
- core/api/services/inbox_tldr.py +1079 -0
- core/api/services/inbox_triage.py +899 -0
- core/api/services/ingest/__init__.py +13 -0
- core/api/services/ingest/api_key_auth.py +136 -0
- core/api/services/ingest/auto_approve.py +120 -0
- core/api/services/ingest/classifier.py +138 -0
- core/api/services/ingest/confidence.py +173 -0
- core/api/services/ingest/dispatch.py +88 -0
- core/api/services/ingest/embedding_router.py +272 -0
- core/api/services/ingest/events.py +33 -0
- core/api/services/ingest/ignore_patterns.py +79 -0
- core/api/services/ingest/image_probe.py +218 -0
- core/api/services/ingest/ingress.py +263 -0
- core/api/services/ingest/insert_saga.py +793 -0
- core/api/services/ingest/llm/__init__.py +13 -0
- core/api/services/ingest/llm/anthropic_haiku.py +23 -0
- core/api/services/ingest/llm/base.py +59 -0
- core/api/services/ingest/llm/byok_provider.py +130 -0
- core/api/services/ingest/llm/classification_context.py +301 -0
- core/api/services/ingest/llm/config_store.py +246 -0
- core/api/services/ingest/llm/factory.py +24 -0
- core/api/services/ingest/llm/kg_enricher.py +306 -0
- core/api/services/ingest/llm/local_gateway.py +821 -0
- core/api/services/ingest/llm/local_vllm.py +23 -0
- core/api/services/ingest/llm/openai_nano.py +349 -0
- core/api/services/ingest/lock_advisory.py +57 -0
- core/api/services/ingest/parser_router.py +1756 -0
- core/api/services/ingest/parsers/__init__.py +1 -0
- core/api/services/ingest/parsers/docling_parser.py +142 -0
- core/api/services/ingest/parsers/docparse_gateway.py +178 -0
- core/api/services/ingest/parsers/docx_parser.py +127 -0
- core/api/services/ingest/parsers/folder_unpacker.py +85 -0
- core/api/services/ingest/parsers/gateway_aux.py +147 -0
- core/api/services/ingest/parsers/image_parser.py +251 -0
- core/api/services/ingest/parsers/internal_markdown.py +89 -0
- core/api/services/ingest/parsers/ocr_gateway.py +117 -0
- core/api/services/ingest/parsers/ocr_pdf_parser.py +112 -0
- core/api/services/ingest/parsers/pdf_types.py +13 -0
- core/api/services/ingest/parsers/transcript_parser.py +445 -0
- core/api/services/ingest/parsers/vision_gateway.py +186 -0
- core/api/services/ingest/parsers/xlsx_parser.py +91 -0
- core/api/services/ingest/parsers/zip_unpacker.py +126 -0
- core/api/services/ingest/preflight.py +393 -0
- core/api/services/ingest/retry_voyage.py +88 -0
- core/api/services/ingest/routing_policy.py +307 -0
- core/api/services/ingest/serializers/__init__.py +1 -0
- core/api/services/ingest/serializers/xlsx_to_markdown.py +80 -0
- core/api/services/ingest/skip_log.py +74 -0
- core/api/services/ingest/watcher.py +637 -0
- core/api/services/kg/__init__.py +0 -0
- core/api/services/kg/audit.py +49 -0
- core/api/services/kg/hybrid_search.py +691 -0
- core/api/services/kg/lens.py +339 -0
- core/api/services/kg/pr_impact.py +770 -0
- core/api/services/kg/queries.py +152 -0
- core/api/services/kg/ranking.py +89 -0
- core/api/services/kg/rrf.py +143 -0
- core/api/services/kg_watcher_control.py +161 -0
- core/api/services/local_llm/__init__.py +19 -0
- core/api/services/local_llm/async_client.py +385 -0
- core/api/services/local_llm/client.py +173 -0
- core/api/services/local_llm/url_validator.py +44 -0
- core/api/services/metrics_collector.py +646 -0
- core/api/services/metrics_providers.py +65 -0
- core/api/services/model_registry.py +266 -0
- core/api/services/model_router.py +137 -0
- core/api/services/n8n_client.py +77 -0
- core/api/services/newsletter_llm_gateway.py +66 -0
- core/api/services/notification_service.py +134 -0
- core/api/services/openai_responses.py +55 -0
- core/api/services/opencode_metrics.py +375 -0
- core/api/services/opencode_sessions.py +173 -0
- core/api/services/pii_redactor.py +138 -0
- core/api/services/pr_impact_pipeline/__init__.py +21 -0
- core/api/services/pr_impact_pipeline/differ.py +421 -0
- core/api/services/pr_impact_pipeline/dispatcher.py +415 -0
- core/api/services/pr_impact_pipeline/gc.py +93 -0
- core/api/services/pr_impact_pipeline/languages.py +192 -0
- core/api/services/pr_impact_pipeline/parser.py +178 -0
- core/api/services/pr_impact_pipeline/writer.py +394 -0
- core/api/services/pr_service.py +1393 -0
- core/api/services/project_paths.py +70 -0
- core/api/services/project_status_updates.py +265 -0
- core/api/services/providers.py +276 -0
- core/api/services/push_service.py +170 -0
- core/api/services/reminder_service.py +89 -0
- core/api/services/runas.py +41 -0
- core/api/services/salience_service.py +69 -0
- core/api/services/security_collector.py +281 -0
- core/api/services/session_catalog.py +385 -0
- core/api/services/session_metrics_service.py +301 -0
- core/api/services/session_ops.py +272 -0
- core/api/services/session_state.py +173 -0
- core/api/services/share_links.py +222 -0
- core/api/services/task_transitions.py +146 -0
- core/api/services/terminal_metrics.py +462 -0
- core/api/services/terminal_metrics_dump.py +203 -0
- core/api/services/tmux.py +1205 -0
- core/api/services/webhook_service.py +422 -0
- core/api/services/workspace_sync.py +164 -0
- core/api/templates/__init__.py +1 -0
- core/api/templates/markdown_share.py +164 -0
- core/api/terminal.py +1031 -0
- core/api/tests/__init__.py +0 -0
- core/api/tests/test_agent_facing_auth_dependencies.py +132 -0
- core/api/tests/test_audit_permissions.py +133 -0
- core/api/tests/test_backfill_session_conversations.py +90 -0
- core/api/tests/test_backfill_working_seconds_msg.py +129 -0
- core/api/tests/test_claude_metrics.py +326 -0
- core/api/tests/test_codex_metrics.py +189 -0
- core/api/tests/test_finder_paths.py +74 -0
- core/api/tests/test_git_ops_merge.py +155 -0
- core/api/tests/test_learnings_check_search.py +81 -0
- core/api/tests/test_metrics_providers.py +133 -0
- core/api/tests/test_migration_087.py +164 -0
- core/api/tests/test_migration_088.py +94 -0
- core/api/tests/test_migration_089.py +116 -0
- core/api/tests/test_openai_responses.py +24 -0
- core/api/tests/test_opencode_metrics.py +740 -0
- core/api/tests/test_opencode_sessions.py +321 -0
- core/api/tests/test_pr_workflow_e2e.py +457 -0
- core/api/tests/test_projects_handoffs.py +31 -0
- core/api/tests/test_providers.py +138 -0
- core/api/tests/test_safety_bridge.py +347 -0
- core/api/tests/test_session_catalog.py +142 -0
- core/api/tests/test_session_conversations.py +512 -0
- core/api/tests/test_session_metrics_service.py +270 -0
- core/api/tests/test_session_resume_paths.py +548 -0
- core/api/tests/test_session_theme_mode_migration.py +56 -0
- core/api/tests/test_sessions_rbac.py +131 -0
- core/api/tests/test_share_edit.py +398 -0
- core/api/tests/test_share_repo.py +200 -0
- core/api/tests/test_terminal_session_manager.py +98 -0
- core/api/tests/test_terminal_upload.py +34 -0
- core/api/tests/test_tmux.py +272 -0
- core/api/tests/test_workspace_sync.py +186 -0
- core/api/tests/test_ws_ticket_in_memory.py +73 -0
- core/api/use_cases/__init__.py +11 -0
- core/api/use_cases/_context.py +89 -0
- core/api/use_cases/_errors.py +62 -0
- core/api/use_cases/_roles.py +16 -0
- core/api/use_cases/audit.py +171 -0
- core/api/use_cases/brain.py +1232 -0
- core/api/use_cases/costs.py +249 -0
- core/api/use_cases/graph.py +1153 -0
- core/api/use_cases/handoffs.py +506 -0
- core/api/use_cases/ingest_triage.py +1229 -0
- core/api/use_cases/learnings.py +538 -0
- core/api/use_cases/projects.py +705 -0
- core/api/use_cases/pull_requests.py +415 -0
- core/api/use_cases/search.py +926 -0
- core/api/use_cases/tasks.py +1495 -0
- core/api/visibility.py +141 -0
- core/cli/__init__.py +5 -0
- core/cli/_index_source.py +632 -0
- core/cli/_runtime_ctx.py +160 -0
- core/cli/_transmute.py +241 -0
- core/cli/marvis_doctor.py +704 -0
- core/cli/marvis_feedback.py +396 -0
- core/cli/marvis_governance.py +315 -0
- core/cli/marvis_hooks.py +515 -0
- core/cli/marvis_init.py +757 -0
- core/cli/marvis_mcp.py +401 -0
- core/cli/marvis_runtime.py +855 -0
- core/cli/marvis_telemetry.py +228 -0
- core/scripts/_drift_check.py +716 -0
- core/scripts/_frontmatter.py +66 -0
- core/scripts/_graph_writer.py +189 -0
- core/scripts/ast_parser.py +1553 -0
- core/scripts/install_hooks/__init__.py +1 -0
- core/scripts/install_hooks/_config.sh +109 -0
- core/scripts/install_hooks/block-dangerous-bash.sh +23 -0
- core/scripts/install_hooks/block-db-direct-write.sh +23 -0
- core/scripts/install_hooks/block-push-no-task.sh +23 -0
- core/scripts/install_hooks/block-staging-to-prod.sh +23 -0
- core/scripts/install_hooks/block-subtree-push.sh +23 -0
- core/scripts/install_hooks/config.json +53 -0
- core/scripts/install_hooks/enforce-no-merge-main.sh +23 -0
- core/scripts/install_hooks/enforce-worktree.sh +23 -0
- core/scripts/install_hooks/quality-gate.sh +170 -0
- core/scripts/install_hooks/safety_bridge.py +968 -0
- core/scripts/install_hooks/secret-scan.sh +23 -0
- core/scripts/migrate_spike_node_ids.py +122 -0
- core/scripts/populate_artifacts.py +2198 -0
- core/scripts/populate_cross_project.py +2457 -0
- core/scripts/populate_inbox_nodes.py +357 -0
- core/scripts/populate_pr_impact.py +267 -0
- core/scripts/populate_project_nodes.py +603 -0
- core/scripts/populate_touch_counter.py +337 -0
- core/scripts/reparse_failed.py +57 -0
- core/scripts/safety_bridge.py +968 -0
- core/telemetry/__init__.py +9 -0
- core/telemetry/client.py +405 -0
- core/telemetry/schema.py +122 -0
- core/wizard/__init__.py +65 -0
- core/wizard/byok_vault.py +147 -0
- core/wizard/defaults.py +58 -0
- core/wizard/state.py +117 -0
- core/wizard/steps.py +70 -0
- core/wizard/validation.py +136 -0
- marvisx_cli-0.1.0.dist-info/METADATA +201 -0
- marvisx_cli-0.1.0.dist-info/RECORD +587 -0
- marvisx_cli-0.1.0.dist-info/WHEEL +5 -0
- marvisx_cli-0.1.0.dist-info/entry_points.txt +3 -0
- marvisx_cli-0.1.0.dist-info/licenses/LICENSE +98 -0
- marvisx_cli-0.1.0.dist-info/top_level.txt +3 -0
- migrations/001_initial.sql +33 -0
- migrations/002_tasks.sql +30 -0
- migrations/003_session_management.sql +7 -0
- migrations/004_projects_comments.sql +65 -0
- migrations/005_session_intelligence.sql +15 -0
- migrations/006_settings.sql +12 -0
- migrations/007_task_scoring.sql +12 -0
- migrations/008_cost_tracking.sql +31 -0
- migrations/009_session_card_metrics.sql +3 -0
- migrations/010_monitoring.sql +55 -0
- migrations/012_agent_api.sql +21 -0
- migrations/013_session_complete.sql +8 -0
- migrations/015_pull_requests.sql +43 -0
- migrations/015_pull_requests_down.sql +5 -0
- migrations/016_users_raci.sql +116 -0
- migrations/017_task_cost_entries.sql +87 -0
- migrations/018_agents.sql +73 -0
- migrations/018_agents_down.sql +13 -0
- migrations/019_review_feedback.sql +18 -0
- migrations/020_pr_commit_sha.sql +4 -0
- migrations/021_webhook_events.sql +18 -0
- migrations/022_devx_agent_managed.sql +11 -0
- migrations/022_devx_agent_managed_down.sql +6 -0
- migrations/023_devx_p1_gate.sql +7 -0
- migrations/023_devx_p1_gate_down.sql +3 -0
- migrations/024_chat_messages.sql +16 -0
- migrations/024_pr_conversation_id.sql +8 -0
- migrations/024_task_indexes.sql +21 -0
- migrations/024_task_indexes_down.sql +7 -0
- migrations/025_audit_log.sql +17 -0
- migrations/026_agent_tokens.sql +20 -0
- migrations/027_teams_auth_phase_b.sql +35 -0
- migrations/028_learnings.sql +23 -0
- migrations/029_team_roles.sql +14 -0
- migrations/030_finder_pins.sql +10 -0
- migrations/031_pr_deploy_status.sql +9 -0
- migrations/032_task_reminders.sql +7 -0
- migrations/033_events_retry_count.sql +6 -0
- migrations/033_session_owner.sql +9 -0
- migrations/034_notifications.sql +38 -0
- migrations/035_shared_links.sql +15 -0
- migrations/036_session_index_upgrade.sql +29 -0
- migrations/037_pr_approval.sql +15 -0
- migrations/038_pr_submitted_by.sql +6 -0
- migrations/039_push_subscriptions.sql +17 -0
- migrations/040_semantic_search.sql +16 -0
- migrations/041_workspaces.sql +63 -0
- migrations/042_oidc_providers.sql +24 -0
- migrations/043_ci_checks.sql +31 -0
- migrations/044_agent_metrics.sql +30 -0
- migrations/045_documents_doc_type.sql +5 -0
- migrations/046_salience.sql +13 -0
- migrations/047_seed_missing_agents.sql +6 -0
- migrations/048_fix_agent_paths_roles.sql +5 -0
- migrations/049_agent_role_and_learnings_schema.sql +3 -0
- migrations/050_session_provider.sql +2 -0
- migrations/051_session_launch_profile.sql +4 -0
- migrations/052_session_theme_mode.sql +2 -0
- migrations/052_task_kind.sql +4 -0
- migrations/053_inbox_items.sql +31 -0
- migrations/054_inbox_triage_contract.sql +30 -0
- migrations/055_inbox_topic_treatment.sql +12 -0
- migrations/056_inbox_treatment_read_save.sql +57 -0
- migrations/057_session_theme_mode_backfill.sql +4 -0
- migrations/058_inbox_item_status_lifecycle.sql +13 -0
- migrations/059_inbox_tldr_and_source_scores.sql +18 -0
- migrations/060_newsletter.sql +16 -0
- migrations/061_inbox_redesign.sql +69 -0
- migrations/062_fix_inbox_sources_backfill.sql +37 -0
- migrations/063_task_completion_mode.sql +23 -0
- migrations/064_judge_mode_setting.sql +4 -0
- migrations/065_knowledge_graph_spike.sql +40 -0
- migrations/066_digest_ranking_inputs.sql +10 -0
- migrations/066_kg_artifact_nodes.sql +129 -0
- migrations/067_inbox_digest_selections.sql +28 -0
- migrations/067_kg_temporal.sql +53 -0
- migrations/068_inbox_digest_app_settings.sql +9 -0
- migrations/068_kg_touch_counter.sql +52 -0
- migrations/069_kg_doc_types.sql +117 -0
- migrations/070_digest_ranking_inputs_recovery.sql +3 -0
- migrations/071_inbox_digest_selections_recovery.sql +3 -0
- migrations/072_inbox_digest_app_settings_recovery.sql +3 -0
- migrations/073_kg_cross_project.sql +216 -0
- migrations/073_kg_cross_project_down.sql +77 -0
- migrations/074_kg_infra_types.sql +208 -0
- migrations/074_kg_infra_types_down.sql +80 -0
- migrations/075_kg_file_state_recovery.sql +35 -0
- migrations/075_kg_file_state_recovery_down.sql +5 -0
- migrations/076_kg_watcher_state.sql +33 -0
- migrations/076_kg_watcher_state_down.sql +3 -0
- migrations/077_kg_doc_types_extend.sql +226 -0
- migrations/077_kg_doc_types_extend_down.sql +80 -0
- migrations/078_kg_fts5.sql +102 -0
- migrations/078_kg_fts5_down.sql +14 -0
- migrations/079_kg_missing_indexes.sql +31 -0
- migrations/079_kg_missing_indexes_down.sql +10 -0
- migrations/080_kg_fts5_extended.sql +232 -0
- migrations/080_kg_fts5_extended_down.sql +25 -0
- migrations/081_kg_lens_indexes.sql +9 -0
- migrations/081_kg_lens_indexes_down.sql +3 -0
- migrations/082_kg_pins.sql +26 -0
- migrations/082_kg_pins_down.sql +14 -0
- migrations/083_kg_graph_nodes_degree.sql +20 -0
- migrations/083_kg_graph_nodes_degree_down.sql +15 -0
- migrations/084_drop_legacy_scheduler_tables.sql +58 -0
- migrations/084_drop_legacy_scheduler_tables_down.sql +112 -0
- migrations/085_kg_edge_resolves_to.sql +142 -0
- migrations/085_kg_edge_resolves_to_down.sql +66 -0
- migrations/086_project_status_updates_feed.sql +20 -0
- migrations/086_project_status_updates_feed_down.sql +36 -0
- migrations/087_session_metrics_dual.sql +50 -0
- migrations/087_session_metrics_dual_down.sql +21 -0
- migrations/088_rename_context_pct_legacy.sql +23 -0
- migrations/088_rename_context_pct_legacy_down.sql +8 -0
- migrations/089_session_metrics_equivalent_cost.sql +26 -0
- migrations/089_session_metrics_equivalent_cost_down.sql +11 -0
- migrations/090_kg_inbox_node_type.sql +26 -0
- migrations/090_kg_inbox_node_type_down.sql +20 -0
- migrations/091_kg_inbox_node_type_check.sql +265 -0
- migrations/091_kg_inbox_node_type_check_down.sql +129 -0
- migrations/092_sessions_activity_state_ts.sql +29 -0
- migrations/092_sessions_activity_state_ts_down.sql +14 -0
- migrations/093_sessions_activity_state_column.sql +29 -0
- migrations/093_sessions_activity_state_column_down.sql +10 -0
- migrations/094_ingest_pending.sql +55 -0
- migrations/094_ingest_pending_down.sql +15 -0
- migrations/095_kg_intent_first.sql +77 -0
- migrations/095_kg_intent_first_down.sql +25 -0
- migrations/096_kg_xlsx_artifact_prefix.sql +17 -0
- migrations/096_kg_xlsx_artifact_prefix_down.sql +11 -0
- migrations/097_ingest_change_history.sql +37 -0
- migrations/097_ingest_change_history_down.sql +13 -0
- migrations/098_kg_node_type_business.sql +254 -0
- migrations/098_kg_node_type_business_down.sql +195 -0
- migrations/099_kg_edges_restore_weight.sql +58 -0
- migrations/099_kg_edges_restore_weight_down.sql +12 -0
- migrations/100_kg_enriched_at.sql +25 -0
- migrations/100_kg_enriched_at_down.sql +12 -0
- migrations/101_local_llm_shadow_comparisons.sql +66 -0
- migrations/101_local_llm_shadow_comparisons_down.sql +15 -0
- migrations/102_promote_llm_costs.sql +69 -0
- migrations/102_promote_llm_costs_down.sql +19 -0
- migrations/103_ingest_skipped_log.sql +46 -0
- migrations/103_ingest_skipped_log_down.sql +15 -0
- migrations/120_docs_governance.sql +50 -0
- migrations/120_docs_governance_down.sql +11 -0
- migrations/121_notification_event_fk_cleanup.sql +21 -0
- migrations/121_notification_event_fk_cleanup_down.sql +10 -0
- migrations/122_docs_drift_history.sql +34 -0
- migrations/122_docs_drift_history_down.sql +15 -0
- migrations/123_ingest_parser_waiting_status.sql +69 -0
- migrations/123_ingest_parser_waiting_status_down.sql +69 -0
- migrations/124_heypocket_recordings.sql +63 -0
- migrations/124_heypocket_recordings_down.sql +13 -0
- migrations/125_kg_node_type_record.sql +219 -0
- migrations/125_kg_node_type_record_down.sql +205 -0
- migrations/126_ingest_terminal_upload_source_kind.sql +69 -0
- migrations/126_ingest_terminal_upload_source_kind_down.sql +69 -0
- migrations/127_brain_v1_substrate.sql +200 -0
- migrations/127_brain_v1_substrate_down.sql +32 -0
- migrations/128_brain_drift_signals.sql +157 -0
- migrations/128_brain_drift_signals_down.sql +23 -0
- migrations/129_brain_memory_operations.sql +232 -0
- migrations/129_brain_memory_operations_down.sql +27 -0
- migrations/130_brain_findings.sql +258 -0
- migrations/130_brain_findings_down.sql +29 -0
- migrations/132_kg_pr_modifies.sql +242 -0
- migrations/132_kg_pr_modifies_down.sql +99 -0
- migrations/133_brain_v1_2_direction_schema.sql +476 -0
- migrations/133_brain_v1_2_direction_schema_down.sql +273 -0
- migrations/134_brain_journal_narrative_polished.sql +8 -0
- migrations/134_brain_journal_narrative_polished_down.sql +6 -0
- migrations/135_kg_edges_provider.sql +21 -0
- migrations/136_documents_fts.sql +56 -0
- migrations/137_promote_llm_costs.sql +59 -0
- migrations/137_promote_llm_costs_down.sql +19 -0
- migrations/138_ingest_api_keys.sql +39 -0
- migrations/138_ingest_api_keys_down.sql +8 -0
- migrations/139_ingest_pending_ingress.sql +91 -0
- migrations/139_ingest_pending_ingress_down.sql +73 -0
- migrations/140_ingest_idempotency_quota.sql +45 -0
- migrations/140_ingest_idempotency_quota_down.sql +9 -0
- migrations/141_ingest_pending_metadata.sql +16 -0
- migrations/141_ingest_pending_metadata_down.sql +7 -0
- migrations/142_llm_function_config.sql +36 -0
- migrations/142_llm_function_config_down.sql +8 -0
- migrations/143_kg_code_embeddings.sql +25 -0
- migrations/143_kg_code_embeddings_down.sql +5 -0
- migrations/__init__.py +4 -0
- projects/_template/project.yaml +46 -0
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
"""Approve-and-insert saga for phase-1 file ingestion."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Literal
|
|
15
|
+
|
|
16
|
+
from core.api.db import acquire_write_db
|
|
17
|
+
from core.api.paths import repo_root
|
|
18
|
+
from core.api.services.ingest.embedding_router import embed_and_index
|
|
19
|
+
from core.api.services.ingest.events import broadcast_ingest_changed
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
PROJECTS_ROOT = Path("/data/projects")
|
|
24
|
+
REPO_ROOT = repo_root(__file__)
|
|
25
|
+
SUBPROCESS_TIMEOUT_SEC = 60
|
|
26
|
+
|
|
27
|
+
# Phase 1.5 E6: keep-ref so fire-and-forget background tasks don't get GC'd
|
|
28
|
+
# mid-flight (asyncio.create_task warning). Tasks self-discard on completion.
|
|
29
|
+
_PENDING_ENRICHMENT_TASKS: set[asyncio.Task[None]] = set()
|
|
30
|
+
_NODE_TYPE_ALLOWLIST = {
|
|
31
|
+
"handoff",
|
|
32
|
+
"solution",
|
|
33
|
+
"learning",
|
|
34
|
+
"audit",
|
|
35
|
+
"analysis",
|
|
36
|
+
"research",
|
|
37
|
+
"guide",
|
|
38
|
+
"plan",
|
|
39
|
+
"brainstorm",
|
|
40
|
+
"file",
|
|
41
|
+
# Phase 1.5 E5-fix9: business document taxonomy.
|
|
42
|
+
"policy",
|
|
43
|
+
"contract",
|
|
44
|
+
"transcript",
|
|
45
|
+
"record",
|
|
46
|
+
"report",
|
|
47
|
+
}
|
|
48
|
+
_SAFE_ID_RE = re.compile(r"[^a-zA-Z0-9_.:-]+")
|
|
49
|
+
_COMMIT_SUBJECT_CONTROL_RE = re.compile(r"[\x00-\x1f\x7f]+")
|
|
50
|
+
_COMMIT_SUBJECT_MAX_FRAGMENT = 120
|
|
51
|
+
_TEXT_TARGET_SUFFIXES = {".md", ".txt"}
|
|
52
|
+
ProjectType = Literal["work", "code", "system"]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _project_root(slug: str) -> Path:
|
|
56
|
+
return (PROJECTS_ROOT / slug).resolve()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _load_project_entry(slug: str) -> tuple[ProjectType, Path | None]:
|
|
60
|
+
"""Resolve project type and repo path from project.yaml via the project index.
|
|
61
|
+
|
|
62
|
+
On metadata drift we fail closed as ``work``: no git commit and no external
|
|
63
|
+
embedding unless the separate DB policy says otherwise.
|
|
64
|
+
"""
|
|
65
|
+
yaml_path = _project_root(slug) / "project.yaml"
|
|
66
|
+
if yaml_path.exists():
|
|
67
|
+
try:
|
|
68
|
+
import yaml
|
|
69
|
+
from core.api.config import ALLOWED_REPO_PARENTS
|
|
70
|
+
|
|
71
|
+
data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
|
|
72
|
+
if not isinstance(data, dict):
|
|
73
|
+
logger.warning("project.yaml is not a mapping for slug=%s", slug)
|
|
74
|
+
return "work", None
|
|
75
|
+
raw_type = data.get("type", "work")
|
|
76
|
+
project_type: ProjectType = (
|
|
77
|
+
raw_type if raw_type in ("work", "code", "system") else "work"
|
|
78
|
+
)
|
|
79
|
+
raw_repo = data.get("repo_path")
|
|
80
|
+
repo_path = Path(raw_repo).resolve() if raw_repo else None
|
|
81
|
+
if repo_path and not any(
|
|
82
|
+
repo_path.is_relative_to(parent) for parent in ALLOWED_REPO_PARENTS
|
|
83
|
+
):
|
|
84
|
+
logger.warning("repo_path containment violation for slug=%s: %s", slug, repo_path)
|
|
85
|
+
repo_path = None
|
|
86
|
+
return project_type, repo_path
|
|
87
|
+
except Exception:
|
|
88
|
+
logger.warning(
|
|
89
|
+
"project.yaml parse failed for slug=%s; defaulting to work",
|
|
90
|
+
slug,
|
|
91
|
+
exc_info=True,
|
|
92
|
+
)
|
|
93
|
+
return "work", None
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
from core.api.routers.projects import _find_project_entry
|
|
97
|
+
|
|
98
|
+
entry = _find_project_entry(slug)
|
|
99
|
+
except Exception:
|
|
100
|
+
logger.warning("project index lookup failed for slug=%s", slug, exc_info=True)
|
|
101
|
+
return "work", None
|
|
102
|
+
|
|
103
|
+
if entry is None:
|
|
104
|
+
logger.warning("project index missing for slug=%s, defaulting to work", slug)
|
|
105
|
+
return "work", None
|
|
106
|
+
if entry.project_type not in ("work", "code", "system"):
|
|
107
|
+
logger.warning(
|
|
108
|
+
"project type unknown for slug=%s type=%r, defaulting to work",
|
|
109
|
+
slug,
|
|
110
|
+
entry.project_type,
|
|
111
|
+
)
|
|
112
|
+
return "work", None
|
|
113
|
+
return entry.project_type, entry.repo_path
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _safe_relative_path(value: str) -> Path:
|
|
117
|
+
rel = Path(value)
|
|
118
|
+
if rel.is_absolute() or ".." in rel.parts:
|
|
119
|
+
raise ValueError(f"Unsafe target path: {value}")
|
|
120
|
+
return rel
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _unique_target_path(project_root: Path, target_folder: str, target_filename: str, sha: str) -> Path:
|
|
124
|
+
folder = _safe_relative_path(target_folder)
|
|
125
|
+
filename = _safe_relative_path(target_filename)
|
|
126
|
+
if len(filename.parts) != 1:
|
|
127
|
+
raise ValueError("target_filename must be a basename")
|
|
128
|
+
target_dir = (project_root / folder).resolve()
|
|
129
|
+
if not target_dir.is_relative_to(project_root):
|
|
130
|
+
raise ValueError("target_folder escapes project root")
|
|
131
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
|
|
133
|
+
candidate = (target_dir / filename.name).resolve()
|
|
134
|
+
if not candidate.is_relative_to(project_root):
|
|
135
|
+
raise ValueError("target path escapes project root")
|
|
136
|
+
if not candidate.exists():
|
|
137
|
+
return candidate
|
|
138
|
+
|
|
139
|
+
stem = candidate.stem
|
|
140
|
+
suffix = candidate.suffix or ".md"
|
|
141
|
+
return target_dir / f"{stem}-{sha[:8]}{suffix}"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _sidecar_path(path: Path) -> Path:
|
|
145
|
+
return path.with_suffix(".metadata.json")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _move_sidecar_if_present(source_path: Path, target_path: Path, sha: str) -> None:
|
|
149
|
+
sidecar = _sidecar_path(source_path)
|
|
150
|
+
if not sidecar.exists():
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
target_sidecar = _sidecar_path(target_path)
|
|
154
|
+
if target_sidecar.exists():
|
|
155
|
+
target_sidecar = target_path.with_name(
|
|
156
|
+
f"{target_path.stem}-{sha[:8]}.metadata.json"
|
|
157
|
+
)
|
|
158
|
+
sidecar.replace(target_sidecar)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _frontmatter_value(value: Any) -> str:
|
|
162
|
+
if isinstance(value, list):
|
|
163
|
+
items = [str(item).strip() for item in value if str(item).strip()]
|
|
164
|
+
return json.dumps(items, ensure_ascii=False)
|
|
165
|
+
return json.dumps(str(value), ensure_ascii=False)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _classification_frontmatter(classification: dict[str, Any]) -> str:
|
|
169
|
+
lines: list[str] = []
|
|
170
|
+
for key in ("type", "title", "tags"):
|
|
171
|
+
value = classification.get(key)
|
|
172
|
+
if value in (None, "", []):
|
|
173
|
+
continue
|
|
174
|
+
lines.append(f"{key}: {_frontmatter_value(value)}")
|
|
175
|
+
if not lines:
|
|
176
|
+
return ""
|
|
177
|
+
return "---\n" + "\n".join(lines) + "\n---\n\n"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _looks_like_frontmatter(text: str) -> bool:
|
|
181
|
+
return text.startswith("---\n") or text.startswith("---\r\n")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _text_artifact_body(extracted_text: str, classification: dict[str, Any]) -> str:
|
|
185
|
+
body = extracted_text.lstrip("\ufeff")
|
|
186
|
+
if _looks_like_frontmatter(body):
|
|
187
|
+
return body
|
|
188
|
+
return f"{_classification_frontmatter(classification)}{body}"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _should_materialize_extracted_text(
|
|
192
|
+
*,
|
|
193
|
+
source_path: Path,
|
|
194
|
+
target_path: Path,
|
|
195
|
+
extracted_text: str,
|
|
196
|
+
) -> bool:
|
|
197
|
+
if not extracted_text.strip():
|
|
198
|
+
return False
|
|
199
|
+
if target_path.suffix.lower() not in _TEXT_TARGET_SUFFIXES:
|
|
200
|
+
return False
|
|
201
|
+
return source_path.suffix.lower() != target_path.suffix.lower()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _write_text_atomic(target_path: Path, text: str) -> None:
|
|
205
|
+
tmp_path = target_path.with_name(f".{target_path.name}.{os.getpid()}.tmp")
|
|
206
|
+
try:
|
|
207
|
+
tmp_path.write_text(text, encoding="utf-8")
|
|
208
|
+
tmp_path.replace(target_path)
|
|
209
|
+
finally:
|
|
210
|
+
tmp_path.unlink(missing_ok=True)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _materialize_target_file(
|
|
214
|
+
*,
|
|
215
|
+
source_path: Path,
|
|
216
|
+
target_path: Path,
|
|
217
|
+
row,
|
|
218
|
+
classification: dict[str, Any],
|
|
219
|
+
) -> None:
|
|
220
|
+
"""Create the target artifact without confusing source bytes and parser output."""
|
|
221
|
+
extracted_text = str(row["extracted_text"] or "")
|
|
222
|
+
if _should_materialize_extracted_text(
|
|
223
|
+
source_path=source_path,
|
|
224
|
+
target_path=target_path,
|
|
225
|
+
extracted_text=extracted_text,
|
|
226
|
+
):
|
|
227
|
+
_write_text_atomic(
|
|
228
|
+
target_path,
|
|
229
|
+
_text_artifact_body(extracted_text, classification),
|
|
230
|
+
)
|
|
231
|
+
_move_sidecar_if_present(source_path, target_path, row["sha256"])
|
|
232
|
+
source_path.unlink()
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
source_path.replace(target_path)
|
|
236
|
+
_move_sidecar_if_present(source_path, target_path, row["sha256"])
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
async def _run_populator(module: str, target_path: Path) -> tuple[bool, str]:
|
|
240
|
+
cmd = [sys.executable, "-m", module, "--incremental", str(target_path)]
|
|
241
|
+
env = {**os.environ, "KG_HOOK_DISABLED": "1"}
|
|
242
|
+
try:
|
|
243
|
+
proc = await asyncio.create_subprocess_exec(
|
|
244
|
+
*cmd,
|
|
245
|
+
cwd=str(REPO_ROOT),
|
|
246
|
+
env=env,
|
|
247
|
+
stdout=asyncio.subprocess.PIPE,
|
|
248
|
+
stderr=asyncio.subprocess.PIPE,
|
|
249
|
+
)
|
|
250
|
+
except OSError as exc:
|
|
251
|
+
return False, str(exc)
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
stdout_b, stderr_b = await asyncio.wait_for(
|
|
255
|
+
proc.communicate(), timeout=SUBPROCESS_TIMEOUT_SEC
|
|
256
|
+
)
|
|
257
|
+
except asyncio.TimeoutError:
|
|
258
|
+
proc.kill()
|
|
259
|
+
try:
|
|
260
|
+
await asyncio.wait_for(proc.wait(), timeout=5)
|
|
261
|
+
except asyncio.TimeoutError:
|
|
262
|
+
pass
|
|
263
|
+
return False, f"{module} timed out"
|
|
264
|
+
|
|
265
|
+
stdout = stdout_b.decode("utf-8", errors="replace") if stdout_b else ""
|
|
266
|
+
stderr = stderr_b.decode("utf-8", errors="replace") if stderr_b else ""
|
|
267
|
+
if proc.returncode != 0:
|
|
268
|
+
return False, stderr[:500] or stdout[:500] or f"{module} exited {proc.returncode}"
|
|
269
|
+
return True, stdout[:500]
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _commit_subject_fragment(value: str, fallback: str) -> str:
|
|
273
|
+
cleaned = _COMMIT_SUBJECT_CONTROL_RE.sub(" ", str(value))
|
|
274
|
+
cleaned = re.sub(r"\s+", " ", cleaned).strip()
|
|
275
|
+
if not cleaned:
|
|
276
|
+
return fallback
|
|
277
|
+
return cleaned[:_COMMIT_SUBJECT_MAX_FRAGMENT]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _commit_ingested_file(
|
|
281
|
+
*,
|
|
282
|
+
slug: str,
|
|
283
|
+
repo_path: Path | None,
|
|
284
|
+
target_path: Path,
|
|
285
|
+
target_filename: str,
|
|
286
|
+
) -> bool:
|
|
287
|
+
if repo_path is None:
|
|
288
|
+
logger.info("ingest git commit skipped: project=%s has no repo_path", slug)
|
|
289
|
+
return False
|
|
290
|
+
repo = repo_path.resolve()
|
|
291
|
+
if not (repo / ".git").exists():
|
|
292
|
+
logger.info("ingest git commit skipped: repo_path is not a git repo: %s", repo)
|
|
293
|
+
return False
|
|
294
|
+
if not target_path.is_relative_to(repo):
|
|
295
|
+
logger.info("ingest git commit skipped: target outside repo: %s", target_path)
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
rel_target = target_path.relative_to(repo)
|
|
299
|
+
env = {
|
|
300
|
+
**os.environ,
|
|
301
|
+
"GIT_AUTHOR_NAME": "ingest-bot",
|
|
302
|
+
"GIT_AUTHOR_EMAIL": "ingest@marvisx",
|
|
303
|
+
"GIT_COMMITTER_NAME": "ingest-bot",
|
|
304
|
+
"GIT_COMMITTER_EMAIL": "ingest@marvisx",
|
|
305
|
+
}
|
|
306
|
+
subprocess.run(
|
|
307
|
+
["git", "add", "--", str(rel_target)],
|
|
308
|
+
cwd=str(repo),
|
|
309
|
+
env=env,
|
|
310
|
+
check=True,
|
|
311
|
+
timeout=10,
|
|
312
|
+
)
|
|
313
|
+
result = subprocess.run(
|
|
314
|
+
[
|
|
315
|
+
"git",
|
|
316
|
+
"commit",
|
|
317
|
+
"--only",
|
|
318
|
+
"-m",
|
|
319
|
+
"feat(ingest): "
|
|
320
|
+
f"{_commit_subject_fragment(slug, 'project')} "
|
|
321
|
+
f"{_commit_subject_fragment(target_filename, 'file')}",
|
|
322
|
+
"--",
|
|
323
|
+
str(rel_target),
|
|
324
|
+
],
|
|
325
|
+
cwd=str(repo),
|
|
326
|
+
env=env,
|
|
327
|
+
capture_output=True,
|
|
328
|
+
text=True,
|
|
329
|
+
timeout=30,
|
|
330
|
+
check=False,
|
|
331
|
+
)
|
|
332
|
+
if result.returncode != 0:
|
|
333
|
+
raise RuntimeError(result.stderr.strip()[:500] or "git commit failed")
|
|
334
|
+
logger.info("ingest git commit created: project=%s target=%s", slug, rel_target)
|
|
335
|
+
return True
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
async def _set_saga_error(ingest_id: str, project_slug: str, message: str) -> None:
|
|
339
|
+
async with acquire_write_db() as db:
|
|
340
|
+
await db.execute(
|
|
341
|
+
"""
|
|
342
|
+
UPDATE ingest_pending
|
|
343
|
+
SET status = 'parse_error',
|
|
344
|
+
error_message = ?,
|
|
345
|
+
updated_at = datetime('now')
|
|
346
|
+
WHERE id = ?
|
|
347
|
+
""",
|
|
348
|
+
(message[:1000], ingest_id),
|
|
349
|
+
)
|
|
350
|
+
await db.commit()
|
|
351
|
+
await broadcast_ingest_changed(
|
|
352
|
+
"saga_error",
|
|
353
|
+
ingest_id=ingest_id,
|
|
354
|
+
project_slug=project_slug,
|
|
355
|
+
status="parse_error",
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
async def compensate_step(ingest_id: str, step_failed: str) -> None:
|
|
360
|
+
"""Best-effort compensation for partially written ingest side effects."""
|
|
361
|
+
pattern = f'%"ingest_id":"{ingest_id}"%'
|
|
362
|
+
spaced_pattern = f'%"ingest_id": "{ingest_id}"%'
|
|
363
|
+
async with acquire_write_db() as db:
|
|
364
|
+
if step_failed in {"populate", "kg_edges", "ensure_kg_edge"}:
|
|
365
|
+
await db.execute(
|
|
366
|
+
"DELETE FROM kg_edge_activity WHERE metadata_json LIKE ? OR metadata_json LIKE ?",
|
|
367
|
+
(pattern, spaced_pattern),
|
|
368
|
+
)
|
|
369
|
+
await db.execute(
|
|
370
|
+
"DELETE FROM graph_edges WHERE metadata LIKE ? OR metadata LIKE ?",
|
|
371
|
+
(pattern, spaced_pattern),
|
|
372
|
+
)
|
|
373
|
+
await db.execute(
|
|
374
|
+
"DELETE FROM graph_nodes WHERE metadata LIKE ? OR metadata LIKE ?",
|
|
375
|
+
(pattern, spaced_pattern),
|
|
376
|
+
)
|
|
377
|
+
elif step_failed == "voyage":
|
|
378
|
+
from core.api.services.ingest.retry_voyage import enqueue_voyage_removal
|
|
379
|
+
|
|
380
|
+
await enqueue_voyage_removal(ingest_id)
|
|
381
|
+
await db.execute(
|
|
382
|
+
"""
|
|
383
|
+
UPDATE ingest_pending
|
|
384
|
+
SET status = 'parse_error',
|
|
385
|
+
error_message = ?,
|
|
386
|
+
updated_at = datetime('now')
|
|
387
|
+
WHERE id = ?
|
|
388
|
+
""",
|
|
389
|
+
(f"compensated failed ingest step: {step_failed}", ingest_id),
|
|
390
|
+
)
|
|
391
|
+
await db.commit()
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _fallback_node_id(document_type: str, slug: str, target_path: Path) -> str:
|
|
395
|
+
digest = hashlib.sha1(str(target_path).encode("utf-8")).hexdigest()[:12]
|
|
396
|
+
safe_slug = _SAFE_ID_RE.sub("-", slug)
|
|
397
|
+
return f"{document_type}:artifact:{safe_slug}-{digest}"
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
async def _ensure_kg_edge(
|
|
401
|
+
*,
|
|
402
|
+
ingest_id: str,
|
|
403
|
+
project_slug: str,
|
|
404
|
+
target_path: Path,
|
|
405
|
+
document_type: str,
|
|
406
|
+
classification: dict[str, Any],
|
|
407
|
+
populator_warnings: list[str],
|
|
408
|
+
) -> str | None:
|
|
409
|
+
"""Returns the artifact node id (for downstream Phase 1.5 E6 KG enrichment trigger)."""
|
|
410
|
+
now_iso = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
411
|
+
project_node_id = f"project:artifact:{project_slug}"
|
|
412
|
+
metadata_json = json.dumps(
|
|
413
|
+
{
|
|
414
|
+
"ingest_id": ingest_id,
|
|
415
|
+
"target_path": str(target_path),
|
|
416
|
+
"classification": classification,
|
|
417
|
+
"populator_warnings": populator_warnings,
|
|
418
|
+
},
|
|
419
|
+
ensure_ascii=False,
|
|
420
|
+
)
|
|
421
|
+
candidate_paths = [
|
|
422
|
+
str(target_path),
|
|
423
|
+
str(target_path.relative_to(PROJECTS_ROOT)),
|
|
424
|
+
]
|
|
425
|
+
if project_slug == "marvisx":
|
|
426
|
+
candidate_paths.append(f"projects/{target_path.relative_to(PROJECTS_ROOT)}")
|
|
427
|
+
|
|
428
|
+
async with acquire_write_db() as db:
|
|
429
|
+
await db.execute(
|
|
430
|
+
"""
|
|
431
|
+
INSERT OR IGNORE INTO graph_nodes
|
|
432
|
+
(id, type, name, qualified_name, file_path, metadata, project_id,
|
|
433
|
+
created_at, updated_at, last_seen_at)
|
|
434
|
+
VALUES (?, 'project', ?, ?, NULL, '{}', ?, datetime('now'), datetime('now'), ?)
|
|
435
|
+
""",
|
|
436
|
+
(project_node_id, project_slug, f"project.{project_slug}", project_slug, now_iso),
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
placeholders = ",".join("?" for _ in candidate_paths)
|
|
440
|
+
async with db.execute(
|
|
441
|
+
f"""
|
|
442
|
+
SELECT id, type
|
|
443
|
+
FROM graph_nodes
|
|
444
|
+
WHERE project_id = ?
|
|
445
|
+
AND file_path IN ({placeholders})
|
|
446
|
+
AND deprecated_at IS NULL
|
|
447
|
+
ORDER BY
|
|
448
|
+
CASE
|
|
449
|
+
WHEN id LIKE 'xlsx:artifact:%' THEN 0
|
|
450
|
+
WHEN type != 'file' THEN 1
|
|
451
|
+
ELSE 2
|
|
452
|
+
END
|
|
453
|
+
LIMIT 1
|
|
454
|
+
""",
|
|
455
|
+
(project_slug, *candidate_paths),
|
|
456
|
+
) as cursor:
|
|
457
|
+
node = await cursor.fetchone()
|
|
458
|
+
|
|
459
|
+
if node is None:
|
|
460
|
+
node_type = document_type if document_type in _NODE_TYPE_ALLOWLIST else "file"
|
|
461
|
+
artifact_node_id = _fallback_node_id(node_type, project_slug, target_path)
|
|
462
|
+
await db.execute(
|
|
463
|
+
"""
|
|
464
|
+
INSERT OR IGNORE INTO graph_nodes
|
|
465
|
+
(id, type, name, qualified_name, file_path, metadata, project_id,
|
|
466
|
+
created_at, updated_at, last_seen_at)
|
|
467
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'), ?)
|
|
468
|
+
""",
|
|
469
|
+
(
|
|
470
|
+
artifact_node_id,
|
|
471
|
+
node_type,
|
|
472
|
+
target_path.name,
|
|
473
|
+
f"{node_type}.{target_path.stem}",
|
|
474
|
+
str(target_path),
|
|
475
|
+
metadata_json,
|
|
476
|
+
project_slug,
|
|
477
|
+
now_iso,
|
|
478
|
+
),
|
|
479
|
+
)
|
|
480
|
+
else:
|
|
481
|
+
artifact_node_id = node["id"]
|
|
482
|
+
|
|
483
|
+
await db.execute(
|
|
484
|
+
"""
|
|
485
|
+
INSERT INTO graph_edges
|
|
486
|
+
(source_id, target_id, relation, confidence, source, metadata,
|
|
487
|
+
source_file, created_at, first_seen_at, last_seen_at, project_id,
|
|
488
|
+
weight, last_touched_at)
|
|
489
|
+
VALUES (?, ?, 'contains', 1.0, 'db', ?, ?, datetime('now'), ?, ?, ?, 1.0, ?)
|
|
490
|
+
ON CONFLICT(source_id, target_id, relation) DO UPDATE SET
|
|
491
|
+
metadata = excluded.metadata,
|
|
492
|
+
last_seen_at = excluded.last_seen_at,
|
|
493
|
+
weight = 1.0,
|
|
494
|
+
last_touched_at = excluded.last_touched_at
|
|
495
|
+
""",
|
|
496
|
+
(
|
|
497
|
+
project_node_id,
|
|
498
|
+
artifact_node_id,
|
|
499
|
+
metadata_json,
|
|
500
|
+
str(target_path),
|
|
501
|
+
now_iso,
|
|
502
|
+
now_iso,
|
|
503
|
+
project_slug,
|
|
504
|
+
now_iso,
|
|
505
|
+
),
|
|
506
|
+
)
|
|
507
|
+
await db.execute(
|
|
508
|
+
"""
|
|
509
|
+
INSERT INTO kg_edge_activity
|
|
510
|
+
(edge_source_id, edge_target_id, edge_relation, event_type,
|
|
511
|
+
source_agent, metadata_json)
|
|
512
|
+
VALUES (?, ?, 'contains', 'ingest_insert', 'pir-ingest', ?)
|
|
513
|
+
""",
|
|
514
|
+
(project_node_id, artifact_node_id, metadata_json),
|
|
515
|
+
)
|
|
516
|
+
await db.commit()
|
|
517
|
+
return artifact_node_id
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
async def _load_ingest_row(ingest_id: str):
|
|
521
|
+
async with acquire_write_db() as db:
|
|
522
|
+
async with db.execute(
|
|
523
|
+
"SELECT * FROM ingest_pending WHERE id = ?", (ingest_id,)
|
|
524
|
+
) as cursor:
|
|
525
|
+
return await cursor.fetchone()
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _target_path_from_inserted_row(row) -> Path:
|
|
529
|
+
project_root = _project_root(row["project_slug"])
|
|
530
|
+
classification = json.loads(row["classification_json"] or "{}")
|
|
531
|
+
target_folder = row["target_folder"] or classification.get("target_folder")
|
|
532
|
+
target_filename = row["target_filename"] or classification.get("target_filename")
|
|
533
|
+
if not target_folder or not target_filename:
|
|
534
|
+
raise ValueError("missing target_folder/target_filename")
|
|
535
|
+
folder = _safe_relative_path(str(target_folder))
|
|
536
|
+
filename = _safe_relative_path(str(target_filename))
|
|
537
|
+
if len(filename.parts) != 1:
|
|
538
|
+
raise ValueError("target_filename must be a basename")
|
|
539
|
+
target_path = (project_root / folder / filename.name).resolve()
|
|
540
|
+
if not target_path.is_relative_to(project_root):
|
|
541
|
+
raise ValueError("target path escapes project root")
|
|
542
|
+
return target_path
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
async def _finish_inserted_row(row) -> None:
|
|
546
|
+
project_slug = row["project_slug"]
|
|
547
|
+
target_path = _target_path_from_inserted_row(row)
|
|
548
|
+
if not target_path.exists():
|
|
549
|
+
raise FileNotFoundError(str(target_path))
|
|
550
|
+
|
|
551
|
+
project_type, _repo_path = _load_project_entry(project_slug)
|
|
552
|
+
classification = json.loads(row["classification_json"] or "{}")
|
|
553
|
+
populator_warnings: list[str] = []
|
|
554
|
+
for module in ("core.scripts.populate_artifacts", "core.scripts.populate_cross_project"):
|
|
555
|
+
ok, message = await _run_populator(module, target_path)
|
|
556
|
+
if not ok:
|
|
557
|
+
populator_warnings.append(f"{module}: {message}")
|
|
558
|
+
|
|
559
|
+
await _ensure_kg_edge(
|
|
560
|
+
ingest_id=row["id"],
|
|
561
|
+
project_slug=project_slug,
|
|
562
|
+
target_path=target_path,
|
|
563
|
+
document_type=str(classification.get("type") or "file"),
|
|
564
|
+
classification=classification,
|
|
565
|
+
populator_warnings=populator_warnings,
|
|
566
|
+
)
|
|
567
|
+
embed_status = await embed_and_index(
|
|
568
|
+
ingest_id=row["id"],
|
|
569
|
+
slug=project_slug,
|
|
570
|
+
target_path=target_path,
|
|
571
|
+
extracted_text=row["extracted_text"],
|
|
572
|
+
document_type=str(classification.get("type") or "file"),
|
|
573
|
+
title=str(classification.get("title") or target_path.stem),
|
|
574
|
+
project_type=project_type,
|
|
575
|
+
)
|
|
576
|
+
logger.info("saga recovery embed: ingest_id=%s status=%s", row["id"], embed_status)
|
|
577
|
+
|
|
578
|
+
error_message = "; ".join(populator_warnings)[:1000] if populator_warnings else None
|
|
579
|
+
async with acquire_write_db() as db:
|
|
580
|
+
await db.execute(
|
|
581
|
+
"""
|
|
582
|
+
UPDATE ingest_pending
|
|
583
|
+
SET status = 'done',
|
|
584
|
+
error_message = ?,
|
|
585
|
+
updated_at = datetime('now')
|
|
586
|
+
WHERE id = ?
|
|
587
|
+
AND status = 'inserted'
|
|
588
|
+
""",
|
|
589
|
+
(error_message, row["id"]),
|
|
590
|
+
)
|
|
591
|
+
await db.commit()
|
|
592
|
+
await broadcast_ingest_changed(
|
|
593
|
+
"done",
|
|
594
|
+
ingest_id=row["id"],
|
|
595
|
+
project_slug=project_slug,
|
|
596
|
+
status="done",
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
async def resume_saga_from_status(
|
|
601
|
+
ingest_id: str,
|
|
602
|
+
prev_status: str,
|
|
603
|
+
timeout: int | None = None,
|
|
604
|
+
) -> None:
|
|
605
|
+
"""Idempotently resume a stale ingest saga row from the live schema states."""
|
|
606
|
+
step_timeout = timeout or int(os.environ.get("INGEST_STEP_TIMEOUT_SECONDS", "60"))
|
|
607
|
+
if prev_status == "approved":
|
|
608
|
+
await asyncio.wait_for(execute_saga(ingest_id), timeout=step_timeout)
|
|
609
|
+
return
|
|
610
|
+
if prev_status == "inserted":
|
|
611
|
+
row = await _load_ingest_row(ingest_id)
|
|
612
|
+
if row is None or row["status"] != "inserted":
|
|
613
|
+
return
|
|
614
|
+
await asyncio.wait_for(_finish_inserted_row(row), timeout=step_timeout)
|
|
615
|
+
return
|
|
616
|
+
raise ValueError(f"unsupported ingest recovery status: {prev_status}")
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
async def execute_saga(ingest_id: str) -> None:
|
|
620
|
+
row = None
|
|
621
|
+
async with acquire_write_db() as db:
|
|
622
|
+
async with db.execute(
|
|
623
|
+
"SELECT * FROM ingest_pending WHERE id = ?", (ingest_id,)
|
|
624
|
+
) as cursor:
|
|
625
|
+
row = await cursor.fetchone()
|
|
626
|
+
if row is None or row["status"] != "approved":
|
|
627
|
+
return
|
|
628
|
+
# U3 defense-in-depth: an api_ingress row must never AUTO-insert (no human
|
|
629
|
+
# in the loop) unless its key policy is 'trusted'. This backstop guards
|
|
630
|
+
# AUTO-insert only — explicit human triage approval (basis "approve:<user>")
|
|
631
|
+
# is an insertion authorization and is always honored, so the primary
|
|
632
|
+
# 'open' flow (triage -> human approve -> inserted) works. The parse-time
|
|
633
|
+
# policy gate already enforces this for the auto path; default-deny here.
|
|
634
|
+
basis = row["triage_decision_id"] or ""
|
|
635
|
+
if (
|
|
636
|
+
row["source_kind"] == "api_ingress"
|
|
637
|
+
and row["ingest_policy"] != "trusted"
|
|
638
|
+
and not basis.startswith("approve:")
|
|
639
|
+
):
|
|
640
|
+
logger.error(
|
|
641
|
+
"saga refused api_ingress auto-insert for row %s: policy=%r is not "
|
|
642
|
+
"'trusted' (basis=%r); open/unknown keys auto-insert only via human triage",
|
|
643
|
+
ingest_id,
|
|
644
|
+
row["ingest_policy"],
|
|
645
|
+
basis,
|
|
646
|
+
)
|
|
647
|
+
return
|
|
648
|
+
|
|
649
|
+
project_slug = row["project_slug"]
|
|
650
|
+
try:
|
|
651
|
+
project_root = _project_root(project_slug)
|
|
652
|
+
project_type, repo_path = _load_project_entry(project_slug)
|
|
653
|
+
logger.info(
|
|
654
|
+
"saga branching: ingest_id=%s project=%s project_type=%s",
|
|
655
|
+
ingest_id,
|
|
656
|
+
project_slug,
|
|
657
|
+
project_type,
|
|
658
|
+
)
|
|
659
|
+
source_path = Path(row["file_path"]).resolve()
|
|
660
|
+
if not source_path.exists():
|
|
661
|
+
raise FileNotFoundError(str(source_path))
|
|
662
|
+
if not source_path.is_relative_to(project_root):
|
|
663
|
+
raise ValueError("source path escapes project root")
|
|
664
|
+
|
|
665
|
+
classification = json.loads(row["classification_json"] or "{}")
|
|
666
|
+
target_folder = row["target_folder"] or classification.get("target_folder")
|
|
667
|
+
target_filename = row["target_filename"] or classification.get("target_filename")
|
|
668
|
+
if not target_folder or not target_filename:
|
|
669
|
+
raise ValueError("missing target_folder/target_filename")
|
|
670
|
+
|
|
671
|
+
target_path = _unique_target_path(
|
|
672
|
+
project_root,
|
|
673
|
+
str(target_folder),
|
|
674
|
+
str(target_filename),
|
|
675
|
+
row["sha256"],
|
|
676
|
+
)
|
|
677
|
+
_materialize_target_file(
|
|
678
|
+
source_path=source_path,
|
|
679
|
+
target_path=target_path,
|
|
680
|
+
row=row,
|
|
681
|
+
classification=classification,
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
commit_created = False
|
|
685
|
+
if project_type in ("code", "system"):
|
|
686
|
+
try:
|
|
687
|
+
commit_created = _commit_ingested_file(
|
|
688
|
+
slug=project_slug,
|
|
689
|
+
repo_path=repo_path,
|
|
690
|
+
target_path=target_path,
|
|
691
|
+
target_filename=target_path.name,
|
|
692
|
+
)
|
|
693
|
+
except Exception as exc:
|
|
694
|
+
logger.warning("ingest git commit blocked: %s", exc)
|
|
695
|
+
await _set_saga_error(ingest_id, project_slug, f"workspace guard: {exc}")
|
|
696
|
+
return
|
|
697
|
+
|
|
698
|
+
async with acquire_write_db() as db:
|
|
699
|
+
await db.execute(
|
|
700
|
+
"""
|
|
701
|
+
UPDATE ingest_pending
|
|
702
|
+
SET status = 'inserted',
|
|
703
|
+
target_folder = ?,
|
|
704
|
+
target_filename = ?,
|
|
705
|
+
updated_at = datetime('now')
|
|
706
|
+
WHERE id = ?
|
|
707
|
+
""",
|
|
708
|
+
(
|
|
709
|
+
str(target_path.parent.relative_to(project_root)),
|
|
710
|
+
target_path.name,
|
|
711
|
+
ingest_id,
|
|
712
|
+
),
|
|
713
|
+
)
|
|
714
|
+
await db.commit()
|
|
715
|
+
|
|
716
|
+
populator_warnings: list[str] = []
|
|
717
|
+
if commit_created:
|
|
718
|
+
await asyncio.sleep(1.5)
|
|
719
|
+
else:
|
|
720
|
+
for module in ("core.scripts.populate_artifacts", "core.scripts.populate_cross_project"):
|
|
721
|
+
ok, message = await _run_populator(module, target_path)
|
|
722
|
+
if not ok:
|
|
723
|
+
populator_warnings.append(f"{module}: {message}")
|
|
724
|
+
|
|
725
|
+
artifact_node_id = await _ensure_kg_edge(
|
|
726
|
+
ingest_id=ingest_id,
|
|
727
|
+
project_slug=project_slug,
|
|
728
|
+
target_path=target_path,
|
|
729
|
+
document_type=str(classification.get("type") or "file"),
|
|
730
|
+
classification=classification,
|
|
731
|
+
populator_warnings=populator_warnings,
|
|
732
|
+
)
|
|
733
|
+
embed_status = await embed_and_index(
|
|
734
|
+
ingest_id=ingest_id,
|
|
735
|
+
slug=project_slug,
|
|
736
|
+
target_path=target_path,
|
|
737
|
+
extracted_text=row["extracted_text"],
|
|
738
|
+
document_type=str(classification.get("type") or "file"),
|
|
739
|
+
title=str(classification.get("title") or target_path.stem),
|
|
740
|
+
project_type=project_type,
|
|
741
|
+
)
|
|
742
|
+
logger.info("saga embed: ingest_id=%s status=%s", ingest_id, embed_status)
|
|
743
|
+
|
|
744
|
+
error_message = "; ".join(populator_warnings)[:1000] if populator_warnings else None
|
|
745
|
+
async with acquire_write_db() as db:
|
|
746
|
+
await db.execute(
|
|
747
|
+
"""
|
|
748
|
+
UPDATE ingest_pending
|
|
749
|
+
SET status = 'done',
|
|
750
|
+
error_message = ?,
|
|
751
|
+
updated_at = datetime('now')
|
|
752
|
+
WHERE id = ?
|
|
753
|
+
""",
|
|
754
|
+
(error_message, ingest_id),
|
|
755
|
+
)
|
|
756
|
+
await db.commit()
|
|
757
|
+
await broadcast_ingest_changed(
|
|
758
|
+
"done",
|
|
759
|
+
ingest_id=ingest_id,
|
|
760
|
+
project_slug=project_slug,
|
|
761
|
+
status="done",
|
|
762
|
+
)
|
|
763
|
+
# Phase 1.5 E6: KG edge enrichment background (LLM #2). Fire-and-forget,
|
|
764
|
+
# never raises. Guarded by LLM_KG_ENRICHER_ENABLED env. Cron Phase 2 future
|
|
765
|
+
# re-triggers nodes with kg_enriched_at IS NULL.
|
|
766
|
+
if artifact_node_id and (
|
|
767
|
+
(os.environ.get("LLM_KG_ENRICHER_ENABLED", "false") or "").strip().lower() == "true"
|
|
768
|
+
):
|
|
769
|
+
from core.api.services.ingest.llm.kg_enricher import enrich_kg_for_node
|
|
770
|
+
|
|
771
|
+
task = asyncio.create_task(enrich_kg_for_node(artifact_node_id))
|
|
772
|
+
_PENDING_ENRICHMENT_TASKS.add(task)
|
|
773
|
+
task.add_done_callback(_PENDING_ENRICHMENT_TASKS.discard)
|
|
774
|
+
except Exception as exc:
|
|
775
|
+
logger.exception("ingest saga failed: id=%s", ingest_id)
|
|
776
|
+
async with acquire_write_db() as db:
|
|
777
|
+
await db.execute(
|
|
778
|
+
"""
|
|
779
|
+
UPDATE ingest_pending
|
|
780
|
+
SET status = 'parse_error',
|
|
781
|
+
error_message = ?,
|
|
782
|
+
updated_at = datetime('now')
|
|
783
|
+
WHERE id = ?
|
|
784
|
+
""",
|
|
785
|
+
(str(exc)[:1000], ingest_id),
|
|
786
|
+
)
|
|
787
|
+
await db.commit()
|
|
788
|
+
await broadcast_ingest_changed(
|
|
789
|
+
"saga_error",
|
|
790
|
+
ingest_id=ingest_id,
|
|
791
|
+
project_slug=project_slug,
|
|
792
|
+
status="parse_error",
|
|
793
|
+
)
|