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
core/cli/_runtime_ctx.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# v1.0.0 - 2026-05-27 - S2 F1: shared runtime helpers for the thin marvis CLI
|
|
2
|
+
"""Shared plumbing for the ``marvis`` runtime subcommands (status/brief/triage/...).
|
|
3
|
+
|
|
4
|
+
The runtime commands are thin terminal adapters over the S1 ``use_cases`` layer:
|
|
5
|
+
no HTTP, no token. Every command follows the same shape — build the local
|
|
6
|
+
single-user :class:`CallerContext`, open a DB context, call the (async) use_case,
|
|
7
|
+
serialize the result. This module is the ONE place that:
|
|
8
|
+
|
|
9
|
+
- opens an asyncio loop (:func:`run_async`),
|
|
10
|
+
- holds the local identity singleton (:data:`LOCAL_CTX`),
|
|
11
|
+
- wires read/write DB access (:func:`with_db` / :func:`with_write_db`),
|
|
12
|
+
- emits output with ``--json`` purity (:func:`emit`),
|
|
13
|
+
- maps :class:`ServiceError` to a clean exit code (:func:`handle_service_error`).
|
|
14
|
+
|
|
15
|
+
Design constraints (S1/S2 learnings, non-negotiable):
|
|
16
|
+
|
|
17
|
+
- **Writes go through ``acquire_write_db`` only.** ``acquire_db()`` opens a
|
|
18
|
+
``query_only=ON`` connection — an INSERT/UPDATE through it raises. The read
|
|
19
|
+
vs write split is honored by :func:`with_db` vs :func:`with_write_db`.
|
|
20
|
+
- **Lazy imports.** Heavy modules (``core.api.use_cases.*``, embedding) are
|
|
21
|
+
imported INSIDE the command bodies, never at module top, so ``marvis --help``
|
|
22
|
+
and ``marvis status`` stay fast and never trigger a model load.
|
|
23
|
+
- **``--json`` purity.** When ``--json`` is set, stdout carries ONLY valid JSON.
|
|
24
|
+
Rich tables and every human message/warning go to stderr, so
|
|
25
|
+
``marvis brief slug --json | jq`` never breaks.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import asyncio
|
|
30
|
+
import json
|
|
31
|
+
import sys
|
|
32
|
+
from contextlib import asynccontextmanager
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any, AsyncIterator, Awaitable, Callable, TypeVar
|
|
35
|
+
|
|
36
|
+
import typer
|
|
37
|
+
from rich.console import Console
|
|
38
|
+
|
|
39
|
+
from core.api.use_cases._context import CallerContext
|
|
40
|
+
|
|
41
|
+
# Human output → stdout (suppressed under --json); warnings/logs → stderr.
|
|
42
|
+
console = Console(stderr=False)
|
|
43
|
+
err_console = Console(stderr=True)
|
|
44
|
+
|
|
45
|
+
# Local single-user identity, reused across every runtime command. No token,
|
|
46
|
+
# no JWT — this is the OSS local operator (human session, four-eyes collapsed).
|
|
47
|
+
LOCAL_CTX: CallerContext = CallerContext.local_single_user()
|
|
48
|
+
|
|
49
|
+
# A few commands (audit trail) need the full local view: the OSS single user is
|
|
50
|
+
# the sole operator, so there is no second permission model to protect against.
|
|
51
|
+
# ``_authorize_audit_read`` narrows plain operators to a learnings-only slice;
|
|
52
|
+
# the local CLI legitimately reads its own complete trail, so it elevates here.
|
|
53
|
+
LOCAL_ADMIN_CTX: CallerContext = CallerContext(
|
|
54
|
+
username="local",
|
|
55
|
+
system_role="super_admin",
|
|
56
|
+
user_type="human",
|
|
57
|
+
is_human_session=True,
|
|
58
|
+
user_id="local",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
T = TypeVar("T")
|
|
62
|
+
|
|
63
|
+
def _apply_settings() -> None:
|
|
64
|
+
"""Point the runtime at the user's configured DB + projects_root (once).
|
|
65
|
+
|
|
66
|
+
Thin delegate to :func:`core.api.runtime_settings.apply_marvis_settings`, the
|
|
67
|
+
single shared implementation the stdio MCP server also calls, so the CLI and
|
|
68
|
+
the MCP surface never diverge on which SQLite file / projects_root they use.
|
|
69
|
+
Best-effort: if no settings file exists the API defaults / env vars stand
|
|
70
|
+
(lets tests and ad-hoc runs work with ``$PIR_DB_PATH``).
|
|
71
|
+
"""
|
|
72
|
+
from core.api.runtime_settings import apply_marvis_settings
|
|
73
|
+
|
|
74
|
+
apply_marvis_settings()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def run_async(coro: Awaitable[T]) -> T:
|
|
78
|
+
"""Run an async use_case from a sync Typer command — the ONLY loop opened.
|
|
79
|
+
|
|
80
|
+
Use-cases stay purely ``async``; nesting a second ``asyncio.run`` inside one
|
|
81
|
+
would explode, so every command funnels through here.
|
|
82
|
+
"""
|
|
83
|
+
return asyncio.run(coro)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@asynccontextmanager
|
|
87
|
+
async def with_db() -> AsyncIterator[Any]:
|
|
88
|
+
"""Read-only DB context (``query_only=ON`` pool/connection). For reads only."""
|
|
89
|
+
_apply_settings()
|
|
90
|
+
from core.api.db import acquire_db
|
|
91
|
+
|
|
92
|
+
async with acquire_db() as db:
|
|
93
|
+
yield db
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@asynccontextmanager
|
|
97
|
+
async def with_write_db() -> AsyncIterator[Any]:
|
|
98
|
+
"""Writer DB context (single-writer + lock). For mutations (e.g. approve).
|
|
99
|
+
|
|
100
|
+
``acquire_write_db`` requires the writer connection to exist; outside the
|
|
101
|
+
FastAPI lifespan we lazily initialize the single-writer pool the first time
|
|
102
|
+
a write is needed.
|
|
103
|
+
"""
|
|
104
|
+
_apply_settings()
|
|
105
|
+
from core.api import db as db_mod
|
|
106
|
+
|
|
107
|
+
if db_mod._writer is None: # lazy one-shot init for the CLI process
|
|
108
|
+
await db_mod.init_pool()
|
|
109
|
+
|
|
110
|
+
async with db_mod.acquire_write_db(label="marvis-cli") as db:
|
|
111
|
+
yield db
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def emit(result: Any, *, json_out: bool, render: Callable[[Any], None] | None = None) -> None:
|
|
115
|
+
"""Serialize a use_case result.
|
|
116
|
+
|
|
117
|
+
- ``json_out=True`` → write ONLY ``json.dumps(result)`` to **stdout** (no Rich
|
|
118
|
+
boxes, no log lines), so the output pipes cleanly into ``jq``.
|
|
119
|
+
- otherwise → call ``render`` (Rich table on ``console`` / stdout). Human
|
|
120
|
+
warnings printed elsewhere go to ``err_console`` / stderr.
|
|
121
|
+
"""
|
|
122
|
+
if json_out:
|
|
123
|
+
sys.stdout.write(json.dumps(_to_jsonable(result), default=str))
|
|
124
|
+
sys.stdout.write("\n")
|
|
125
|
+
return
|
|
126
|
+
if render is not None:
|
|
127
|
+
render(result)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _to_jsonable(obj: Any) -> Any:
|
|
131
|
+
"""Best-effort conversion of Pydantic models / nested DTOs to plain JSON."""
|
|
132
|
+
if hasattr(obj, "model_dump"):
|
|
133
|
+
return obj.model_dump(mode="json")
|
|
134
|
+
if isinstance(obj, dict):
|
|
135
|
+
return {k: _to_jsonable(v) for k, v in obj.items()}
|
|
136
|
+
if isinstance(obj, (list, tuple)):
|
|
137
|
+
return [_to_jsonable(v) for v in obj]
|
|
138
|
+
return obj
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def handle_service_error(func: Callable[..., T]) -> Callable[..., T]:
|
|
142
|
+
"""Decorator: map a ``ServiceError`` raised by a use_case to exit code 1.
|
|
143
|
+
|
|
144
|
+
Prints the human-readable ``message`` to stderr (never stdout, to keep
|
|
145
|
+
``--json`` pipes clean) and exits 1. Typer keeps exit 2 for argument misuse;
|
|
146
|
+
exit 0 is success.
|
|
147
|
+
"""
|
|
148
|
+
import functools
|
|
149
|
+
|
|
150
|
+
@functools.wraps(func)
|
|
151
|
+
def wrapper(*args: Any, **kwargs: Any) -> T:
|
|
152
|
+
from core.api.use_cases._errors import ServiceError
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
return func(*args, **kwargs)
|
|
156
|
+
except ServiceError as exc:
|
|
157
|
+
err_console.print(f"[red]{exc.message}[/red]")
|
|
158
|
+
raise typer.Exit(1) from exc
|
|
159
|
+
|
|
160
|
+
return wrapper
|
core/cli/_transmute.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# v1.0.0 - 2026-05-28 - S0 RI-7 layer 1: deterministic mechanical transmute
|
|
2
|
+
"""Mechanical, deterministic layer of project *transmutation* (RI-7 layer 1).
|
|
3
|
+
|
|
4
|
+
This is the CLI half of the "plan deterministically, execute probabilistically,
|
|
5
|
+
validate strictly" split. It does ONLY the parts that must be reproducible and
|
|
6
|
+
non-negotiable; the intelligent content re-mapping (the agent's job, S-Transmute)
|
|
7
|
+
is OUT of this slice.
|
|
8
|
+
|
|
9
|
+
What it does:
|
|
10
|
+
|
|
11
|
+
1. **Scaffold** the Marvis structure into a *new* directory (separate from the
|
|
12
|
+
source code): ``project.yaml`` + ``docs/`` + ``memory/`` + a seed
|
|
13
|
+
``context.md``. Derived from the source project (name / deduced type).
|
|
14
|
+
2. **Non-destructive guarantee (D3, regola ferrea):** a SHA-256 hash-inventory
|
|
15
|
+
of the source tree is taken BEFORE the scaffold and re-verified AFTER. If a
|
|
16
|
+
single hash changes, that is a hard error. Every write is confined to the new
|
|
17
|
+
dir via a write-guard — the source tree is treated read-only and is NEVER a
|
|
18
|
+
write target.
|
|
19
|
+
3. **Path registry + idempotency:** a ``.marvis-transmute.yaml`` manifest is
|
|
20
|
+
written into the new dir recording ``source_roots[]`` (for the future in-place
|
|
21
|
+
KG index — NOT built here), the reference hash-inventory, and a
|
|
22
|
+
version/timestamp. A second run diffs against the manifest instead of blindly
|
|
23
|
+
clobbering.
|
|
24
|
+
|
|
25
|
+
OUT of this slice (follow-ups, do NOT add here): in-place KG indexing of the
|
|
26
|
+
source code, the agent install-skill, the Hit@5 measurement harness.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import hashlib
|
|
31
|
+
from datetime import datetime, timezone
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
# Schema version of the manifest format — bump on breaking changes.
|
|
36
|
+
MANIFEST_VERSION = 1
|
|
37
|
+
MANIFEST_NAME = ".marvis-transmute.yaml"
|
|
38
|
+
|
|
39
|
+
# Directories never walked when building the source inventory or scaffolding:
|
|
40
|
+
# noise (VCS/build/caches) that would bloat the hash-inventory and the index.
|
|
41
|
+
_SKIP_DIRS = frozenset(
|
|
42
|
+
{
|
|
43
|
+
".git",
|
|
44
|
+
".hg",
|
|
45
|
+
".svn",
|
|
46
|
+
"node_modules",
|
|
47
|
+
"__pycache__",
|
|
48
|
+
".venv",
|
|
49
|
+
"venv",
|
|
50
|
+
"dist",
|
|
51
|
+
"build",
|
|
52
|
+
"target",
|
|
53
|
+
".mypy_cache",
|
|
54
|
+
".pytest_cache",
|
|
55
|
+
".ruff_cache",
|
|
56
|
+
".tox",
|
|
57
|
+
".idea",
|
|
58
|
+
".vscode",
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# The scaffold sub-directories created inside the NEW dir.
|
|
63
|
+
_SCAFFOLD_DIRS = ("docs", "memory")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TransmuteError(RuntimeError):
|
|
67
|
+
"""A non-destructive invariant was violated, or a write escaped the new dir."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Hash-inventory (non-destructive verification)
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _iter_source_files(root: Path):
|
|
76
|
+
"""Yield every regular file under ``root``, skipping noise dirs and symlinks.
|
|
77
|
+
|
|
78
|
+
Symlinks are NOT followed (a symlink escaping the root would let us read — and
|
|
79
|
+
later wrongly attribute changes to — files outside the source tree).
|
|
80
|
+
"""
|
|
81
|
+
stack = [root]
|
|
82
|
+
while stack:
|
|
83
|
+
current = stack.pop()
|
|
84
|
+
try:
|
|
85
|
+
entries = sorted(current.iterdir(), key=lambda p: p.name)
|
|
86
|
+
except (PermissionError, OSError):
|
|
87
|
+
continue
|
|
88
|
+
for entry in entries:
|
|
89
|
+
if entry.is_symlink():
|
|
90
|
+
continue
|
|
91
|
+
if entry.is_dir():
|
|
92
|
+
if entry.name in _SKIP_DIRS:
|
|
93
|
+
continue
|
|
94
|
+
stack.append(entry)
|
|
95
|
+
elif entry.is_file():
|
|
96
|
+
yield entry
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def hash_inventory(root: Path) -> dict[str, str]:
|
|
100
|
+
"""Map ``relative_posix_path -> sha256-hex`` for every file under ``root``.
|
|
101
|
+
|
|
102
|
+
Deterministic and stable: keyed on POSIX-relative paths, sorted by the caller
|
|
103
|
+
when serialized. This is the reference used to prove the source is untouched.
|
|
104
|
+
"""
|
|
105
|
+
root = root.resolve()
|
|
106
|
+
inventory: dict[str, str] = {}
|
|
107
|
+
for f in _iter_source_files(root):
|
|
108
|
+
h = hashlib.sha256()
|
|
109
|
+
with f.open("rb") as fh:
|
|
110
|
+
for chunk in iter(lambda: fh.read(1 << 16), b""):
|
|
111
|
+
h.update(chunk)
|
|
112
|
+
rel = f.resolve().relative_to(root).as_posix()
|
|
113
|
+
inventory[rel] = h.hexdigest()
|
|
114
|
+
return inventory
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def diff_inventory(
|
|
118
|
+
before: dict[str, str], after: dict[str, str]
|
|
119
|
+
) -> dict[str, list[str]]:
|
|
120
|
+
"""Return ``{added, removed, modified}`` between two inventories."""
|
|
121
|
+
before_keys = set(before)
|
|
122
|
+
after_keys = set(after)
|
|
123
|
+
added = sorted(after_keys - before_keys)
|
|
124
|
+
removed = sorted(before_keys - after_keys)
|
|
125
|
+
modified = sorted(k for k in before_keys & after_keys if before[k] != after[k])
|
|
126
|
+
return {"added": added, "removed": removed, "modified": modified}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# Write-guard
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class WriteGuard:
|
|
135
|
+
"""Confine every filesystem write to a single allowed directory (the new dir).
|
|
136
|
+
|
|
137
|
+
The source tree (``forbidden_root``) is read-only by construction: any attempt
|
|
138
|
+
to write at/under it — or anywhere outside ``allowed_root`` — raises. This is
|
|
139
|
+
the allowlist that neutralizes "agent deletes the source" classes of bugs.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(self, allowed_root: Path, forbidden_root: Path) -> None:
|
|
143
|
+
self.allowed_root = allowed_root.resolve()
|
|
144
|
+
self.forbidden_root = forbidden_root.resolve()
|
|
145
|
+
|
|
146
|
+
def _is_within(self, child: Path, parent: Path) -> bool:
|
|
147
|
+
try:
|
|
148
|
+
child.resolve().relative_to(parent)
|
|
149
|
+
return True
|
|
150
|
+
except ValueError:
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
def check(self, target: Path) -> Path:
|
|
154
|
+
"""Validate ``target`` is inside the allowed dir; raise otherwise."""
|
|
155
|
+
resolved = target if target.is_absolute() else (self.allowed_root / target)
|
|
156
|
+
# Resolve the nearest existing ancestor so a not-yet-created file still
|
|
157
|
+
# resolves to a real location (symlink-escape safe).
|
|
158
|
+
probe = resolved
|
|
159
|
+
while not probe.exists() and probe != probe.parent:
|
|
160
|
+
probe = probe.parent
|
|
161
|
+
anchor = probe.resolve() / resolved.name if probe != resolved else resolved.resolve()
|
|
162
|
+
if self._is_within(anchor, self.forbidden_root):
|
|
163
|
+
raise TransmuteError(
|
|
164
|
+
f"write-guard: refusing to write inside the source tree: {target}"
|
|
165
|
+
)
|
|
166
|
+
if not self._is_within(anchor, self.allowed_root):
|
|
167
|
+
raise TransmuteError(
|
|
168
|
+
f"write-guard: write outside the new dir is forbidden: {target}"
|
|
169
|
+
)
|
|
170
|
+
return resolved
|
|
171
|
+
|
|
172
|
+
def mkdir(self, target: Path) -> None:
|
|
173
|
+
self.check(target).mkdir(parents=True, exist_ok=True)
|
|
174
|
+
|
|
175
|
+
def write_text(self, target: Path, text: str) -> None:
|
|
176
|
+
path = self.check(target)
|
|
177
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
path.write_text(text, encoding="utf-8")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
# Manifest (path registry + idempotency)
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def build_manifest(
|
|
187
|
+
*,
|
|
188
|
+
slug: str,
|
|
189
|
+
source_root: Path,
|
|
190
|
+
inventory: dict[str, str],
|
|
191
|
+
project_type: str,
|
|
192
|
+
) -> dict[str, Any]:
|
|
193
|
+
"""Construct the ``.marvis-transmute.yaml`` payload.
|
|
194
|
+
|
|
195
|
+
``source_roots`` is a LIST (multi-language repos = N roots) so a future
|
|
196
|
+
in-place KG indexer can iterate it. We register one root here (the imported
|
|
197
|
+
dir); the ``{path, language, exclude}`` shape is forward-compatible.
|
|
198
|
+
"""
|
|
199
|
+
return {
|
|
200
|
+
"manifest_version": MANIFEST_VERSION,
|
|
201
|
+
"slug": slug,
|
|
202
|
+
"transmuted_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
203
|
+
"project_type": project_type,
|
|
204
|
+
# Path registry: maps the Marvis metadata dir <-> source code location(s).
|
|
205
|
+
# The KG index of the code in-place is a FOLLOW-UP slice (not built here).
|
|
206
|
+
"source_roots": [
|
|
207
|
+
{
|
|
208
|
+
"path": str(source_root.resolve()),
|
|
209
|
+
"language": None,
|
|
210
|
+
"exclude": sorted(_SKIP_DIRS),
|
|
211
|
+
}
|
|
212
|
+
],
|
|
213
|
+
# Reference hash-inventory: the proof-of-untouched baseline (D3).
|
|
214
|
+
"source_inventory": {
|
|
215
|
+
"algorithm": "sha256",
|
|
216
|
+
"file_count": len(inventory),
|
|
217
|
+
"files": dict(sorted(inventory.items())),
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def load_manifest(new_dir: Path) -> dict[str, Any] | None:
|
|
223
|
+
"""Read an existing manifest from ``new_dir`` (or ``None`` if absent)."""
|
|
224
|
+
import yaml
|
|
225
|
+
|
|
226
|
+
path = new_dir / MANIFEST_NAME
|
|
227
|
+
if not path.is_file():
|
|
228
|
+
return None
|
|
229
|
+
try:
|
|
230
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
231
|
+
return data if isinstance(data, dict) else None
|
|
232
|
+
except Exception: # noqa: BLE001
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def manifest_drift(
|
|
237
|
+
manifest: dict[str, Any], current_inventory: dict[str, str]
|
|
238
|
+
) -> dict[str, list[str]]:
|
|
239
|
+
"""Diff the source against the inventory recorded in an existing manifest."""
|
|
240
|
+
recorded = (manifest.get("source_inventory") or {}).get("files") or {}
|
|
241
|
+
return diff_inventory(recorded, current_inventory)
|