@hybridlabor-api/aos 4.4.1 → 4.4.2-beta.1
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.
- package/.agents/skills/firecrawl/SKILL.md +149 -0
- package/.agents/skills/firecrawl/rules/install.md +82 -0
- package/.agents/skills/firecrawl/rules/security.md +26 -0
- package/.agents/skills/firecrawl-agent/SKILL.md +58 -0
- package/.agents/skills/firecrawl-build/SKILL.md +39 -0
- package/.agents/skills/firecrawl-build-interact/SKILL.md +68 -0
- package/.agents/skills/firecrawl-build-onboarding/SKILL.md +103 -0
- package/.agents/skills/firecrawl-build-onboarding/references/auth-flow.md +39 -0
- package/.agents/skills/firecrawl-build-onboarding/references/project-setup.md +20 -0
- package/.agents/skills/firecrawl-build-onboarding/references/sdk-installation.md +17 -0
- package/.agents/skills/firecrawl-build-scrape/SKILL.md +69 -0
- package/.agents/skills/firecrawl-build-search/SKILL.md +69 -0
- package/.agents/skills/firecrawl-crawl/SKILL.md +59 -0
- package/.agents/skills/firecrawl-download/SKILL.md +70 -0
- package/.agents/skills/firecrawl-interact/SKILL.md +84 -0
- package/.agents/skills/firecrawl-map/SKILL.md +51 -0
- package/.agents/skills/firecrawl-scrape/SKILL.md +69 -0
- package/.agents/skills/firecrawl-search/SKILL.md +60 -0
- package/CLAUDE.md +573 -0
- package/README.de.md +0 -8
- package/README.md +0 -8
- package/README.pt.md +0 -8
- package/assets/brand/ao-ant-mark.svg +91 -0
- package/assets/brand/bdb-core-mark.svg +34 -0
- package/assets/brand/heimdall-guardian-mark.svg +16 -0
- package/assets/brand/memb-crystal-mark.svg +18 -0
- package/bin/aos-config.mjs +89 -0
- package/bin/aos-dashboard.mjs +608 -0
- package/bin/aos-uninstall.mjs +227 -0
- package/docs/sessions/audit-2026-09-14-silent-failures.md +114 -0
- package/docs/skills_table.md +0 -3
- package/installer.js +471 -237
- package/mcps/RhinoMCP/cc-plugin/.claude/settings.json +10 -0
- package/mcps/after-effects-mcp/build/index.js +840 -0
- package/mcps/after-effects-mcp/build/scripts/applyEffect.jsx +153 -0
- package/mcps/after-effects-mcp/build/scripts/applyEffectTemplate.jsx +218 -0
- package/mcps/after-effects-mcp/build/scripts/createComposition.jsx +71 -0
- package/mcps/after-effects-mcp/build/scripts/createShapeLayer.jsx +147 -0
- package/mcps/after-effects-mcp/build/scripts/createSolidLayer.jsx +114 -0
- package/mcps/after-effects-mcp/build/scripts/createTextLayer.jsx +115 -0
- package/mcps/after-effects-mcp/build/scripts/getLayerInfo.jsx +192 -0
- package/mcps/after-effects-mcp/build/scripts/getProjectInfo.jsx +90 -0
- package/mcps/after-effects-mcp/build/scripts/listCompositions.jsx +50 -0
- package/mcps/after-effects-mcp/build/scripts/mcp-bridge-auto.jsx +1773 -0
- package/mcps/after-effects-mcp/build/scripts/setLayerProperties.jsx +160 -0
- package/mcps/computer-use-mcp/dist/client.d.ts +150 -0
- package/mcps/computer-use-mcp/dist/client.js +136 -0
- package/mcps/computer-use-mcp/dist/entrypoint.d.ts +16 -0
- package/mcps/computer-use-mcp/dist/entrypoint.js +26 -0
- package/mcps/computer-use-mcp/dist/native.d.ts +212 -0
- package/mcps/computer-use-mcp/dist/native.js +50 -0
- package/mcps/computer-use-mcp/dist/server.d.ts +32 -0
- package/mcps/computer-use-mcp/dist/server.js +342 -0
- package/mcps/computer-use-mcp/dist/session.d.ts +101 -0
- package/mcps/computer-use-mcp/dist/session.js +2372 -0
- package/package.json +13 -4
- package/skills/global_config/aos-project-init/SKILL.md +97 -130
- package/skills/global_config/aos-project-init/assets/openwikiignore.template +62 -0
- package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +38 -1
- package/skills/global_config/aos-setup/SKILL.md +98 -8
- package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +21 -2
- package/skills/global_config/ask-tim/SKILL.md +2 -7
- package/skills/global_config/openwiki-skill/scripts/openwiki_daemon.py +65 -12
- package/bin/setup-saas.mjs +0 -792
- package/mcps/blender-mcp-server/.dockerignore +0 -19
- package/mcps/blender-mcp-server/.github/workflows/ci.yml +0 -67
- package/mcps/blender-mcp-server/.github/workflows/publish-pypi.yml +0 -54
- package/mcps/blender-mcp-server/CHANGELOG.md +0 -64
- package/mcps/blender-mcp-server/CONTRIBUTING.md +0 -107
- package/mcps/blender-mcp-server/Dockerfile +0 -19
- package/mcps/blender-mcp-server/LICENSE +0 -21
- package/mcps/blender-mcp-server/README.md +0 -409
- package/mcps/blender-mcp-server/addon/__init__.py +0 -1440
- package/mcps/blender-mcp-server/addon/models.py +0 -223
- package/mcps/blender-mcp-server/pyproject.toml +0 -120
- package/mcps/blender-mcp-server/scripts/blender_bridge_request.py +0 -71
- package/mcps/blender-mcp-server/scripts/blender_create_test_cube.py +0 -49
- package/mcps/blender-mcp-server/scripts/blender_scene_info.py +0 -38
- package/mcps/blender-mcp-server/scripts/build_addon_zip.sh +0 -29
- package/mcps/blender-mcp-server/scripts/demos/README.md +0 -186
- package/mcps/blender-mcp-server/scripts/demos/dam_break_scene.py +0 -276
- package/mcps/blender-mcp-server/scripts/demos/open_dam_break_gui.py +0 -33
- package/mcps/blender-mcp-server/scripts/demos/pipe_movement_study.py +0 -518
- package/mcps/blender-mcp-server/scripts/demos/pipe_pressure_failure_scene.py +0 -806
- package/mcps/blender-mcp-server/scripts/demos/procedural_dam_break_scene.py +0 -600
- package/mcps/blender-mcp-server/scripts/demos/run_dam_break.py +0 -510
- package/mcps/blender-mcp-server/scripts/launch_blender_gui.py +0 -41
- package/mcps/blender-mcp-server/scripts/library/README.md +0 -64
- package/mcps/blender-mcp-server/scripts/library/apply_transforms.py +0 -46
- package/mcps/blender-mcp-server/scripts/library/camera.py +0 -51
- package/mcps/blender-mcp-server/scripts/library/collections.py +0 -65
- package/mcps/blender-mcp-server/scripts/library/create_mesh.py +0 -41
- package/mcps/blender-mcp-server/scripts/library/effector.py +0 -48
- package/mcps/blender-mcp-server/scripts/library/fluid_domain.py +0 -80
- package/mcps/blender-mcp-server/scripts/library/fluid_inflow.py +0 -98
- package/mcps/blender-mcp-server/scripts/library/frame_range.py +0 -35
- package/mcps/blender-mcp-server/scripts/library/keyframes.py +0 -59
- package/mcps/blender-mcp-server/scripts/library/rigid_body.py +0 -52
- package/mcps/blender-mcp-server/scripts/library/save_blend.py +0 -40
- package/mcps/blender-mcp-server/scripts/server_start.sh +0 -26
- package/mcps/blender-mcp-server/src/blender_mcp_server/__init__.py +0 -3
- package/mcps/blender-mcp-server/src/blender_mcp_server/headless.py +0 -330
- package/mcps/blender-mcp-server/src/blender_mcp_server/server.py +0 -606
- package/mcps/davinci-mcp-professional/.bandit +0 -35
- package/mcps/davinci-mcp-professional/.claude/commands/security-review.md +0 -191
- package/mcps/davinci-mcp-professional/.claude/settings.json +0 -1
- package/mcps/davinci-mcp-professional/.editorconfig +0 -27
- package/mcps/davinci-mcp-professional/.gitattributes +0 -207
- package/mcps/davinci-mcp-professional/.github/CODEOWNERS +0 -2
- package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
- package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- package/mcps/davinci-mcp-professional/.github/PULL_REQUEST_TEMPLATE.md +0 -20
- package/mcps/davinci-mcp-professional/.github/SECURITY.md +0 -97
- package/mcps/davinci-mcp-professional/.github/dependabot.yml +0 -27
- package/mcps/davinci-mcp-professional/.github/workflows/ci.yml +0 -39
- package/mcps/davinci-mcp-professional/ATTRIBUTION.md +0 -4
- package/mcps/davinci-mcp-professional/AUTHORS.md +0 -9
- package/mcps/davinci-mcp-professional/BUGS.md +0 -55
- package/mcps/davinci-mcp-professional/CLAUDE.md +0 -229
- package/mcps/davinci-mcp-professional/CONTRIBUTING.md +0 -51
- package/mcps/davinci-mcp-professional/COPYING.md +0 -228
- package/mcps/davinci-mcp-professional/Doxyfile +0 -379
- package/mcps/davinci-mcp-professional/README.md +0 -143
- package/mcps/davinci-mcp-professional/SECURITY.md +0 -40
- package/mcps/davinci-mcp-professional/USING.md +0 -301
- package/mcps/davinci-mcp-professional/VERSION.md +0 -13
- package/mcps/davinci-mcp-professional/build.py +0 -169
- package/mcps/davinci-mcp-professional/claude_desktop_config_template.json +0 -11
- package/mcps/davinci-mcp-professional/hooks/ci-check.sh +0 -29
- package/mcps/davinci-mcp-professional/hooks/pre-push +0 -46
- package/mcps/davinci-mcp-professional/main.py +0 -40
- package/mcps/davinci-mcp-professional/mcp_server.py +0 -30
- package/mcps/davinci-mcp-professional/pyproject.toml +0 -115
- package/mcps/davinci-mcp-professional/security_audit.py +0 -379
- package/mcps/davinci-mcp-professional/src/davinci_mcp/__init__.py +0 -19
- package/mcps/davinci-mcp-professional/src/davinci_mcp/cli.py +0 -138
- package/mcps/davinci-mcp-professional/src/davinci_mcp/resolve_client.py +0 -342
- package/mcps/davinci-mcp-professional/src/davinci_mcp/resources/__init__.py +0 -60
- package/mcps/davinci-mcp-professional/src/davinci_mcp/server.py +0 -185
- package/mcps/davinci-mcp-professional/src/davinci_mcp/tools/__init__.py +0 -146
- package/mcps/davinci-mcp-professional/src/davinci_mcp/types.py +0 -125
- package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/__init__.py +0 -19
- package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/platform.py +0 -239
- package/mcps/davinci-mcp-professional/test.py +0 -186
- package/mcps/davinci-mcp-professional/uv.lock +0 -2073
- package/mcps/davinci-resolve-mcp/.claude/skills/media-analysis.md +0 -44
- package/mcps/davinci-resolve-mcp/.clinerules +0 -17
- package/mcps/davinci-resolve-mcp/.cursorrules +0 -17
- package/mcps/davinci-resolve-mcp/.github/copilot-instructions.md +0 -17
- package/mcps/davinci-resolve-mcp/.github/workflows/npm-publish.yml +0 -109
- package/mcps/davinci-resolve-mcp/.windsurfrules +0 -17
- package/mcps/davinci-resolve-mcp/AGENTS.md +0 -114
- package/mcps/davinci-resolve-mcp/CHANGELOG.md +0 -2648
- package/mcps/davinci-resolve-mcp/CLAUDE.md +0 -15
- package/mcps/davinci-resolve-mcp/LICENSE +0 -21
- package/mcps/davinci-resolve-mcp/README.md +0 -173
- package/mcps/davinci-resolve-mcp/SECURITY.md +0 -53
- package/mcps/davinci-resolve-mcp/bin/davinci-resolve-mcp.mjs +0 -450
- package/mcps/davinci-resolve-mcp/examples/README.md +0 -53
- package/mcps/davinci-resolve-mcp/examples/markers/README.md +0 -81
- package/mcps/davinci-resolve-mcp/examples/media/README.md +0 -94
- package/mcps/davinci-resolve-mcp/examples/timeline/README.md +0 -98
- package/mcps/davinci-resolve-mcp/install.py +0 -1687
- package/mcps/davinci-resolve-mcp/package.json +0 -52
- package/mcps/davinci-resolve-mcp/release-notes/v2.22.0.md +0 -51
- package/mcps/davinci-resolve-mcp/release-notes/v2.23.0.md +0 -51
- package/mcps/davinci-resolve-mcp/release-notes/v2.23.1.md +0 -35
- package/mcps/davinci-resolve-mcp/release-notes/v2.24.1.md +0 -32
- package/mcps/davinci-resolve-mcp/scripts/audit_api_parity.py +0 -275
- package/mcps/davinci-resolve-mcp/scripts/audit_readwrite_symmetry.py +0 -80
- package/mcps/davinci-resolve-mcp/scripts/doctor.py +0 -227
- package/mcps/davinci-resolve-mcp/scripts/gen_api_limitations.py +0 -144
- package/mcps/davinci-resolve-mcp/scripts/live_media_analysis_polish_probe.py +0 -65
- package/mcps/davinci-resolve-mcp/scripts/measure_bridge_cost.py +0 -65
- package/mcps/davinci-resolve-mcp/scripts/regen_panel_screenshots.py +0 -76
- package/mcps/davinci-resolve-mcp/src/__init__.py +0 -3
- package/mcps/davinci-resolve-mcp/src/analysis_dashboard.py +0 -15505
- package/mcps/davinci-resolve-mcp/src/batch_cli.py +0 -540
- package/mcps/davinci-resolve-mcp/src/control_panel.py +0 -13
- package/mcps/davinci-resolve-mcp/src/granular/__init__.py +0 -17
- package/mcps/davinci-resolve-mcp/src/granular/common.py +0 -741
- package/mcps/davinci-resolve-mcp/src/granular/folder.py +0 -420
- package/mcps/davinci-resolve-mcp/src/granular/gallery.py +0 -306
- package/mcps/davinci-resolve-mcp/src/granular/graph.py +0 -309
- package/mcps/davinci-resolve-mcp/src/granular/media_pool.py +0 -679
- package/mcps/davinci-resolve-mcp/src/granular/media_pool_item.py +0 -1020
- package/mcps/davinci-resolve-mcp/src/granular/media_storage.py +0 -179
- package/mcps/davinci-resolve-mcp/src/granular/project.py +0 -1654
- package/mcps/davinci-resolve-mcp/src/granular/resolve_control.py +0 -538
- package/mcps/davinci-resolve-mcp/src/granular/timeline.py +0 -1075
- package/mcps/davinci-resolve-mcp/src/granular/timeline_item.py +0 -2251
- package/mcps/davinci-resolve-mcp/src/resolve_mcp_server.py +0 -43
- package/mcps/davinci-resolve-mcp/src/server.py +0 -24116
- package/mcps/davinci-resolve-mcp/src/utils/__init__.py +0 -3
- package/mcps/davinci-resolve-mcp/src/utils/actor_identity.py +0 -46
- package/mcps/davinci-resolve-mcp/src/utils/analysis_caps.py +0 -678
- package/mcps/davinci-resolve-mcp/src/utils/analysis_memory.py +0 -755
- package/mcps/davinci-resolve-mcp/src/utils/analysis_runs.py +0 -307
- package/mcps/davinci-resolve-mcp/src/utils/analysis_store.py +0 -945
- package/mcps/davinci-resolve-mcp/src/utils/api_truth.py +0 -525
- package/mcps/davinci-resolve-mcp/src/utils/app_control.py +0 -319
- package/mcps/davinci-resolve-mcp/src/utils/audio_fairlight_live_probe.py +0 -263
- package/mcps/davinci-resolve-mcp/src/utils/brain_edits.py +0 -387
- package/mcps/davinci-resolve-mcp/src/utils/bridge_metrics.py +0 -76
- package/mcps/davinci-resolve-mcp/src/utils/cdl.py +0 -20
- package/mcps/davinci-resolve-mcp/src/utils/clip_query.py +0 -85
- package/mcps/davinci-resolve-mcp/src/utils/cloud_operations.py +0 -192
- package/mcps/davinci-resolve-mcp/src/utils/color_grade_live_probe.py +0 -444
- package/mcps/davinci-resolve-mcp/src/utils/contracts.py +0 -102
- package/mcps/davinci-resolve-mcp/src/utils/cut_ir.py +0 -115
- package/mcps/davinci-resolve-mcp/src/utils/dctl_templates.py +0 -368
- package/mcps/davinci-resolve-mcp/src/utils/deep_vision.py +0 -643
- package/mcps/davinci-resolve-mcp/src/utils/destructive_hook.py +0 -606
- package/mcps/davinci-resolve-mcp/src/utils/edit_engine.py +0 -737
- package/mcps/davinci-resolve-mcp/src/utils/embeddings.py +0 -922
- package/mcps/davinci-resolve-mcp/src/utils/entities.py +0 -579
- package/mcps/davinci-resolve-mcp/src/utils/extension_authoring_live_probe.py +0 -292
- package/mcps/davinci-resolve-mcp/src/utils/failure_tracker.py +0 -119
- package/mcps/davinci-resolve-mcp/src/utils/fuse_templates.py +0 -1968
- package/mcps/davinci-resolve-mcp/src/utils/fusion_composition_live_probe.py +0 -284
- package/mcps/davinci-resolve-mcp/src/utils/fusion_group_settings.py +0 -334
- package/mcps/davinci-resolve-mcp/src/utils/layout_presets.py +0 -333
- package/mcps/davinci-resolve-mcp/src/utils/mcp_stdio.py +0 -32
- package/mcps/davinci-resolve-mcp/src/utils/mcp_transport.py +0 -128
- package/mcps/davinci-resolve-mcp/src/utils/media_analysis.py +0 -7690
- package/mcps/davinci-resolve-mcp/src/utils/media_analysis_jobs.py +0 -805
- package/mcps/davinci-resolve-mcp/src/utils/media_pool_changes.py +0 -121
- package/mcps/davinci-resolve-mcp/src/utils/media_pool_ingest_live_probe.py +0 -592
- package/mcps/davinci-resolve-mcp/src/utils/multicam.py +0 -393
- package/mcps/davinci-resolve-mcp/src/utils/object_inspection.py +0 -301
- package/mcps/davinci-resolve-mcp/src/utils/page_lock.py +0 -75
- package/mcps/davinci-resolve-mcp/src/utils/platform.py +0 -157
- package/mcps/davinci-resolve-mcp/src/utils/proc.py +0 -26
- package/mcps/davinci-resolve-mcp/src/utils/project_cleanup.py +0 -73
- package/mcps/davinci-resolve-mcp/src/utils/project_lifecycle_live_probe.py +0 -376
- package/mcps/davinci-resolve-mcp/src/utils/project_lint.py +0 -131
- package/mcps/davinci-resolve-mcp/src/utils/project_properties.py +0 -601
- package/mcps/davinci-resolve-mcp/src/utils/project_spec.py +0 -459
- package/mcps/davinci-resolve-mcp/src/utils/readback.py +0 -91
- package/mcps/davinci-resolve-mcp/src/utils/render_deliver_live_probe.py +0 -384
- package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_governance.py +0 -214
- package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_ledger.py +0 -244
- package/mcps/davinci-resolve-mcp/src/utils/resolve_busy.py +0 -171
- package/mcps/davinci-resolve-mcp/src/utils/resolve_connection.py +0 -77
- package/mcps/davinci-resolve-mcp/src/utils/review_annotation_live_probe.py +0 -352
- package/mcps/davinci-resolve-mcp/src/utils/script_templates.py +0 -1193
- package/mcps/davinci-resolve-mcp/src/utils/shot_relationships.py +0 -544
- package/mcps/davinci-resolve-mcp/src/utils/structural_diff.py +0 -175
- package/mcps/davinci-resolve-mcp/src/utils/sync_detection.py +0 -889
- package/mcps/davinci-resolve-mcp/src/utils/timeline_brain_db.py +0 -826
- package/mcps/davinci-resolve-mcp/src/utils/timeline_conform_live_probe.py +0 -280
- package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_live_probe.py +0 -1092
- package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_probe.py +0 -185
- package/mcps/davinci-resolve-mcp/src/utils/timeline_title_text.py +0 -87
- package/mcps/davinci-resolve-mcp/src/utils/timeline_versioning.py +0 -730
- package/mcps/davinci-resolve-mcp/src/utils/update_check.py +0 -685
- package/mcps/vectorworks-mcp/.dockerignore +0 -8
- package/mcps/vectorworks-mcp/Dockerfile +0 -28
- package/mcps/vectorworks-mcp/README.md +0 -156
- package/mcps/vectorworks-mcp/app/__init__.py +0 -2
- package/mcps/vectorworks-mcp/app/api.py +0 -52
- package/mcps/vectorworks-mcp/app/chunking.py +0 -56
- package/mcps/vectorworks-mcp/app/config.py +0 -29
- package/mcps/vectorworks-mcp/app/embeddings.py +0 -28
- package/mcps/vectorworks-mcp/app/indexer.py +0 -106
- package/mcps/vectorworks-mcp/app/main.py +0 -33
- package/mcps/vectorworks-mcp/app/mcp_server.py +0 -71
- package/mcps/vectorworks-mcp/app/search.py +0 -54
- package/mcps/vectorworks-mcp/app/utils.py +0 -14
- package/mcps/vectorworks-mcp/data/.gitkeep +0 -1
- package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_PluginScript.htm +0 -95
- package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_ScriptEditor.htm +0 -266
- package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_Scripting.htm +0 -106
- package/mcps/vectorworks-mcp/data/vw-app-help/VW2023_CreatingScriptedPlugins.htm +0 -327
- package/mcps/vectorworks-mcp/data/vw-app-help/VW2024_RunningScripts.htm +0 -116
- package/mcps/vectorworks-mcp/data/vw-devwiki/VS_Function_Reference_category.html +0 -2129
- package/mcps/vectorworks-mcp/data/vw-jp-ref/ObjectsSymbols.html +0 -2035
- package/mcps/vectorworks-mcp/data/vw-jp-ref/ScriptFunctionReference.html +0 -29
- package/mcps/vectorworks-mcp/design.md +0 -57
- package/mcps/vectorworks-mcp/docker-compose.yml +0 -18
- package/mcps/vectorworks-mcp/index/.gitkeep +0 -1
- package/mcps/vectorworks-mcp/index/meta.jsonl +0 -3956
- package/mcps/vectorworks-mcp/index/vw.faiss +0 -0
- package/mcps/vectorworks-mcp/requirements.txt +0 -10
- package/mcps/vectorworks-mcp/scripts/fetch_docs_minimal.sh +0 -84
- package/mcps/vectorworks-mcp/scripts/fetch_github_vectorworks.sh +0 -85
- package/mcps/vectorworks-mcp/templates/index.html +0 -40
- package/scripts/ecosystem-health-audit.js +0 -257
- package/skills/bdb-dev-os-skill/SKILL.md +0 -49
- package/skills/bdb-dev-os-skill/package.json +0 -11
- package/skills/bdbsaastraining/SKILL.md +0 -316
- package/skills/bdbsaastraining/references/exam-pool.md +0 -313
- package/skills/bdbsaastraining/references/interaction-modes.md +0 -94
- package/skills/bdbsaastraining/references/track-a-ai-agent.md +0 -111
- package/skills/bdbsaastraining/references/track-b-wordpress.md +0 -111
- package/skills/bdbsaastraining/references/track-c-mailserver.md +0 -107
- package/skills/bdbsaastraining/references/track-d-debian.md +0 -109
- package/skills/bdbsaastraining/references/track-e-custom.md +0 -83
- package/skills/bdbsaastraining/scripts/build_profile.py +0 -529
- package/skills/bdbsaastraining/scripts/generate_certificate.py +0 -161
- package/skills/bdbsaastraining/scripts/preflight_check.sh +0 -150
- package/skills/bdbsaastraining/templates/certificate_template.html +0 -508
- package/skills/global_config/bdb-ecosystem-health/SKILL.md +0 -68
- package/skills/global_config/bdbsaas-ops/SKILL.md +0 -16
- package/skills/global_config/bdbsaashost/SKILL.md +0 -125
|
@@ -1,826 +0,0 @@
|
|
|
1
|
-
"""Per-project SQLite store for timeline versioning + brain edits (C6).
|
|
2
|
-
|
|
3
|
-
Lives at `<project_root>/_soul/timeline_brain.sqlite`. Tracks:
|
|
4
|
-
|
|
5
|
-
- `timeline_versions` — every archived timeline duplicate (one row per archive)
|
|
6
|
-
- `brain_edits` — every destructive edit, with target metric + before/after
|
|
7
|
-
- `timeline_clip_usage` — which media pool items are placed where on which timeline
|
|
8
|
-
|
|
9
|
-
This DB is the **source of truth** for the timeline-mutation history of a project.
|
|
10
|
-
It coexists with the existing `jobs.sqlite` (operational job tracking) and
|
|
11
|
-
`index.sqlite` (rebuildable cache over JSON reports). When the V2 source-of-truth
|
|
12
|
-
migration (C1) lands, these tables move into the unified DB; until then, this is
|
|
13
|
-
a dedicated standalone store.
|
|
14
|
-
|
|
15
|
-
Concurrency: single-process access expected (Resolve is single-user). WAL mode
|
|
16
|
-
enabled to make reads from the control panel non-blocking against writes from
|
|
17
|
-
the MCP server.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
from __future__ import annotations
|
|
21
|
-
|
|
22
|
-
import logging
|
|
23
|
-
import os
|
|
24
|
-
import sqlite3
|
|
25
|
-
import threading
|
|
26
|
-
import time
|
|
27
|
-
from contextlib import contextmanager
|
|
28
|
-
from typing import Callable, Dict, Iterator, Optional, Tuple
|
|
29
|
-
|
|
30
|
-
logger = logging.getLogger("resolve-mcp.timeline-brain-db")
|
|
31
|
-
|
|
32
|
-
SCHEMA_VERSION = 12
|
|
33
|
-
DB_FILENAME = "timeline_brain.sqlite"
|
|
34
|
-
SOUL_DIRNAME = "_soul"
|
|
35
|
-
|
|
36
|
-
# How long SQLite waits internally for a busy lock before raising
|
|
37
|
-
# SQLITE_BUSY (in ms). Set high enough to absorb the dashboard's read traffic.
|
|
38
|
-
BUSY_TIMEOUT_MS = 5000
|
|
39
|
-
|
|
40
|
-
# Application-level retry settings on top of busy_timeout, used by transaction().
|
|
41
|
-
# These guard against rare cases where the timeout is exhausted under load.
|
|
42
|
-
_BUSY_RETRY_BACKOFF_SECONDS = (0.1, 0.3, 0.5)
|
|
43
|
-
|
|
44
|
-
_CONNECTION_LOCK = threading.Lock()
|
|
45
|
-
_CONNECTIONS: Dict[str, sqlite3.Connection] = {}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def db_path_for_project(project_root: str) -> str:
|
|
49
|
-
"""Return the on-disk path to the timeline-brain DB for a project."""
|
|
50
|
-
return os.path.join(project_root, SOUL_DIRNAME, DB_FILENAME)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _ensure_parent_dir(path: str) -> None:
|
|
54
|
-
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def _init_pragmas(conn: sqlite3.Connection) -> None:
|
|
58
|
-
conn.execute("PRAGMA journal_mode=WAL")
|
|
59
|
-
conn.execute("PRAGMA foreign_keys=ON")
|
|
60
|
-
conn.execute("PRAGMA synchronous=NORMAL")
|
|
61
|
-
conn.execute(f"PRAGMA busy_timeout={BUSY_TIMEOUT_MS}")
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# ── Migration registry ───────────────────────────────────────────────────────
|
|
65
|
-
#
|
|
66
|
-
# Each entry is keyed by the *target* version. The function receives an open
|
|
67
|
-
# connection and runs any schema changes required to advance from
|
|
68
|
-
# (target_version - 1) to target_version. Connect() walks this dict in order
|
|
69
|
-
# until the DB reaches SCHEMA_VERSION.
|
|
70
|
-
#
|
|
71
|
-
# Conventions:
|
|
72
|
-
# - Migrations are additive when possible (ADD COLUMN, CREATE TABLE IF NOT
|
|
73
|
-
# EXISTS, CREATE INDEX IF NOT EXISTS). Destructive changes need an explicit
|
|
74
|
-
# data-preserving copy-and-rename sequence.
|
|
75
|
-
# - Migration functions must be idempotent — if a column already exists (e.g.
|
|
76
|
-
# from a partial prior run), the migration should detect that and continue.
|
|
77
|
-
# - Bump SCHEMA_VERSION at the top of this module + register the migration
|
|
78
|
-
# here in the same edit. The runner will detect the bump and apply it.
|
|
79
|
-
|
|
80
|
-
_MIGRATIONS: Dict[int, Callable[[sqlite3.Connection], None]] = {}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def register_migration(target_version: int) -> Callable[[Callable[[sqlite3.Connection], None]], Callable[[sqlite3.Connection], None]]:
|
|
84
|
-
"""Decorator: registers a migration that advances the schema to `target_version`."""
|
|
85
|
-
def decorator(fn: Callable[[sqlite3.Connection], None]) -> Callable[[sqlite3.Connection], None]:
|
|
86
|
-
if target_version in _MIGRATIONS:
|
|
87
|
-
raise RuntimeError(f"Duplicate migration for v{target_version}")
|
|
88
|
-
_MIGRATIONS[target_version] = fn
|
|
89
|
-
return fn
|
|
90
|
-
return decorator
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _column_exists(conn: sqlite3.Connection, table: str, column: str) -> bool:
|
|
94
|
-
row = conn.execute(f"PRAGMA table_info({table})").fetchall()
|
|
95
|
-
return any(r[1] == column for r in row)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def _read_schema_version(conn: sqlite3.Connection) -> int:
|
|
99
|
-
try:
|
|
100
|
-
row = conn.execute(
|
|
101
|
-
"SELECT value FROM schema_metadata WHERE key='schema_version'"
|
|
102
|
-
).fetchone()
|
|
103
|
-
except sqlite3.OperationalError:
|
|
104
|
-
# Tables don't exist yet — treat as version 0.
|
|
105
|
-
return 0
|
|
106
|
-
if row is None:
|
|
107
|
-
return 0
|
|
108
|
-
try:
|
|
109
|
-
return int(row["value"] if isinstance(row, sqlite3.Row) else row[0])
|
|
110
|
-
except (TypeError, ValueError):
|
|
111
|
-
return 0
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _write_schema_version(conn: sqlite3.Connection, version: int) -> None:
|
|
115
|
-
conn.execute(
|
|
116
|
-
"INSERT OR REPLACE INTO schema_metadata(key, value) VALUES (?, ?)",
|
|
117
|
-
("schema_version", str(version)),
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def _run_pending_migrations(conn: sqlite3.Connection) -> None:
|
|
122
|
-
"""Bring the DB from its current schema_version up to SCHEMA_VERSION."""
|
|
123
|
-
current = _read_schema_version(conn)
|
|
124
|
-
if current >= SCHEMA_VERSION:
|
|
125
|
-
return
|
|
126
|
-
for target in sorted(_MIGRATIONS):
|
|
127
|
-
if target <= current:
|
|
128
|
-
continue
|
|
129
|
-
if target > SCHEMA_VERSION:
|
|
130
|
-
break
|
|
131
|
-
migration = _MIGRATIONS[target]
|
|
132
|
-
logger.info("Running timeline_brain_db migration: v%d → v%d", target - 1, target)
|
|
133
|
-
migration(conn)
|
|
134
|
-
_write_schema_version(conn, target)
|
|
135
|
-
conn.commit()
|
|
136
|
-
current = target
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def _create_schema(conn: sqlite3.Connection) -> None:
|
|
140
|
-
conn.executescript(
|
|
141
|
-
"""
|
|
142
|
-
CREATE TABLE IF NOT EXISTS schema_metadata (
|
|
143
|
-
key TEXT PRIMARY KEY,
|
|
144
|
-
value TEXT NOT NULL
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
CREATE TABLE IF NOT EXISTS timeline_versions (
|
|
148
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
149
|
-
timeline_name TEXT NOT NULL,
|
|
150
|
-
version INTEGER NOT NULL,
|
|
151
|
-
created_at TEXT NOT NULL,
|
|
152
|
-
analysis_run_id TEXT,
|
|
153
|
-
archived_timeline_name TEXT NOT NULL,
|
|
154
|
-
archived_bin_path TEXT NOT NULL,
|
|
155
|
-
drt_export_path TEXT,
|
|
156
|
-
reason TEXT,
|
|
157
|
-
UNIQUE(timeline_name, version)
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
CREATE INDEX IF NOT EXISTS ix_timeline_versions_name
|
|
161
|
-
ON timeline_versions(timeline_name);
|
|
162
|
-
CREATE INDEX IF NOT EXISTS ix_timeline_versions_run
|
|
163
|
-
ON timeline_versions(analysis_run_id);
|
|
164
|
-
|
|
165
|
-
CREATE TABLE IF NOT EXISTS brain_edits (
|
|
166
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
167
|
-
analysis_run_id TEXT NOT NULL,
|
|
168
|
-
timeline_before TEXT,
|
|
169
|
-
timeline_after TEXT,
|
|
170
|
-
edit_type TEXT NOT NULL,
|
|
171
|
-
tool_name TEXT,
|
|
172
|
-
action_name TEXT,
|
|
173
|
-
target_metric TEXT,
|
|
174
|
-
metric_direction TEXT,
|
|
175
|
-
before_value REAL,
|
|
176
|
-
after_value REAL,
|
|
177
|
-
rationale TEXT,
|
|
178
|
-
params_json TEXT,
|
|
179
|
-
result_summary_json TEXT,
|
|
180
|
-
created_at TEXT NOT NULL,
|
|
181
|
-
rolled_back_at TEXT
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
CREATE INDEX IF NOT EXISTS ix_brain_edits_run
|
|
185
|
-
ON brain_edits(analysis_run_id);
|
|
186
|
-
CREATE INDEX IF NOT EXISTS ix_brain_edits_timeline
|
|
187
|
-
ON brain_edits(timeline_after);
|
|
188
|
-
CREATE INDEX IF NOT EXISTS ix_brain_edits_type
|
|
189
|
-
ON brain_edits(edit_type);
|
|
190
|
-
|
|
191
|
-
CREATE TABLE IF NOT EXISTS timeline_clip_usage (
|
|
192
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
193
|
-
media_pool_item_id TEXT NOT NULL,
|
|
194
|
-
timeline_name TEXT NOT NULL,
|
|
195
|
-
timeline_version INTEGER,
|
|
196
|
-
track_type TEXT NOT NULL,
|
|
197
|
-
track_index INTEGER NOT NULL,
|
|
198
|
-
in_frame INTEGER NOT NULL,
|
|
199
|
-
out_frame INTEGER NOT NULL,
|
|
200
|
-
analysis_run_id_at_placement TEXT,
|
|
201
|
-
observed_at TEXT NOT NULL
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
CREATE INDEX IF NOT EXISTS ix_clip_usage_media
|
|
205
|
-
ON timeline_clip_usage(media_pool_item_id);
|
|
206
|
-
CREATE INDEX IF NOT EXISTS ix_clip_usage_timeline
|
|
207
|
-
ON timeline_clip_usage(timeline_name, timeline_version);
|
|
208
|
-
"""
|
|
209
|
-
)
|
|
210
|
-
# Only stamp the version on a *fresh* DB. On an existing DB the recorded
|
|
211
|
-
# version must be preserved so the migration runner can advance it from
|
|
212
|
-
# whatever schema actually exists on disk. Overwriting unconditionally
|
|
213
|
-
# would block every migration by making the DB always look up-to-date.
|
|
214
|
-
if _read_schema_version(conn) == 0:
|
|
215
|
-
_write_schema_version(conn, 1)
|
|
216
|
-
conn.commit()
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def connect(project_root: str) -> sqlite3.Connection:
|
|
220
|
-
"""Return a cached, schema-initialised connection for `project_root`.
|
|
221
|
-
|
|
222
|
-
On first open: creates the tables if missing, then runs any pending
|
|
223
|
-
migrations to bring the schema up to SCHEMA_VERSION.
|
|
224
|
-
"""
|
|
225
|
-
if not project_root:
|
|
226
|
-
raise ValueError("project_root is required")
|
|
227
|
-
path = db_path_for_project(project_root)
|
|
228
|
-
with _CONNECTION_LOCK:
|
|
229
|
-
existing = _CONNECTIONS.get(path)
|
|
230
|
-
if existing is not None:
|
|
231
|
-
return existing
|
|
232
|
-
_ensure_parent_dir(path)
|
|
233
|
-
conn = sqlite3.connect(path, check_same_thread=False, isolation_level=None)
|
|
234
|
-
conn.row_factory = sqlite3.Row
|
|
235
|
-
_init_pragmas(conn)
|
|
236
|
-
_create_schema(conn)
|
|
237
|
-
_run_pending_migrations(conn)
|
|
238
|
-
_CONNECTIONS[path] = conn
|
|
239
|
-
return conn
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def close_all() -> None:
|
|
243
|
-
"""Close every cached connection. Intended for tests + shutdown."""
|
|
244
|
-
with _CONNECTION_LOCK:
|
|
245
|
-
for conn in _CONNECTIONS.values():
|
|
246
|
-
try:
|
|
247
|
-
conn.close()
|
|
248
|
-
except sqlite3.Error:
|
|
249
|
-
pass
|
|
250
|
-
_CONNECTIONS.clear()
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
@contextmanager
|
|
254
|
-
def transaction(project_root: str) -> Iterator[sqlite3.Connection]:
|
|
255
|
-
"""Context manager wrapping a write transaction.
|
|
256
|
-
|
|
257
|
-
Uses BEGIN IMMEDIATE so concurrent dashboard readers don't trigger SQLITE_BUSY
|
|
258
|
-
when a write is in flight. Retries on `database is locked` with exponential
|
|
259
|
-
backoff so that contention between the MCP server and the dashboard doesn't
|
|
260
|
-
surface as a user-visible error.
|
|
261
|
-
"""
|
|
262
|
-
conn = connect(project_root)
|
|
263
|
-
last_exc: Optional[sqlite3.OperationalError] = None
|
|
264
|
-
began = False
|
|
265
|
-
for delay in (0.0, *_BUSY_RETRY_BACKOFF_SECONDS):
|
|
266
|
-
if delay > 0:
|
|
267
|
-
time.sleep(delay)
|
|
268
|
-
try:
|
|
269
|
-
conn.execute("BEGIN IMMEDIATE")
|
|
270
|
-
began = True
|
|
271
|
-
break
|
|
272
|
-
except sqlite3.OperationalError as exc:
|
|
273
|
-
if "locked" not in str(exc).lower():
|
|
274
|
-
raise
|
|
275
|
-
last_exc = exc
|
|
276
|
-
logger.debug("BEGIN IMMEDIATE saw busy lock (%s), retrying after %.2fs", exc, delay)
|
|
277
|
-
if not began:
|
|
278
|
-
assert last_exc is not None
|
|
279
|
-
raise last_exc
|
|
280
|
-
try:
|
|
281
|
-
yield conn
|
|
282
|
-
except Exception:
|
|
283
|
-
try:
|
|
284
|
-
conn.execute("ROLLBACK")
|
|
285
|
-
except sqlite3.OperationalError:
|
|
286
|
-
pass
|
|
287
|
-
raise
|
|
288
|
-
else:
|
|
289
|
-
conn.execute("COMMIT")
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
def reset_for_test(project_root: str) -> None:
|
|
293
|
-
"""Drop + recreate every table. Tests only."""
|
|
294
|
-
path = db_path_for_project(project_root)
|
|
295
|
-
with _CONNECTION_LOCK:
|
|
296
|
-
conn = _CONNECTIONS.pop(path, None)
|
|
297
|
-
if conn is not None:
|
|
298
|
-
try:
|
|
299
|
-
conn.close()
|
|
300
|
-
except sqlite3.Error:
|
|
301
|
-
pass
|
|
302
|
-
for suffix in ("", "-wal", "-shm"):
|
|
303
|
-
try:
|
|
304
|
-
os.remove(path + suffix)
|
|
305
|
-
except OSError:
|
|
306
|
-
pass
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
# ── v2 migration: analysis_runs table + initiator columns ───────────────────
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
@register_migration(2)
|
|
313
|
-
def _migrate_v2_analysis_runs(conn: sqlite3.Connection) -> None:
|
|
314
|
-
"""Add analysis_runs table + initiator columns on existing tables.
|
|
315
|
-
|
|
316
|
-
`analysis_runs` groups a sequence of brain_edits / timeline_versions under
|
|
317
|
-
a single user- or brain-initiated operation. begin_run inserts a row,
|
|
318
|
-
end_run updates ended_at + summary_json with the cumulative metric deltas.
|
|
319
|
-
"""
|
|
320
|
-
conn.executescript(
|
|
321
|
-
"""
|
|
322
|
-
CREATE TABLE IF NOT EXISTS analysis_runs (
|
|
323
|
-
id TEXT PRIMARY KEY,
|
|
324
|
-
label TEXT,
|
|
325
|
-
initiator TEXT,
|
|
326
|
-
started_at TEXT NOT NULL,
|
|
327
|
-
ended_at TEXT,
|
|
328
|
-
summary_json TEXT
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
CREATE INDEX IF NOT EXISTS ix_analysis_runs_started
|
|
332
|
-
ON analysis_runs(started_at);
|
|
333
|
-
"""
|
|
334
|
-
)
|
|
335
|
-
# Add initiator column on brain_edits + timeline_versions for provenance.
|
|
336
|
-
if not _column_exists(conn, "brain_edits", "initiator"):
|
|
337
|
-
conn.execute("ALTER TABLE brain_edits ADD COLUMN initiator TEXT")
|
|
338
|
-
if not _column_exists(conn, "timeline_versions", "initiator"):
|
|
339
|
-
conn.execute("ALTER TABLE timeline_versions ADD COLUMN initiator TEXT")
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
# ── v3 migration: media_pool_changes table ───────────────────────────────────
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
@register_migration(3)
|
|
346
|
-
def _migrate_v3_media_pool_changes(conn: sqlite3.Connection) -> None:
|
|
347
|
-
"""Track destructive media-pool ops (delete clips/folders, replace, relink).
|
|
348
|
-
|
|
349
|
-
These don't mutate a timeline directly but can offline clips referenced by
|
|
350
|
-
timelines. Separate table from brain_edits because the addressable entity
|
|
351
|
-
is a media_pool_item, not a timeline.
|
|
352
|
-
"""
|
|
353
|
-
conn.executescript(
|
|
354
|
-
"""
|
|
355
|
-
CREATE TABLE IF NOT EXISTS media_pool_changes (
|
|
356
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
357
|
-
analysis_run_id TEXT,
|
|
358
|
-
action TEXT NOT NULL,
|
|
359
|
-
target_id TEXT,
|
|
360
|
-
target_name TEXT,
|
|
361
|
-
before_state_json TEXT,
|
|
362
|
-
after_state_json TEXT,
|
|
363
|
-
params_json TEXT,
|
|
364
|
-
initiator TEXT,
|
|
365
|
-
created_at TEXT NOT NULL
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
CREATE INDEX IF NOT EXISTS ix_media_pool_changes_run
|
|
369
|
-
ON media_pool_changes(analysis_run_id);
|
|
370
|
-
CREATE INDEX IF NOT EXISTS ix_media_pool_changes_target
|
|
371
|
-
ON media_pool_changes(target_id);
|
|
372
|
-
CREATE INDEX IF NOT EXISTS ix_media_pool_changes_action
|
|
373
|
-
ON media_pool_changes(action);
|
|
374
|
-
"""
|
|
375
|
-
)
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
# ── v4 migration: thumbnail_path on timeline_versions ───────────────────────
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
@register_migration(4)
|
|
382
|
-
def _migrate_v4_thumbnail_path(conn: sqlite3.Connection) -> None:
|
|
383
|
-
"""Add thumbnail_path column so the History view can render per-version stills."""
|
|
384
|
-
if not _column_exists(conn, "timeline_versions", "thumbnail_path"):
|
|
385
|
-
conn.execute("ALTER TABLE timeline_versions ADD COLUMN thumbnail_path TEXT")
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
# ── v5 migration: analysis_token_usage table for caps enforcement ───────────
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
# ── v6 migration: caps_events for refusal logging ───────────────────────────
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
@register_migration(6)
|
|
395
|
-
def _migrate_v6_caps_events(conn: sqlite3.Connection) -> None:
|
|
396
|
-
"""Log every caps refusal so the dashboard can show recent denials.
|
|
397
|
-
|
|
398
|
-
Distinct from `analysis_token_usage` because refusals never spend tokens —
|
|
399
|
-
we record the *intent* and the cap that blocked it, for debugging "why
|
|
400
|
-
didn't my analysis run".
|
|
401
|
-
"""
|
|
402
|
-
conn.executescript(
|
|
403
|
-
"""
|
|
404
|
-
CREATE TABLE IF NOT EXISTS caps_events (
|
|
405
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
406
|
-
event_type TEXT NOT NULL, -- 'refusal' | 'timeout' | other
|
|
407
|
-
reason TEXT, -- 'over_clip_cap' | 'over_day_cap' | ...
|
|
408
|
-
preset TEXT,
|
|
409
|
-
estimated_vision_tokens INTEGER,
|
|
410
|
-
current_usage_json TEXT,
|
|
411
|
-
cap_json TEXT,
|
|
412
|
-
headroom_json TEXT,
|
|
413
|
-
clip_id TEXT,
|
|
414
|
-
job_id TEXT,
|
|
415
|
-
occurred_at TEXT NOT NULL,
|
|
416
|
-
day_bucket TEXT NOT NULL
|
|
417
|
-
);
|
|
418
|
-
|
|
419
|
-
CREATE INDEX IF NOT EXISTS ix_caps_events_day ON caps_events(day_bucket);
|
|
420
|
-
CREATE INDEX IF NOT EXISTS ix_caps_events_type ON caps_events(event_type);
|
|
421
|
-
"""
|
|
422
|
-
)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
# ── v7 migration: resolve_ai_op_usage ledger for Resolve 21 GPU/AI ops ──────
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
@register_migration(7)
|
|
429
|
-
def _migrate_v7_resolve_ai_op_usage(conn: sqlite3.Connection) -> None:
|
|
430
|
-
"""Ledger for Resolve-local AI ops (audio classification, IntelliSearch,
|
|
431
|
-
slate, motion-deblur, speech generation).
|
|
432
|
-
|
|
433
|
-
These run on Resolve's own GPU/AI engine and do NOT consume the Claude-side
|
|
434
|
-
analysis token budget tracked in `analysis_token_usage`, so they get their
|
|
435
|
-
own ledger. The value is the wall-clock + file/byte accounting for the two
|
|
436
|
-
media-creating ops (remove_motion_blur, generate_speech). `op_class` is
|
|
437
|
-
'analysis' (no media produced) or 'render' (new media file written).
|
|
438
|
-
"""
|
|
439
|
-
conn.executescript(
|
|
440
|
-
"""
|
|
441
|
-
CREATE TABLE IF NOT EXISTS resolve_ai_op_usage (
|
|
442
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
443
|
-
op TEXT NOT NULL,
|
|
444
|
-
op_class TEXT NOT NULL DEFAULT 'analysis',
|
|
445
|
-
clip_id TEXT,
|
|
446
|
-
session_id TEXT,
|
|
447
|
-
success INTEGER NOT NULL DEFAULT 0,
|
|
448
|
-
wall_clock_ms INTEGER NOT NULL DEFAULT 0,
|
|
449
|
-
output_path TEXT,
|
|
450
|
-
output_bytes INTEGER,
|
|
451
|
-
extra_required TEXT,
|
|
452
|
-
error TEXT,
|
|
453
|
-
occurred_at TEXT NOT NULL,
|
|
454
|
-
day_bucket TEXT NOT NULL
|
|
455
|
-
);
|
|
456
|
-
|
|
457
|
-
CREATE INDEX IF NOT EXISTS ix_resolve_ai_op_usage_op
|
|
458
|
-
ON resolve_ai_op_usage(op);
|
|
459
|
-
CREATE INDEX IF NOT EXISTS ix_resolve_ai_op_usage_session
|
|
460
|
-
ON resolve_ai_op_usage(session_id);
|
|
461
|
-
CREATE INDEX IF NOT EXISTS ix_resolve_ai_op_usage_day
|
|
462
|
-
ON resolve_ai_op_usage(day_bucket);
|
|
463
|
-
"""
|
|
464
|
-
)
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
@register_migration(8)
|
|
468
|
-
def _migrate_v8_actor_identity(conn: sqlite3.Connection) -> None:
|
|
469
|
-
"""Instance-level actor provenance (design decision 2026-06-09).
|
|
470
|
-
|
|
471
|
-
Stamps which process kind performed an op — "<instance>:<pid>" where
|
|
472
|
-
instance is stdio / network-sse / network-http / control-panel /
|
|
473
|
-
batch-cli. Complements `initiator` (auto vs manual) on the versioning
|
|
474
|
-
tables: initiator says WHY a row exists, actor says WHO wrote it.
|
|
475
|
-
"""
|
|
476
|
-
for table in ("resolve_ai_op_usage", "brain_edits", "timeline_versions"):
|
|
477
|
-
if not _column_exists(conn, table, "actor"):
|
|
478
|
-
conn.execute(f"ALTER TABLE {table} ADD COLUMN actor TEXT")
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
@register_migration(5)
|
|
482
|
-
def _migrate_v5_analysis_token_usage(conn: sqlite3.Connection) -> None:
|
|
483
|
-
"""Track real vendor token + frame upload usage so caps can enforce budgets.
|
|
484
|
-
|
|
485
|
-
`scope` is 'clip' | 'job' | 'session' | 'global'. `scope_key` is the entity
|
|
486
|
-
id within that scope (clip_id, job_id, session_id, or empty for global).
|
|
487
|
-
`day_bucket` (YYYY-MM-DD) is indexed for fast per-day rollup queries.
|
|
488
|
-
"""
|
|
489
|
-
conn.executescript(
|
|
490
|
-
"""
|
|
491
|
-
CREATE TABLE IF NOT EXISTS analysis_token_usage (
|
|
492
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
493
|
-
scope TEXT NOT NULL,
|
|
494
|
-
scope_key TEXT,
|
|
495
|
-
vision_tokens INTEGER NOT NULL DEFAULT 0,
|
|
496
|
-
transcription_tokens INTEGER NOT NULL DEFAULT 0,
|
|
497
|
-
frames_uploaded INTEGER NOT NULL DEFAULT 0,
|
|
498
|
-
wall_clock_ms INTEGER NOT NULL DEFAULT 0,
|
|
499
|
-
preset_at_call TEXT,
|
|
500
|
-
occurred_at TEXT NOT NULL,
|
|
501
|
-
day_bucket TEXT NOT NULL
|
|
502
|
-
);
|
|
503
|
-
|
|
504
|
-
CREATE INDEX IF NOT EXISTS ix_analysis_token_usage_scope
|
|
505
|
-
ON analysis_token_usage(scope, scope_key);
|
|
506
|
-
CREATE INDEX IF NOT EXISTS ix_analysis_token_usage_day
|
|
507
|
-
ON analysis_token_usage(day_bucket);
|
|
508
|
-
"""
|
|
509
|
-
)
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
@register_migration(9)
|
|
513
|
-
def _migrate_v9_analysis_core(conn: sqlite3.Connection) -> None:
|
|
514
|
-
"""C1 — DB-canonical clip analysis (Phase A of the analysis/edit-engine program).
|
|
515
|
-
|
|
516
|
-
The DB becomes the source of truth for clip analysis; analysis.json becomes
|
|
517
|
-
a derived export written in lockstep. Shape is hybrid: a canonical
|
|
518
|
-
full-report blob per clip (`analysis_reports`) plus normalized tables for
|
|
519
|
-
what downstream phases query (shots, subjective fields with per-field
|
|
520
|
-
provenance, transcript segments, sampled frames, QC observations).
|
|
521
|
-
Computed layers (technical/motion/cuts/audio) stay inside the blob — they
|
|
522
|
-
re-derive cleanly from source media and nothing queries them row-wise.
|
|
523
|
-
"""
|
|
524
|
-
conn.executescript(
|
|
525
|
-
"""
|
|
526
|
-
CREATE TABLE IF NOT EXISTS clips (
|
|
527
|
-
clip_uuid TEXT PRIMARY KEY, -- rename-stable canonical hash (12-hex)
|
|
528
|
-
clip_dir TEXT, -- report folder name under clips/
|
|
529
|
-
resolve_clip_id TEXT,
|
|
530
|
-
media_id TEXT,
|
|
531
|
-
clip_name TEXT,
|
|
532
|
-
file_path TEXT,
|
|
533
|
-
bin_path TEXT,
|
|
534
|
-
duration_seconds REAL,
|
|
535
|
-
fps REAL,
|
|
536
|
-
resolution TEXT,
|
|
537
|
-
media_type TEXT,
|
|
538
|
-
summary TEXT,
|
|
539
|
-
overall_motion_level TEXT,
|
|
540
|
-
cut_count INTEGER,
|
|
541
|
-
shot_count INTEGER,
|
|
542
|
-
analysis_version TEXT,
|
|
543
|
-
depth TEXT,
|
|
544
|
-
signature_hash TEXT,
|
|
545
|
-
analyzed_at TEXT,
|
|
546
|
-
vision_status TEXT,
|
|
547
|
-
vision_committed_at TEXT,
|
|
548
|
-
created_at TEXT NOT NULL,
|
|
549
|
-
updated_at TEXT NOT NULL
|
|
550
|
-
);
|
|
551
|
-
|
|
552
|
-
CREATE INDEX IF NOT EXISTS ix_clips_clip_name ON clips(clip_name);
|
|
553
|
-
CREATE INDEX IF NOT EXISTS ix_clips_file_path ON clips(file_path);
|
|
554
|
-
|
|
555
|
-
-- Every stable id a clip is known by (legacy hashes, clip_id, media_id,
|
|
556
|
-
-- folder hash, normalized file path) → clip_uuid. Lookup table only.
|
|
557
|
-
CREATE TABLE IF NOT EXISTS clip_aliases (
|
|
558
|
-
alias TEXT NOT NULL,
|
|
559
|
-
clip_uuid TEXT NOT NULL,
|
|
560
|
-
kind TEXT,
|
|
561
|
-
PRIMARY KEY (alias, clip_uuid)
|
|
562
|
-
);
|
|
563
|
-
|
|
564
|
-
CREATE INDEX IF NOT EXISTS ix_clip_aliases_clip ON clip_aliases(clip_uuid);
|
|
565
|
-
|
|
566
|
-
-- Canonical full analysis payload. analysis.json is exported FROM this.
|
|
567
|
-
CREATE TABLE IF NOT EXISTS analysis_reports (
|
|
568
|
-
clip_uuid TEXT PRIMARY KEY,
|
|
569
|
-
report_json TEXT NOT NULL,
|
|
570
|
-
signature_hash TEXT,
|
|
571
|
-
analyzed_at TEXT,
|
|
572
|
-
written_at TEXT NOT NULL,
|
|
573
|
-
FOREIGN KEY (clip_uuid) REFERENCES clips(clip_uuid) ON DELETE CASCADE
|
|
574
|
-
);
|
|
575
|
-
|
|
576
|
-
CREATE TABLE IF NOT EXISTS shots (
|
|
577
|
-
shot_uuid TEXT PRIMARY KEY, -- short_hash(clip_uuid + rounded time region)
|
|
578
|
-
clip_uuid TEXT NOT NULL,
|
|
579
|
-
shot_index INTEGER NOT NULL,
|
|
580
|
-
time_seconds_start REAL,
|
|
581
|
-
time_seconds_end REAL,
|
|
582
|
-
description TEXT,
|
|
583
|
-
qc_flags_json TEXT,
|
|
584
|
-
frame_indices_json TEXT,
|
|
585
|
-
extra_json TEXT, -- forward-compat: any other shot keys
|
|
586
|
-
created_at TEXT NOT NULL,
|
|
587
|
-
updated_at TEXT NOT NULL,
|
|
588
|
-
UNIQUE(clip_uuid, shot_index),
|
|
589
|
-
FOREIGN KEY (clip_uuid) REFERENCES clips(clip_uuid) ON DELETE CASCADE
|
|
590
|
-
);
|
|
591
|
-
|
|
592
|
-
CREATE INDEX IF NOT EXISTS ix_shots_clip ON shots(clip_uuid, shot_index);
|
|
593
|
-
|
|
594
|
-
-- Per-field provenance for subjective (vision/human) values.
|
|
595
|
-
-- Current value = the row with superseded_at IS NULL.
|
|
596
|
-
CREATE TABLE IF NOT EXISTS subjective_fields (
|
|
597
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
598
|
-
entity_type TEXT NOT NULL CHECK(entity_type IN ('clip', 'shot')),
|
|
599
|
-
entity_uuid TEXT NOT NULL,
|
|
600
|
-
field_path TEXT NOT NULL,
|
|
601
|
-
value_json TEXT NOT NULL,
|
|
602
|
-
confidence TEXT,
|
|
603
|
-
source TEXT NOT NULL, -- 'vision_v0.2' | 'human' | ...
|
|
604
|
-
source_model TEXT,
|
|
605
|
-
author TEXT NOT NULL,
|
|
606
|
-
timestamp TEXT NOT NULL,
|
|
607
|
-
superseded_at TEXT
|
|
608
|
-
);
|
|
609
|
-
|
|
610
|
-
CREATE INDEX IF NOT EXISTS ix_sf_entity
|
|
611
|
-
ON subjective_fields(entity_type, entity_uuid, field_path);
|
|
612
|
-
CREATE INDEX IF NOT EXISTS ix_sf_current
|
|
613
|
-
ON subjective_fields(entity_type, entity_uuid, field_path)
|
|
614
|
-
WHERE superseded_at IS NULL;
|
|
615
|
-
|
|
616
|
-
CREATE TABLE IF NOT EXISTS field_changelog (
|
|
617
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
618
|
-
entity_type TEXT NOT NULL,
|
|
619
|
-
entity_uuid TEXT NOT NULL,
|
|
620
|
-
field_path TEXT NOT NULL,
|
|
621
|
-
previous_value_json TEXT,
|
|
622
|
-
new_value_json TEXT NOT NULL,
|
|
623
|
-
previous_source TEXT,
|
|
624
|
-
new_source TEXT NOT NULL,
|
|
625
|
-
previous_author TEXT,
|
|
626
|
-
new_author TEXT NOT NULL,
|
|
627
|
-
change_reason TEXT,
|
|
628
|
-
timestamp TEXT NOT NULL
|
|
629
|
-
);
|
|
630
|
-
|
|
631
|
-
CREATE INDEX IF NOT EXISTS ix_changelog_entity
|
|
632
|
-
ON field_changelog(entity_type, entity_uuid, timestamp);
|
|
633
|
-
|
|
634
|
-
CREATE TABLE IF NOT EXISTS transcript_segments (
|
|
635
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
636
|
-
clip_uuid TEXT NOT NULL,
|
|
637
|
-
segment_index INTEGER NOT NULL,
|
|
638
|
-
start_seconds REAL,
|
|
639
|
-
end_seconds REAL,
|
|
640
|
-
text TEXT,
|
|
641
|
-
speaker_id TEXT,
|
|
642
|
-
UNIQUE(clip_uuid, segment_index),
|
|
643
|
-
FOREIGN KEY (clip_uuid) REFERENCES clips(clip_uuid) ON DELETE CASCADE
|
|
644
|
-
);
|
|
645
|
-
|
|
646
|
-
CREATE INDEX IF NOT EXISTS ix_transcript_segments_clip
|
|
647
|
-
ON transcript_segments(clip_uuid, start_seconds);
|
|
648
|
-
|
|
649
|
-
CREATE TABLE IF NOT EXISTS frames (
|
|
650
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
651
|
-
clip_uuid TEXT NOT NULL,
|
|
652
|
-
shot_uuid TEXT,
|
|
653
|
-
frame_index INTEGER NOT NULL,
|
|
654
|
-
time_seconds REAL,
|
|
655
|
-
frame_path TEXT,
|
|
656
|
-
selection_reason TEXT,
|
|
657
|
-
motion_peak INTEGER NOT NULL DEFAULT 0,
|
|
658
|
-
UNIQUE(clip_uuid, frame_index),
|
|
659
|
-
FOREIGN KEY (clip_uuid) REFERENCES clips(clip_uuid) ON DELETE CASCADE
|
|
660
|
-
);
|
|
661
|
-
|
|
662
|
-
CREATE INDEX IF NOT EXISTS ix_frames_clip ON frames(clip_uuid, frame_index);
|
|
663
|
-
CREATE INDEX IF NOT EXISTS ix_frames_shot ON frames(shot_uuid);
|
|
664
|
-
|
|
665
|
-
CREATE TABLE IF NOT EXISTS qc_observations (
|
|
666
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
667
|
-
clip_uuid TEXT NOT NULL,
|
|
668
|
-
shot_uuid TEXT,
|
|
669
|
-
observation_type TEXT NOT NULL,
|
|
670
|
-
severity TEXT NOT NULL DEFAULT 'info',
|
|
671
|
-
message TEXT NOT NULL,
|
|
672
|
-
related_shot_indices_json TEXT,
|
|
673
|
-
confidence TEXT,
|
|
674
|
-
source TEXT NOT NULL,
|
|
675
|
-
resolved INTEGER NOT NULL DEFAULT 0,
|
|
676
|
-
resolved_by TEXT,
|
|
677
|
-
resolved_at TEXT,
|
|
678
|
-
resolution_note TEXT,
|
|
679
|
-
created_at TEXT NOT NULL,
|
|
680
|
-
FOREIGN KEY (clip_uuid) REFERENCES clips(clip_uuid) ON DELETE CASCADE
|
|
681
|
-
);
|
|
682
|
-
|
|
683
|
-
CREATE INDEX IF NOT EXISTS ix_qc_clip ON qc_observations(clip_uuid);
|
|
684
|
-
CREATE INDEX IF NOT EXISTS ix_qc_unresolved
|
|
685
|
-
ON qc_observations(clip_uuid, resolved) WHERE resolved = 0;
|
|
686
|
-
"""
|
|
687
|
-
)
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
@register_migration(10)
|
|
691
|
-
def _migrate_v10_embeddings(conn: sqlite3.Connection) -> None:
|
|
692
|
-
"""C3 — embeddings for similarity search (Phase C of the analysis program).
|
|
693
|
-
|
|
694
|
-
One row per (entity, kind, model). Vectors are float32 BLOBs; similarity
|
|
695
|
-
is brute-force cosine in the app layer (thousands of rows, not millions).
|
|
696
|
-
`content_hash` fingerprints the embedded content so re-runs only re-embed
|
|
697
|
-
entities whose text/frames changed.
|
|
698
|
-
"""
|
|
699
|
-
conn.executescript(
|
|
700
|
-
"""
|
|
701
|
-
CREATE TABLE IF NOT EXISTS embeddings (
|
|
702
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
703
|
-
entity_type TEXT NOT NULL CHECK(entity_type IN ('clip', 'shot', 'frame', 'segment')),
|
|
704
|
-
entity_uuid TEXT NOT NULL,
|
|
705
|
-
embedding_kind TEXT NOT NULL, -- 'text' | 'visual'
|
|
706
|
-
model_name TEXT NOT NULL,
|
|
707
|
-
dimension INTEGER NOT NULL,
|
|
708
|
-
vector BLOB NOT NULL,
|
|
709
|
-
content_hash TEXT,
|
|
710
|
-
computed_at TEXT NOT NULL,
|
|
711
|
-
UNIQUE(entity_type, entity_uuid, embedding_kind, model_name)
|
|
712
|
-
);
|
|
713
|
-
|
|
714
|
-
CREATE INDEX IF NOT EXISTS ix_embeddings_kind
|
|
715
|
-
ON embeddings(embedding_kind, model_name);
|
|
716
|
-
CREATE INDEX IF NOT EXISTS ix_embeddings_entity
|
|
717
|
-
ON embeddings(entity_type, entity_uuid);
|
|
718
|
-
"""
|
|
719
|
-
)
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
@register_migration(11)
|
|
723
|
-
def _migrate_v11_entities(conn: sqlite3.Connection) -> None:
|
|
724
|
-
"""C5 — cross-clip entities (Phase D of the analysis program).
|
|
725
|
-
|
|
726
|
-
`entities` rows start as provisional clusters over the v10 visual
|
|
727
|
-
embeddings (source='clustering', label NULL) and are enriched by one
|
|
728
|
-
host-vision call per cluster representative (source='vision_entity_v1')
|
|
729
|
-
or by humans. `entity_appearances` records every frame an entity was
|
|
730
|
-
seen in, with the clip/shot derivation and the match similarity.
|
|
731
|
-
"""
|
|
732
|
-
conn.executescript(
|
|
733
|
-
"""
|
|
734
|
-
CREATE TABLE IF NOT EXISTS entities (
|
|
735
|
-
entity_uuid TEXT PRIMARY KEY,
|
|
736
|
-
kind TEXT, -- person|place|object|unknown
|
|
737
|
-
label TEXT,
|
|
738
|
-
description TEXT,
|
|
739
|
-
confidence TEXT,
|
|
740
|
-
source TEXT NOT NULL,
|
|
741
|
-
representative_frame_ref TEXT, -- 'clip_uuid:frame_index'
|
|
742
|
-
representative_frame_path TEXT,
|
|
743
|
-
cluster_size INTEGER,
|
|
744
|
-
created_at TEXT NOT NULL,
|
|
745
|
-
updated_at TEXT NOT NULL
|
|
746
|
-
);
|
|
747
|
-
|
|
748
|
-
CREATE INDEX IF NOT EXISTS ix_entities_kind ON entities(kind);
|
|
749
|
-
|
|
750
|
-
CREATE TABLE IF NOT EXISTS entity_appearances (
|
|
751
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
752
|
-
entity_uuid TEXT NOT NULL,
|
|
753
|
-
clip_uuid TEXT NOT NULL,
|
|
754
|
-
shot_uuid TEXT,
|
|
755
|
-
frame_ref TEXT NOT NULL, -- 'clip_uuid:frame_index'
|
|
756
|
-
similarity REAL,
|
|
757
|
-
UNIQUE(entity_uuid, frame_ref),
|
|
758
|
-
FOREIGN KEY (entity_uuid) REFERENCES entities(entity_uuid) ON DELETE CASCADE
|
|
759
|
-
);
|
|
760
|
-
|
|
761
|
-
CREATE INDEX IF NOT EXISTS ix_entity_appearances_entity
|
|
762
|
-
ON entity_appearances(entity_uuid);
|
|
763
|
-
CREATE INDEX IF NOT EXISTS ix_entity_appearances_clip
|
|
764
|
-
ON entity_appearances(clip_uuid);
|
|
765
|
-
"""
|
|
766
|
-
)
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
@register_migration(12)
|
|
770
|
-
def _migrate_v12_shot_relationships(conn: sqlite3.Connection) -> None:
|
|
771
|
-
"""Cross-shot relationships (spec §4 — pattern recognition only).
|
|
772
|
-
|
|
773
|
-
Three types: same_setup_as / alt_take_of (symmetric — stored once with
|
|
774
|
-
the canonically-ordered pair) and continues_from (directional — the
|
|
775
|
-
SOURCE shot continues from the TARGET shot, i.e. target precedes source).
|
|
776
|
-
Rows are append-only with supersede semantics: current rows have
|
|
777
|
-
superseded_at IS NULL.
|
|
778
|
-
"""
|
|
779
|
-
conn.executescript(
|
|
780
|
-
"""
|
|
781
|
-
CREATE TABLE IF NOT EXISTS shot_relationships (
|
|
782
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
783
|
-
source_shot_uuid TEXT NOT NULL,
|
|
784
|
-
target_shot_uuid TEXT NOT NULL,
|
|
785
|
-
relationship_type TEXT NOT NULL CHECK(relationship_type IN
|
|
786
|
-
('same_setup_as', 'continues_from', 'alt_take_of')),
|
|
787
|
-
confidence TEXT,
|
|
788
|
-
source TEXT NOT NULL,
|
|
789
|
-
author TEXT NOT NULL,
|
|
790
|
-
timestamp TEXT NOT NULL,
|
|
791
|
-
superseded_at TEXT,
|
|
792
|
-
UNIQUE(source_shot_uuid, target_shot_uuid, relationship_type, timestamp),
|
|
793
|
-
FOREIGN KEY (source_shot_uuid) REFERENCES shots(shot_uuid) ON DELETE CASCADE,
|
|
794
|
-
FOREIGN KEY (target_shot_uuid) REFERENCES shots(shot_uuid) ON DELETE CASCADE
|
|
795
|
-
);
|
|
796
|
-
|
|
797
|
-
CREATE INDEX IF NOT EXISTS idx_relationships_source
|
|
798
|
-
ON shot_relationships(source_shot_uuid) WHERE superseded_at IS NULL;
|
|
799
|
-
CREATE INDEX IF NOT EXISTS idx_relationships_target
|
|
800
|
-
ON shot_relationships(target_shot_uuid) WHERE superseded_at IS NULL;
|
|
801
|
-
CREATE INDEX IF NOT EXISTS idx_relationships_type
|
|
802
|
-
ON shot_relationships(relationship_type) WHERE superseded_at IS NULL;
|
|
803
|
-
"""
|
|
804
|
-
)
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
def latest_version(conn: sqlite3.Connection, timeline_name: str) -> Optional[int]:
|
|
808
|
-
"""Return the highest archived version number for `timeline_name`, or None."""
|
|
809
|
-
row = conn.execute(
|
|
810
|
-
"SELECT MAX(version) AS v FROM timeline_versions WHERE timeline_name = ?",
|
|
811
|
-
(timeline_name,),
|
|
812
|
-
).fetchone()
|
|
813
|
-
if row is None or row["v"] is None:
|
|
814
|
-
return None
|
|
815
|
-
return int(row["v"])
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
def run_archived_for_run(conn: sqlite3.Connection, timeline_name: str, analysis_run_id: str) -> bool:
|
|
819
|
-
"""Has an archive already happened for this (timeline, run) pair?"""
|
|
820
|
-
if not analysis_run_id:
|
|
821
|
-
return False
|
|
822
|
-
row = conn.execute(
|
|
823
|
-
"SELECT 1 FROM timeline_versions WHERE timeline_name = ? AND analysis_run_id = ? LIMIT 1",
|
|
824
|
-
(timeline_name, analysis_run_id),
|
|
825
|
-
).fetchone()
|
|
826
|
-
return row is not None
|