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