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,393 @@
|
|
|
1
|
+
# Git + Knowledge Graph source collector
|
|
2
|
+
# (sub-01 §3 — `commit_changed` / `kg_changed` / `doc_changed`).
|
|
3
|
+
#
|
|
4
|
+
# Three streams collapsed into a single collector because they share the
|
|
5
|
+
# same substrate (filesystem + graph nodes) and the same cycle math. Strict
|
|
6
|
+
# anti-patterns enforced (regression-guarded by the no-neighbors test):
|
|
7
|
+
# * Per-project neighbor expansion → N+1 disaster, banned.
|
|
8
|
+
# Use graph_service.get_hotspots instead.
|
|
9
|
+
# * Direct SELECT against the graph node table in this module → banned.
|
|
10
|
+
# Read paths live in api.services.graph_service.
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
from collections.abc import AsyncIterator
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from core.api.db import acquire_db
|
|
21
|
+
from core.api.paths import repo_root
|
|
22
|
+
from core.api.services import graph_service
|
|
23
|
+
from core.api.services.brain.digest_collector import SourceCollector
|
|
24
|
+
from core.api.services.brain.models import EventDraft, SourceCollectorContext
|
|
25
|
+
from core.api.services.brain.sources.base import (
|
|
26
|
+
normalize_iso,
|
|
27
|
+
resolve_source_project,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ----------------------------------------------------------------------
|
|
34
|
+
# Tunables (kept inline rather than promoted to settings — the plan §3
|
|
35
|
+
# fixes them explicitly and lifting them to app_settings would create
|
|
36
|
+
# another knob that drifts).
|
|
37
|
+
# ----------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
COMMIT_CAP_PER_CYCLE: int = 100
|
|
40
|
+
KG_HOTSPOT_LIMIT: int = 20
|
|
41
|
+
KG_HOTSPOT_WINDOW = "30d"
|
|
42
|
+
DOC_NODE_KINDS: frozenset[str] = frozenset(
|
|
43
|
+
{
|
|
44
|
+
"handoff",
|
|
45
|
+
"solution",
|
|
46
|
+
"guide",
|
|
47
|
+
"plan",
|
|
48
|
+
"brainstorm",
|
|
49
|
+
"audit",
|
|
50
|
+
"spike",
|
|
51
|
+
"analysis",
|
|
52
|
+
"research",
|
|
53
|
+
"rubric",
|
|
54
|
+
"report",
|
|
55
|
+
"policy",
|
|
56
|
+
"contract",
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def _resolve_repo_root() -> Path:
|
|
61
|
+
"""Resolve the git repository root for commit_changed scans.
|
|
62
|
+
|
|
63
|
+
Order of precedence:
|
|
64
|
+
1. ``BRAIN_GIT_REPO_ROOT`` env var (explicit override, used in prod where
|
|
65
|
+
pir-api is deployed under ``/data/pir/api/`` and the source repo lives
|
|
66
|
+
elsewhere — typically ``~/workspace``).
|
|
67
|
+
2. ``parents[3]`` of this file (development default — works when pir-api
|
|
68
|
+
runs from the monorepo checkout, e.g. CI or dev workstation).
|
|
69
|
+
|
|
70
|
+
The function returns a Path; callers still gate ``git log`` execution on
|
|
71
|
+
``(repo_root / ".git").exists()`` so a wrong value degrades to an empty
|
|
72
|
+
event stream (collector continues, no crash).
|
|
73
|
+
"""
|
|
74
|
+
env_override = os.getenv("BRAIN_GIT_REPO_ROOT")
|
|
75
|
+
if env_override:
|
|
76
|
+
return Path(env_override).expanduser().resolve()
|
|
77
|
+
return repo_root(__file__)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
_REPO_ROOT = _resolve_repo_root()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ----------------------------------------------------------------------
|
|
84
|
+
# commit_changed
|
|
85
|
+
# ----------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
_GIT_LOG_FORMAT = "%H%x1f%aI%x1f%an%x1f%s"
|
|
89
|
+
_GIT_LOG_SEP = "\x1f"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def _git_log_window(
|
|
93
|
+
*,
|
|
94
|
+
since: datetime,
|
|
95
|
+
until: datetime,
|
|
96
|
+
cwd: Path,
|
|
97
|
+
cap: int,
|
|
98
|
+
) -> list[dict[str, str]]:
|
|
99
|
+
"""Run `git log` for the cycle window.
|
|
100
|
+
|
|
101
|
+
Returns at most `cap` commits ordered ASC by commit time so the watermark
|
|
102
|
+
advances monotonically. Failure to invoke git (binary missing, non-repo)
|
|
103
|
+
surfaces as an empty list — the collector continues and the partial
|
|
104
|
+
failure tracking happens at the cycle level via try/except.
|
|
105
|
+
"""
|
|
106
|
+
if not (cwd / ".git").exists():
|
|
107
|
+
return []
|
|
108
|
+
try:
|
|
109
|
+
proc = await asyncio.create_subprocess_exec(
|
|
110
|
+
"git",
|
|
111
|
+
"log",
|
|
112
|
+
f"--since={since.astimezone(timezone.utc).isoformat()}",
|
|
113
|
+
f"--until={until.astimezone(timezone.utc).isoformat()}",
|
|
114
|
+
f"--format={_GIT_LOG_FORMAT}",
|
|
115
|
+
"--reverse",
|
|
116
|
+
f"--max-count={cap}",
|
|
117
|
+
"--no-merges",
|
|
118
|
+
cwd=str(cwd),
|
|
119
|
+
stdout=asyncio.subprocess.PIPE,
|
|
120
|
+
stderr=asyncio.subprocess.PIPE,
|
|
121
|
+
)
|
|
122
|
+
except FileNotFoundError:
|
|
123
|
+
return []
|
|
124
|
+
try:
|
|
125
|
+
stdout_b, _stderr_b = await asyncio.wait_for(proc.communicate(), timeout=10.0)
|
|
126
|
+
except asyncio.TimeoutError:
|
|
127
|
+
proc.kill()
|
|
128
|
+
await proc.wait()
|
|
129
|
+
return []
|
|
130
|
+
|
|
131
|
+
rows: list[dict[str, str]] = []
|
|
132
|
+
for line in stdout_b.decode("utf-8", errors="replace").splitlines():
|
|
133
|
+
parts = line.split(_GIT_LOG_SEP, 3)
|
|
134
|
+
if len(parts) != 4:
|
|
135
|
+
continue
|
|
136
|
+
sha, author_iso, author_name, subject = parts
|
|
137
|
+
rows.append(
|
|
138
|
+
{
|
|
139
|
+
"sha": sha,
|
|
140
|
+
"author_iso": author_iso,
|
|
141
|
+
"author_name": author_name,
|
|
142
|
+
"subject": subject,
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
return rows
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _commit_event(
|
|
149
|
+
*, sha: str, author_iso: str, author_name: str, subject: str, now: datetime
|
|
150
|
+
) -> EventDraft:
|
|
151
|
+
observed_at = normalize_iso(author_iso) or now
|
|
152
|
+
evidence = {
|
|
153
|
+
"sha": sha,
|
|
154
|
+
"subject": subject[:200],
|
|
155
|
+
"author": author_name,
|
|
156
|
+
"author_iso": observed_at.isoformat(),
|
|
157
|
+
}
|
|
158
|
+
return EventDraft(
|
|
159
|
+
event_type="commit_changed",
|
|
160
|
+
source_system="git",
|
|
161
|
+
source_ref=f"commit:{sha}",
|
|
162
|
+
title=subject[:200] or f"commit {sha[:7]}",
|
|
163
|
+
summary=f"{author_name}: {subject[:160]}",
|
|
164
|
+
observed_at=observed_at,
|
|
165
|
+
derived_from_state_at=observed_at,
|
|
166
|
+
evidence=evidence,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ----------------------------------------------------------------------
|
|
171
|
+
# kg_changed (via graph_service.get_hotspots — NEVER per-project neighbor scan)
|
|
172
|
+
# ----------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _node_id_for(node_type: str, qualified_name: str) -> str:
|
|
176
|
+
"""Mirror KG id convention so source_ref ↔ KG node_id round-trips."""
|
|
177
|
+
prefix_map = {
|
|
178
|
+
"function": "py",
|
|
179
|
+
"file": "py",
|
|
180
|
+
"module": "py",
|
|
181
|
+
}
|
|
182
|
+
prefix = prefix_map.get(node_type, node_type)
|
|
183
|
+
return f"{prefix}:{node_type}:{qualified_name}"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _kg_event(*, row: dict, now: datetime) -> EventDraft | None:
|
|
187
|
+
touched_at = normalize_iso(row.get("touch_last_at"))
|
|
188
|
+
if touched_at is None:
|
|
189
|
+
return None
|
|
190
|
+
node_id = row.get("id") or _node_id_for(row["type"], row.get("qualified_name", ""))
|
|
191
|
+
project, program = resolve_source_project(row.get("project_id"))
|
|
192
|
+
evidence = {
|
|
193
|
+
"node_id": node_id,
|
|
194
|
+
"type": row.get("type"),
|
|
195
|
+
"touch_count_30d": row.get("touch_count_30d", 0),
|
|
196
|
+
"touch_count_7d": row.get("touch_count_7d", 0),
|
|
197
|
+
"touch_last_at": touched_at.isoformat(),
|
|
198
|
+
}
|
|
199
|
+
return EventDraft(
|
|
200
|
+
event_type="kg_changed",
|
|
201
|
+
source_system="kg",
|
|
202
|
+
source_ref=node_id,
|
|
203
|
+
title=str(row.get("name") or node_id)[:200],
|
|
204
|
+
summary=f"KG hotspot {node_id}: {row.get('touch_count_30d', 0)}/30d touches",
|
|
205
|
+
observed_at=touched_at,
|
|
206
|
+
derived_from_state_at=touched_at,
|
|
207
|
+
evidence=evidence,
|
|
208
|
+
source_project=project,
|
|
209
|
+
program_key=program,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ----------------------------------------------------------------------
|
|
214
|
+
# doc_changed (graph_nodes via graph_service helper — no direct SQL)
|
|
215
|
+
# ----------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
async def _list_doc_nodes_changed_since(
|
|
219
|
+
*, since: datetime, limit: int = 200
|
|
220
|
+
) -> list[dict]:
|
|
221
|
+
"""Read doc-kind nodes whose touch_last_at advanced past the watermark.
|
|
222
|
+
|
|
223
|
+
Routed through graph_service.find_recently_touched_doc_nodes so this
|
|
224
|
+
collector stays free of direct SQL against the graph node table
|
|
225
|
+
(anti-pattern enforced via grep on the file).
|
|
226
|
+
"""
|
|
227
|
+
fetcher = getattr(graph_service, "find_recently_touched_doc_nodes", None)
|
|
228
|
+
if fetcher is None:
|
|
229
|
+
# Fall back to no-op rather than the banned direct query — tests
|
|
230
|
+
# cover this branch explicitly so the absence is visible in CI.
|
|
231
|
+
return []
|
|
232
|
+
return await fetcher(
|
|
233
|
+
since_iso=since.astimezone(timezone.utc).isoformat(),
|
|
234
|
+
kinds=tuple(sorted(DOC_NODE_KINDS)),
|
|
235
|
+
limit=limit,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _doc_event(*, row: dict, now: datetime) -> EventDraft | None:
|
|
240
|
+
touched_at = normalize_iso(row.get("touch_last_at"))
|
|
241
|
+
if touched_at is None:
|
|
242
|
+
return None
|
|
243
|
+
node_id = row.get("id")
|
|
244
|
+
path = row.get("file_path") or row.get("name") or node_id
|
|
245
|
+
last_sha = row.get("last_modified_git_sha") or row.get("first_seen_git_sha") or ""
|
|
246
|
+
project, program = resolve_source_project(row.get("project_id"))
|
|
247
|
+
evidence = {
|
|
248
|
+
"node_id": node_id,
|
|
249
|
+
"kind": row.get("type"),
|
|
250
|
+
"path": path,
|
|
251
|
+
"sha": last_sha,
|
|
252
|
+
"touch_last_at": touched_at.isoformat(),
|
|
253
|
+
}
|
|
254
|
+
return EventDraft(
|
|
255
|
+
event_type="doc_changed",
|
|
256
|
+
source_system="kg",
|
|
257
|
+
source_ref=f"doc:{path}@{last_sha[:12] if last_sha else 'untracked'}",
|
|
258
|
+
title=str(row.get("name") or path)[:200],
|
|
259
|
+
summary=f"Doc {row.get('type')} updated: {path}",
|
|
260
|
+
observed_at=touched_at,
|
|
261
|
+
derived_from_state_at=touched_at,
|
|
262
|
+
evidence=evidence,
|
|
263
|
+
source_project=project,
|
|
264
|
+
program_key=program,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ----------------------------------------------------------------------
|
|
269
|
+
# Collector entry point
|
|
270
|
+
# ----------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _resolve_git_window(ctx: SourceCollectorContext) -> tuple[datetime, datetime]:
|
|
274
|
+
"""Return (since, until) for ``git log`` — clamped to the cycle window.
|
|
275
|
+
|
|
276
|
+
Bug fix 2026-05-18: collectors must honor ``cycle_window_*`` instead of
|
|
277
|
+
blindly reading from the watermark up to ``cutoff_at``. For backward-
|
|
278
|
+
compat (periodic path with no window bounds) this collapses to the
|
|
279
|
+
legacy ``(watermark, cutoff_at]`` slice.
|
|
280
|
+
"""
|
|
281
|
+
if (
|
|
282
|
+
ctx.cycle_window_start is not None
|
|
283
|
+
and ctx.cycle_window_start > ctx.since_watermark
|
|
284
|
+
):
|
|
285
|
+
since = ctx.cycle_window_start
|
|
286
|
+
else:
|
|
287
|
+
since = ctx.since_watermark
|
|
288
|
+
if (
|
|
289
|
+
ctx.cycle_window_end is not None
|
|
290
|
+
and ctx.cycle_window_end < ctx.cutoff_at
|
|
291
|
+
):
|
|
292
|
+
until = ctx.cycle_window_end
|
|
293
|
+
else:
|
|
294
|
+
until = ctx.cutoff_at
|
|
295
|
+
return since, until
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
async def collect_git(ctx: SourceCollectorContext) -> AsyncIterator[EventDraft]:
|
|
299
|
+
"""Git commit_changed collector — source_system='git'.
|
|
300
|
+
|
|
301
|
+
Repo root resolution order:
|
|
302
|
+
1. ``BRAIN_GIT_REPO_ROOT`` env var (production: pir-api deployed under
|
|
303
|
+
/data/pir/api/ → repo lives at ~/workspace).
|
|
304
|
+
2. Module-level ``_REPO_ROOT`` (dev default + monkeypatchable in tests).
|
|
305
|
+
|
|
306
|
+
Cycle window: bounded via ``ctx.cycle_window_start`` / ``cycle_window_end``
|
|
307
|
+
when present, otherwise falls back to ``(since_watermark, cutoff_at]``.
|
|
308
|
+
"""
|
|
309
|
+
env_override = os.getenv("BRAIN_GIT_REPO_ROOT")
|
|
310
|
+
repo_root = (
|
|
311
|
+
Path(env_override).expanduser().resolve() if env_override else _REPO_ROOT
|
|
312
|
+
)
|
|
313
|
+
since, until = _resolve_git_window(ctx)
|
|
314
|
+
commits = await _git_log_window(
|
|
315
|
+
since=since,
|
|
316
|
+
until=until,
|
|
317
|
+
cwd=repo_root,
|
|
318
|
+
cap=COMMIT_CAP_PER_CYCLE,
|
|
319
|
+
)
|
|
320
|
+
for commit in commits:
|
|
321
|
+
observed_at = normalize_iso(commit["author_iso"])
|
|
322
|
+
if observed_at is not None and not ctx.in_window(observed_at):
|
|
323
|
+
continue
|
|
324
|
+
yield _commit_event(
|
|
325
|
+
sha=commit["sha"],
|
|
326
|
+
author_iso=commit["author_iso"],
|
|
327
|
+
author_name=commit["author_name"],
|
|
328
|
+
subject=commit["subject"],
|
|
329
|
+
now=ctx.now,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
async def collect_kg(ctx: SourceCollectorContext) -> AsyncIterator[EventDraft]:
|
|
334
|
+
"""KG kg_changed (hotspots) + doc_changed (doc nodes) — source_system='kg'.
|
|
335
|
+
|
|
336
|
+
Cycle window enforcement via ``ctx.in_window`` (bug fix 2026-05-18).
|
|
337
|
+
"""
|
|
338
|
+
# kg_changed (hotspot delta projection)
|
|
339
|
+
async with acquire_db() as db:
|
|
340
|
+
hotspots = await graph_service.get_hotspots(
|
|
341
|
+
db,
|
|
342
|
+
window=KG_HOTSPOT_WINDOW,
|
|
343
|
+
limit=KG_HOTSPOT_LIMIT,
|
|
344
|
+
type_filter="file",
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
for row in hotspots:
|
|
348
|
+
touched_at = normalize_iso(row.get("touch_last_at"))
|
|
349
|
+
if touched_at is None or not ctx.in_window(touched_at):
|
|
350
|
+
continue
|
|
351
|
+
evt = _kg_event(row=row, now=ctx.now)
|
|
352
|
+
if evt is not None:
|
|
353
|
+
yield evt
|
|
354
|
+
|
|
355
|
+
# doc_changed — fetch since the lower bound (max(watermark, window_start))
|
|
356
|
+
since_doc, _until_doc = _resolve_git_window(ctx)
|
|
357
|
+
try:
|
|
358
|
+
doc_rows = await _list_doc_nodes_changed_since(since=since_doc)
|
|
359
|
+
except Exception: # pragma: no cover — helper missing or KG offline
|
|
360
|
+
logger.exception("brain.sources.git_kg: doc_changed fetch failed")
|
|
361
|
+
doc_rows = []
|
|
362
|
+
|
|
363
|
+
for row in doc_rows:
|
|
364
|
+
touched_at = normalize_iso(row.get("touch_last_at"))
|
|
365
|
+
if touched_at is None or not ctx.in_window(touched_at):
|
|
366
|
+
continue
|
|
367
|
+
evt = _doc_event(row=row, now=ctx.now)
|
|
368
|
+
if evt is not None:
|
|
369
|
+
yield evt
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# Backward-compat alias for tests/code che ancora referenziano collect()
|
|
373
|
+
async def collect(ctx: SourceCollectorContext) -> AsyncIterator[EventDraft]:
|
|
374
|
+
async for evt in collect_git(ctx):
|
|
375
|
+
yield evt
|
|
376
|
+
async for evt in collect_kg(ctx):
|
|
377
|
+
yield evt
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
git_collector = SourceCollector(source_system="git", collect=collect_git)
|
|
381
|
+
kg_collector = SourceCollector(source_system="kg", collect=collect_kg)
|
|
382
|
+
# Legacy alias (NON registrare — single source_system='git_kg' viola CHECK constraint)
|
|
383
|
+
git_kg_collector = git_collector
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
__all__ = [
|
|
387
|
+
"COMMIT_CAP_PER_CYCLE",
|
|
388
|
+
"DOC_NODE_KINDS",
|
|
389
|
+
"KG_HOTSPOT_LIMIT",
|
|
390
|
+
"KG_HOTSPOT_WINDOW",
|
|
391
|
+
"collect",
|
|
392
|
+
"git_kg_collector",
|
|
393
|
+
]
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Handoffs source collector (sub-01 §3 — `handoff_changed`).
|
|
2
|
+
#
|
|
3
|
+
# Handoffs are filesystem-only: `{metadata_path}/memory/handoff-*.md`. There
|
|
4
|
+
# is no `handoff_index` table in console.db today, so the plan's "fallback
|
|
5
|
+
# to handoff_index.updated_at" is implemented as: `mtime` is authoritative;
|
|
6
|
+
# if a fallback table appears later we add a second branch here.
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from collections.abc import AsyncIterator
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
from core.api.services.brain.digest_collector import SourceCollector
|
|
17
|
+
from core.api.services.brain.models import EventDraft, SourceCollectorContext
|
|
18
|
+
from core.api.services.brain.sources.base import (
|
|
19
|
+
resolve_source_project,
|
|
20
|
+
stable_short_hash,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Per-cycle hard cap protects against an accidental bulk-touch of every
|
|
27
|
+
# handoff file (e.g. `chmod -R`, restore-from-backup). Plan §6.D0 says 1000
|
|
28
|
+
# events/source — we leave the per-source cap to the orchestrator and just
|
|
29
|
+
# bound the directory walk for safety.
|
|
30
|
+
MAX_HANDOFFS_SCANNED: int = 2000
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _iter_project_roots() -> list[Path]:
|
|
34
|
+
"""Return project metadata roots without coupling to router globals.
|
|
35
|
+
|
|
36
|
+
The Marvis convention (kb/projects.md): `/data/projects/<slug>/` plus
|
|
37
|
+
`~/workspace/projects/<slug>/`. We import lazily and tolerate missing
|
|
38
|
+
`api.routers.projects` (e.g. during unit tests that don't bootstrap the
|
|
39
|
+
full API surface).
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
from core.api.routers.projects import PROJECT_DIRS
|
|
43
|
+
|
|
44
|
+
return [p for p in PROJECT_DIRS if p.exists()]
|
|
45
|
+
except Exception: # pragma: no cover — defensive
|
|
46
|
+
defaults = [
|
|
47
|
+
Path("/data/projects"),
|
|
48
|
+
Path.home() / "workspace" / "projects",
|
|
49
|
+
]
|
|
50
|
+
return [p for p in defaults if p.exists()]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _parse_handoff_frontmatter(path: Path) -> tuple[list[str], str | None, str | None]:
|
|
54
|
+
"""Return (tags, session, slug_hint) from frontmatter; best-effort."""
|
|
55
|
+
try:
|
|
56
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
57
|
+
except OSError:
|
|
58
|
+
return [], None, None
|
|
59
|
+
if not text.startswith("---"):
|
|
60
|
+
return [], None, None
|
|
61
|
+
try:
|
|
62
|
+
_, body = text.split("---", 1)
|
|
63
|
+
meta_block, _ = body.split("---", 1)
|
|
64
|
+
meta = yaml.safe_load(meta_block) or {}
|
|
65
|
+
except (ValueError, yaml.YAMLError):
|
|
66
|
+
return [], None, None
|
|
67
|
+
if not isinstance(meta, dict):
|
|
68
|
+
return [], None, None
|
|
69
|
+
raw_tags = meta.get("tags") or []
|
|
70
|
+
if isinstance(raw_tags, str):
|
|
71
|
+
raw_tags = [raw_tags]
|
|
72
|
+
tags = [str(t) for t in raw_tags if isinstance(t, (str, int))]
|
|
73
|
+
session = (
|
|
74
|
+
str(meta.get("session"))
|
|
75
|
+
if meta.get("session") not in (None, "")
|
|
76
|
+
else None
|
|
77
|
+
)
|
|
78
|
+
slug_hint = meta.get("project") or meta.get("slug")
|
|
79
|
+
return tags, session, str(slug_hint) if slug_hint else None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _project_slug_from_path(handoff_path: Path) -> str | None:
|
|
83
|
+
"""Recover the project slug from `<root>/<slug>/memory/<file>` shape."""
|
|
84
|
+
parts = handoff_path.parts
|
|
85
|
+
if "memory" not in parts:
|
|
86
|
+
return None
|
|
87
|
+
idx = parts.index("memory")
|
|
88
|
+
return parts[idx - 1] if idx >= 1 else None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def collect(ctx: SourceCollectorContext) -> AsyncIterator[EventDraft]:
|
|
92
|
+
seen = 0
|
|
93
|
+
for root in _iter_project_roots():
|
|
94
|
+
try:
|
|
95
|
+
project_dirs = [d for d in root.iterdir() if d.is_dir()]
|
|
96
|
+
except OSError:
|
|
97
|
+
continue
|
|
98
|
+
for project_dir in project_dirs:
|
|
99
|
+
memory_dir = project_dir / "memory"
|
|
100
|
+
if not memory_dir.is_dir():
|
|
101
|
+
continue
|
|
102
|
+
try:
|
|
103
|
+
handoffs = list(memory_dir.glob("handoff-*.md"))
|
|
104
|
+
except OSError:
|
|
105
|
+
continue
|
|
106
|
+
for handoff in handoffs:
|
|
107
|
+
if seen >= MAX_HANDOFFS_SCANNED:
|
|
108
|
+
return
|
|
109
|
+
seen += 1
|
|
110
|
+
try:
|
|
111
|
+
stat = handoff.stat()
|
|
112
|
+
except OSError:
|
|
113
|
+
continue
|
|
114
|
+
mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc)
|
|
115
|
+
# Cycle window enforcement (bug fix 2026-05-18) — collapses to
|
|
116
|
+
# the legacy (watermark, cutoff] filter when no window bounds.
|
|
117
|
+
if not ctx.in_window(mtime):
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
tags, session, slug_hint = _parse_handoff_frontmatter(handoff)
|
|
121
|
+
slug = slug_hint or _project_slug_from_path(handoff) or project_dir.name
|
|
122
|
+
source_project, program_key = resolve_source_project(slug)
|
|
123
|
+
filename_hash = stable_short_hash(handoff.name)
|
|
124
|
+
source_ref = f"handoff:{slug}:{filename_hash}"
|
|
125
|
+
evidence = {
|
|
126
|
+
"path": str(handoff),
|
|
127
|
+
"mtime": mtime.isoformat(),
|
|
128
|
+
"size": stat.st_size,
|
|
129
|
+
"frontmatter_tags": sorted(tags),
|
|
130
|
+
}
|
|
131
|
+
if session:
|
|
132
|
+
evidence["session"] = session
|
|
133
|
+
|
|
134
|
+
title = handoff.name
|
|
135
|
+
summary = (
|
|
136
|
+
f"handoff {handoff.name} updated"
|
|
137
|
+
+ (f" (session {session})" if session else "")
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
yield EventDraft(
|
|
141
|
+
event_type="handoff_changed",
|
|
142
|
+
source_system="handoff",
|
|
143
|
+
source_ref=source_ref,
|
|
144
|
+
title=title[:200],
|
|
145
|
+
summary=summary[:2000],
|
|
146
|
+
observed_at=mtime,
|
|
147
|
+
derived_from_state_at=mtime,
|
|
148
|
+
evidence=evidence,
|
|
149
|
+
source_project=source_project,
|
|
150
|
+
program_key=program_key,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
handoffs_collector = SourceCollector(source_system="handoff", collect=collect)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
__all__ = ["collect", "handoffs_collector"]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Ingestor source collector (sub-01 §3 — `ingest_changed` + `external_update_seen`).
|
|
2
|
+
# Reads inbox_items rows whose updated_at falls in (watermark, cutoff_at]
|
|
3
|
+
# and yields one event per row. No business state is mutated.
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import logging
|
|
8
|
+
from collections.abc import AsyncIterator
|
|
9
|
+
|
|
10
|
+
from core.api.db import acquire_db
|
|
11
|
+
from core.api.services.brain.digest_collector import SourceCollector
|
|
12
|
+
from core.api.services.brain.models import EventDraft, SourceCollectorContext
|
|
13
|
+
from core.api.services.brain.sources.base import (
|
|
14
|
+
normalize_iso,
|
|
15
|
+
resolve_source_project,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Inbox sources that represent EXTERNAL feeds (RSS, email, web). These map to
|
|
22
|
+
# `external_update_seen` per plan §3 so the Drift layer can distinguish "user
|
|
23
|
+
# uploaded something" from "we noticed an upstream feed changed". Anything
|
|
24
|
+
# else (manual upload, API ingest, gmail-marvisx) maps to `ingest_changed`.
|
|
25
|
+
_EXTERNAL_SOURCES: frozenset[str] = frozenset({"rss", "email", "web", "external"})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _classify_event_type(source_value: str | None) -> str:
|
|
29
|
+
if not source_value:
|
|
30
|
+
return "ingest_changed"
|
|
31
|
+
return (
|
|
32
|
+
"external_update_seen"
|
|
33
|
+
if source_value.lower() in _EXTERNAL_SOURCES
|
|
34
|
+
else "ingest_changed"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _source_ref(row_id: str, source_value: str | None, url: str | None) -> str:
|
|
39
|
+
if (source_value or "").lower() in _EXTERNAL_SOURCES and url:
|
|
40
|
+
url_hash = hashlib.blake2b(url.encode("utf-8"), digest_size=8).hexdigest()
|
|
41
|
+
return f"external:{url_hash}"
|
|
42
|
+
return f"ingest:{row_id}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Pagination page size — bounded read avoids loading >cap rows at once and
|
|
46
|
+
# enables collectors to span sources with >1000 events per cycle (bug fix
|
|
47
|
+
# 2026-05-18: previous code fetched a single LIMIT-less batch and the
|
|
48
|
+
# orchestrator-level cap silently truncated to per_source_event_cap).
|
|
49
|
+
_PAGE_SIZE: int = 500
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def collect(ctx: SourceCollectorContext) -> AsyncIterator[EventDraft]:
|
|
53
|
+
"""Yield ingestor events within the cycle window.
|
|
54
|
+
|
|
55
|
+
Window = (max(watermark, cycle_window_start), min(cutoff_at, cycle_window_end)]
|
|
56
|
+
SQL filter uses string comparison on ISO timestamps; Python-side
|
|
57
|
+
``ctx.in_window`` re-checks each row for TZ-naive substrate rows.
|
|
58
|
+
Pagination via LIMIT/OFFSET so cycles with >page_size events still
|
|
59
|
+
surface every row.
|
|
60
|
+
"""
|
|
61
|
+
lower_iso = ctx.lower_bound_iso
|
|
62
|
+
upper_iso = ctx.upper_bound_iso
|
|
63
|
+
|
|
64
|
+
offset = 0
|
|
65
|
+
while True:
|
|
66
|
+
async with acquire_db() as db:
|
|
67
|
+
cur = await db.execute(
|
|
68
|
+
"SELECT id, source, status, default_program, title, url, "
|
|
69
|
+
" workspace_id, updated_at "
|
|
70
|
+
"FROM inbox_items "
|
|
71
|
+
"WHERE updated_at > ? AND updated_at <= ? "
|
|
72
|
+
"ORDER BY updated_at ASC "
|
|
73
|
+
"LIMIT ? OFFSET ?",
|
|
74
|
+
(lower_iso, upper_iso, _PAGE_SIZE, offset),
|
|
75
|
+
)
|
|
76
|
+
rows = await cur.fetchall()
|
|
77
|
+
if not rows:
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
for row in rows:
|
|
81
|
+
row_id = row[0] if not hasattr(row, "keys") else row["id"]
|
|
82
|
+
source_value = row[1] if not hasattr(row, "keys") else row["source"]
|
|
83
|
+
status = row[2] if not hasattr(row, "keys") else row["status"]
|
|
84
|
+
default_program = (
|
|
85
|
+
row[3] if not hasattr(row, "keys") else row["default_program"]
|
|
86
|
+
)
|
|
87
|
+
title = row[4] if not hasattr(row, "keys") else row["title"]
|
|
88
|
+
url = row[5] if not hasattr(row, "keys") else row["url"]
|
|
89
|
+
updated_at_raw = row[7] if not hasattr(row, "keys") else row["updated_at"]
|
|
90
|
+
|
|
91
|
+
observed_at = normalize_iso(updated_at_raw) or ctx.now
|
|
92
|
+
if not ctx.in_window(observed_at):
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
event_type = _classify_event_type(source_value)
|
|
96
|
+
ref = _source_ref(str(row_id), source_value, url)
|
|
97
|
+
project, program = resolve_source_project(default_program)
|
|
98
|
+
|
|
99
|
+
evidence = {
|
|
100
|
+
"item_id": str(row_id),
|
|
101
|
+
"source_kind": source_value,
|
|
102
|
+
"status": status,
|
|
103
|
+
"project_hint": default_program,
|
|
104
|
+
"updated_at": observed_at.isoformat(),
|
|
105
|
+
}
|
|
106
|
+
if url:
|
|
107
|
+
evidence["url"] = url
|
|
108
|
+
|
|
109
|
+
yield EventDraft(
|
|
110
|
+
event_type=event_type,
|
|
111
|
+
source_system="ingest",
|
|
112
|
+
source_ref=ref,
|
|
113
|
+
title=(title or f"Ingest item {row_id}")[:200],
|
|
114
|
+
summary=f"{source_value or 'inbox'} item status={status}",
|
|
115
|
+
observed_at=observed_at,
|
|
116
|
+
derived_from_state_at=observed_at,
|
|
117
|
+
evidence=evidence,
|
|
118
|
+
source_project=project,
|
|
119
|
+
program_key=program,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if len(rows) < _PAGE_SIZE:
|
|
123
|
+
break
|
|
124
|
+
offset += _PAGE_SIZE
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
ingestor_collector = SourceCollector(source_system="ingest", collect=collect)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
__all__ = ["collect", "ingestor_collector"]
|