@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,606 +0,0 @@
|
|
|
1
|
-
"""Version-on-mutate hook + destructive-action registry (C6).
|
|
2
|
-
|
|
3
|
-
Wraps each top-level compound tool function in `src/server.py` so that any
|
|
4
|
-
destructive sub-action automatically:
|
|
5
|
-
|
|
6
|
-
1. Resolves the project root via the configured project-root provider.
|
|
7
|
-
2. Calls `timeline_versioning.ensure_versioned_before_mutation()` for the run.
|
|
8
|
-
3. Captures the declared metric's `before_value` (if `metric` was passed in params).
|
|
9
|
-
4. Runs the original handler.
|
|
10
|
-
5. Captures the declared metric's `after_value` (and gap stats by default).
|
|
11
|
-
6. Logs a `brain_edits` row.
|
|
12
|
-
|
|
13
|
-
If anything in the hook fails — Resolve isn't connected, no current project,
|
|
14
|
-
project_root can't be resolved, the DB can't be opened — the wrapper degrades
|
|
15
|
-
silently and the underlying handler still runs. The user's edit must never be
|
|
16
|
-
blocked by a versioning failure. (Failures are logged at WARNING.)
|
|
17
|
-
|
|
18
|
-
## Registering destructive actions
|
|
19
|
-
|
|
20
|
-
Add `(tool_name, action_name)` entries to `DESTRUCTIVE_ACTIONS_BY_TOOL`. The
|
|
21
|
-
enforcement test in `tests/test_destructive_decorator_coverage.py` confirms
|
|
22
|
-
every tool that owns destructive actions is decorated.
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
from __future__ import annotations
|
|
26
|
-
|
|
27
|
-
import functools
|
|
28
|
-
import logging
|
|
29
|
-
from typing import Any, Callable, Dict, FrozenSet, Optional, Tuple
|
|
30
|
-
|
|
31
|
-
from src.utils import analysis_runs, brain_edits, media_pool_changes, timeline_versioning
|
|
32
|
-
|
|
33
|
-
logger = logging.getLogger("resolve-mcp.destructive-hook")
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# ── Destructive action registry ──────────────────────────────────────────────
|
|
37
|
-
#
|
|
38
|
-
# Tool name → frozenset of action strings that mutate the working timeline (or
|
|
39
|
-
# the timeline-item subtree). Adding a new destructive action means: (a) add it
|
|
40
|
-
# here; (b) optionally add a metric-capture entry below if it has a sensible
|
|
41
|
-
# default metric.
|
|
42
|
-
|
|
43
|
-
# Each action is keyed under the tool whose @_destructive_op wrapper actually
|
|
44
|
-
# DISPATCHES it. Historically many entries were filed under the wrong tool key
|
|
45
|
-
# (e.g. create_timeline/auto_sync_audio under "timeline" though they are media_pool
|
|
46
|
-
# actions; set_cdl/copy_grades under "timeline_item" though timeline_item_color
|
|
47
|
-
# dispatches them; *_fusion_comp / *_take stale names that never matched), so
|
|
48
|
-
# is_destructive() returned False and version-on-mutate archiving silently did not
|
|
49
|
-
# fire. EX-REG re-filed every action under its real dispatcher and dropped inert
|
|
50
|
-
# entries whose tool is not @_destructive_op-wrapped (e.g. media_pool_item
|
|
51
|
-
# replace_clip/link_*). The test_destructive_registry_drift guard asserts every
|
|
52
|
-
# string here is a real handler so this can't regress.
|
|
53
|
-
DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
|
|
54
|
-
"media_pool": frozenset({
|
|
55
|
-
"delete_clips",
|
|
56
|
-
"delete_folders",
|
|
57
|
-
"move_clips",
|
|
58
|
-
"move_folders",
|
|
59
|
-
"delete_clip_mattes",
|
|
60
|
-
"delete_timelines",
|
|
61
|
-
"create_timeline",
|
|
62
|
-
"create_timeline_from_clips",
|
|
63
|
-
"append_to_timeline",
|
|
64
|
-
"setup_multicam_timeline",
|
|
65
|
-
"create_stereo_clip",
|
|
66
|
-
"auto_sync_audio",
|
|
67
|
-
"set_clip_marks",
|
|
68
|
-
"clear_clip_marks",
|
|
69
|
-
}),
|
|
70
|
-
"edit_engine": frozenset({
|
|
71
|
-
"execute_selects",
|
|
72
|
-
"execute_tighten",
|
|
73
|
-
"execute_swap",
|
|
74
|
-
}),
|
|
75
|
-
"timeline": frozenset({
|
|
76
|
-
"delete_clips",
|
|
77
|
-
"move_clips",
|
|
78
|
-
"duplicate_clips",
|
|
79
|
-
"copy_clips",
|
|
80
|
-
"copy_range",
|
|
81
|
-
"duplicate_range",
|
|
82
|
-
"overwrite_range",
|
|
83
|
-
"lift_range",
|
|
84
|
-
"apply_cuts",
|
|
85
|
-
"create_compound_clip",
|
|
86
|
-
"create_fusion_clip",
|
|
87
|
-
"convert_to_stereo",
|
|
88
|
-
"set_clips_linked",
|
|
89
|
-
"duplicate",
|
|
90
|
-
"insert_generator",
|
|
91
|
-
"insert_fusion_generator",
|
|
92
|
-
"insert_fusion_composition",
|
|
93
|
-
"insert_ofx_generator",
|
|
94
|
-
"insert_title",
|
|
95
|
-
"insert_fusion_title",
|
|
96
|
-
"add_track",
|
|
97
|
-
"delete_track",
|
|
98
|
-
"set_track_lock",
|
|
99
|
-
"set_track_enable",
|
|
100
|
-
"set_track_name",
|
|
101
|
-
"set_voice_isolation_state",
|
|
102
|
-
"set_name",
|
|
103
|
-
"set_start_timecode",
|
|
104
|
-
"set_setting",
|
|
105
|
-
"set_mark_in_out",
|
|
106
|
-
"clear_mark_in_out",
|
|
107
|
-
"set_title_text",
|
|
108
|
-
"import_into_timeline",
|
|
109
|
-
}),
|
|
110
|
-
"timeline_markers": frozenset({
|
|
111
|
-
"add",
|
|
112
|
-
"delete_at_frame",
|
|
113
|
-
"delete_by_custom_data",
|
|
114
|
-
"delete_by_color",
|
|
115
|
-
"update_custom_data",
|
|
116
|
-
}),
|
|
117
|
-
"timeline_ai": frozenset({
|
|
118
|
-
"detect_scene_cuts",
|
|
119
|
-
"analyze_dolby_vision",
|
|
120
|
-
"create_subtitles",
|
|
121
|
-
}),
|
|
122
|
-
"timeline_item": frozenset({
|
|
123
|
-
"set_clip_enabled",
|
|
124
|
-
"set_property",
|
|
125
|
-
"set_name",
|
|
126
|
-
"set_voice_isolation_state",
|
|
127
|
-
"update_sidecar",
|
|
128
|
-
"set_transform",
|
|
129
|
-
"set_crop",
|
|
130
|
-
"set_retime",
|
|
131
|
-
"set_composite",
|
|
132
|
-
"set_audio",
|
|
133
|
-
}),
|
|
134
|
-
"timeline_item_markers": frozenset({
|
|
135
|
-
"add",
|
|
136
|
-
"add_flag",
|
|
137
|
-
"clear_flags",
|
|
138
|
-
"set_clip_color",
|
|
139
|
-
"clear_clip_color",
|
|
140
|
-
"delete_at_frame",
|
|
141
|
-
"delete_by_custom_data",
|
|
142
|
-
"delete_by_color",
|
|
143
|
-
"update_custom_data",
|
|
144
|
-
}),
|
|
145
|
-
"timeline_item_fusion": frozenset({
|
|
146
|
-
"add_comp",
|
|
147
|
-
"delete_comp",
|
|
148
|
-
"import_comp",
|
|
149
|
-
"load_comp",
|
|
150
|
-
"rename_comp",
|
|
151
|
-
}),
|
|
152
|
-
"timeline_item_color": frozenset({
|
|
153
|
-
"set_cdl",
|
|
154
|
-
"copy_grades",
|
|
155
|
-
"reset_all_node_colors",
|
|
156
|
-
"assign_color_group",
|
|
157
|
-
"remove_from_color_group",
|
|
158
|
-
"create_magic_mask",
|
|
159
|
-
"regenerate_magic_mask",
|
|
160
|
-
"smart_reframe",
|
|
161
|
-
"stabilize",
|
|
162
|
-
"export_lut",
|
|
163
|
-
"add_version",
|
|
164
|
-
"delete_version",
|
|
165
|
-
"load_version",
|
|
166
|
-
"rename_version",
|
|
167
|
-
"set_color_cache",
|
|
168
|
-
"set_fusion_cache",
|
|
169
|
-
}),
|
|
170
|
-
"timeline_item_takes": frozenset({
|
|
171
|
-
"add",
|
|
172
|
-
"delete",
|
|
173
|
-
"select",
|
|
174
|
-
"finalize",
|
|
175
|
-
}),
|
|
176
|
-
"graph": frozenset({
|
|
177
|
-
"set_lut",
|
|
178
|
-
"set_node_enabled",
|
|
179
|
-
"apply_arri_cdl_lut",
|
|
180
|
-
"apply_grade_from_drx",
|
|
181
|
-
"reset_all_grades",
|
|
182
|
-
}),
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
# ── No-archive filters ──────────────────────────────────────────────────────
|
|
187
|
-
#
|
|
188
|
-
# Some destructive *actions* are technically mutations but their *payloads* are
|
|
189
|
-
# free-text metadata that nobody wants to roll back ("Notes" updates, comment
|
|
190
|
-
# tweaks, etc.). Registering them in NO_ARCHIVE_ON_KEYS means: if the action's
|
|
191
|
-
# `key` param is in this set, skip versioning. Other key choices still archive.
|
|
192
|
-
#
|
|
193
|
-
# Shape: (tool, action) → {keys_that_skip_archiving}
|
|
194
|
-
# - empty set or missing entry → always archive
|
|
195
|
-
# - non-empty set → only the listed keys skip archiving
|
|
196
|
-
|
|
197
|
-
NO_ARCHIVE_ON_KEYS: Dict[Tuple[str, str], frozenset] = {
|
|
198
|
-
("timeline_item", "set_property"): frozenset({"Notes"}),
|
|
199
|
-
# set_clip_property on timeline can target Name/Notes/Comments/etc. — only
|
|
200
|
-
# Notes/Comments are noise; Name changes are real edits and stay archived.
|
|
201
|
-
("timeline", "set_clip_property"): frozenset({"Notes", "Comments"}),
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
# ── Strict-mode allowlist ───────────────────────────────────────────────────
|
|
206
|
-
#
|
|
207
|
-
# Actions in this set REFUSE to run if the version-on-mutate archive fails. For
|
|
208
|
-
# everything else (the vast majority), the underlying handler still runs even
|
|
209
|
-
# when archive fails — preferring user progress over perfect history. But
|
|
210
|
-
# `delete_timelines` permanently destroys timelines; if we couldn't archive
|
|
211
|
-
# first, the user almost certainly wants to know before losing data.
|
|
212
|
-
|
|
213
|
-
STRICT_DEFAULT_ACTIONS: frozenset = frozenset({
|
|
214
|
-
("timeline", "delete_track"),
|
|
215
|
-
# NOTE: delete_timelines is a media_pool action (EX-REG re-keyed it); it is
|
|
216
|
-
# archive+confirm-token gated (EX3) rather than strict, per the decision that
|
|
217
|
-
# strict over-blocks deletes when there is nothing to archive.
|
|
218
|
-
# delete_clips with ripple=True is destructive in a way single-clip delete
|
|
219
|
-
# isn't — handled separately in is_strict_required() because it depends on
|
|
220
|
-
# the params payload, not just the action name.
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def _payload_only_touches_no_archive_keys(
|
|
225
|
-
tool_name: str, action: str, params: Optional[Dict[str, Any]],
|
|
226
|
-
) -> bool:
|
|
227
|
-
"""Return True iff the destructive action's payload is filtered out."""
|
|
228
|
-
allowlist = NO_ARCHIVE_ON_KEYS.get((tool_name, action))
|
|
229
|
-
if not allowlist or not isinstance(params, dict):
|
|
230
|
-
return False
|
|
231
|
-
# Only filter if `key` param is present (set_property style). Otherwise
|
|
232
|
-
# default to archiving.
|
|
233
|
-
key = params.get("key")
|
|
234
|
-
if not key:
|
|
235
|
-
return False
|
|
236
|
-
return str(key) in allowlist
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
def is_destructive(tool_name: str, action: str, params: Optional[Dict[str, Any]] = None) -> bool:
|
|
240
|
-
"""Should this (tool, action, payload) trigger version-on-mutate?
|
|
241
|
-
|
|
242
|
-
Checks the registry AND the payload-aware no-archive filter. Idempotent.
|
|
243
|
-
"""
|
|
244
|
-
if action not in DESTRUCTIVE_ACTIONS_BY_TOOL.get(tool_name, frozenset()):
|
|
245
|
-
return False
|
|
246
|
-
if _payload_only_touches_no_archive_keys(tool_name, action, params):
|
|
247
|
-
return False
|
|
248
|
-
return True
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def is_strict_required(tool_name: str, action: str, params: Optional[Dict[str, Any]]) -> bool:
|
|
252
|
-
"""True if the call must refuse to run when archive fails.
|
|
253
|
-
|
|
254
|
-
Triggers on (a) STRICT_DEFAULT_ACTIONS membership, (b) ripple delete on
|
|
255
|
-
timeline.delete_clips, or (c) explicit `strict=True` in params.
|
|
256
|
-
"""
|
|
257
|
-
if isinstance(params, dict) and params.get("strict") is True:
|
|
258
|
-
return True
|
|
259
|
-
if (tool_name, action) in STRICT_DEFAULT_ACTIONS:
|
|
260
|
-
return True
|
|
261
|
-
if (
|
|
262
|
-
tool_name == "timeline"
|
|
263
|
-
and action == "delete_clips"
|
|
264
|
-
and isinstance(params, dict)
|
|
265
|
-
and bool(params.get("ripple"))
|
|
266
|
-
):
|
|
267
|
-
return True
|
|
268
|
-
return False
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
# ── Provider hooks ───────────────────────────────────────────────────────────
|
|
272
|
-
|
|
273
|
-
_ProjectRootProvider = Callable[[], Optional[Tuple[Any, Any, str, Optional[str]]]]
|
|
274
|
-
"""Returns (resolve_handle, project_handle, project_root, project_name) or None."""
|
|
275
|
-
|
|
276
|
-
_PROVIDER: Optional[_ProjectRootProvider] = None
|
|
277
|
-
|
|
278
|
-
_PreferenceProvider = Callable[[str], Any]
|
|
279
|
-
"""Reads a named preference (e.g. timeline_versioning_auto_save_after_archive)."""
|
|
280
|
-
|
|
281
|
-
_PREFERENCE_PROVIDER: Optional[_PreferenceProvider] = None
|
|
282
|
-
|
|
283
|
-
_PendingConfirmCheck = Callable[[str, str, Optional[Dict[str, Any]]], bool]
|
|
284
|
-
"""Returns True iff this (tool, action, params) call will be short-circuited by
|
|
285
|
-
the confirm-token gate to issue a fresh token (no mutation will run). When True
|
|
286
|
-
the wrapper skips the pre-mutation archive — there's nothing yet to archive.
|
|
287
|
-
"""
|
|
288
|
-
|
|
289
|
-
_PENDING_CONFIRM_CHECK: Optional[_PendingConfirmCheck] = None
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
def register_project_root_provider(fn: _ProjectRootProvider) -> None:
|
|
293
|
-
"""Install the provider used by the hook to locate the active project root.
|
|
294
|
-
|
|
295
|
-
`src/server.py` registers a provider during startup that consults its
|
|
296
|
-
`get_resolve()` helper + the analysis output-root resolver.
|
|
297
|
-
"""
|
|
298
|
-
global _PROVIDER
|
|
299
|
-
_PROVIDER = fn
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
def register_preference_provider(fn: _PreferenceProvider) -> None:
|
|
303
|
-
"""Install the provider used by the hook to read C6 preferences."""
|
|
304
|
-
global _PREFERENCE_PROVIDER
|
|
305
|
-
_PREFERENCE_PROVIDER = fn
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
def register_pending_confirm_check(fn: _PendingConfirmCheck) -> None:
|
|
309
|
-
"""Install the predicate used to detect token-issuance calls.
|
|
310
|
-
|
|
311
|
-
When the predicate returns True, the wrapper bypasses the pre-mutation
|
|
312
|
-
archive and the brain_edit row entirely — the underlying handler is still
|
|
313
|
-
invoked so it can mint and return the confirm_token, but no state has
|
|
314
|
-
changed yet. This avoids one wasted archive per token-issuance call (F4).
|
|
315
|
-
"""
|
|
316
|
-
global _PENDING_CONFIRM_CHECK
|
|
317
|
-
_PENDING_CONFIRM_CHECK = fn
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
def _read_preference(key: str, default: Any = None) -> Any:
|
|
321
|
-
if _PREFERENCE_PROVIDER is None:
|
|
322
|
-
return default
|
|
323
|
-
try:
|
|
324
|
-
value = _PREFERENCE_PROVIDER(key)
|
|
325
|
-
except Exception as exc:
|
|
326
|
-
logger.debug("preference provider for %s raised: %s", key, exc)
|
|
327
|
-
return default
|
|
328
|
-
return default if value is None else value
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
def _resolve_versioning_context() -> Optional[Tuple[Any, Any, str, Optional[str]]]:
|
|
332
|
-
if _PROVIDER is None:
|
|
333
|
-
return None
|
|
334
|
-
try:
|
|
335
|
-
return _PROVIDER()
|
|
336
|
-
except Exception as exc:
|
|
337
|
-
logger.debug("project-root provider raised: %s", exc)
|
|
338
|
-
return None
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
# ── Action analysis_run_id source ────────────────────────────────────────────
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
def _extract_analysis_run_id(params: Optional[Dict[str, Any]], project_root: Optional[str] = None) -> str:
|
|
345
|
-
"""Resolve the analysis_run_id for this call.
|
|
346
|
-
|
|
347
|
-
Order:
|
|
348
|
-
1. explicit param `analysis_run_id` or `run_id`
|
|
349
|
-
2. process-level current run (active begin_run/auto-run; idle timer applied)
|
|
350
|
-
3. open a fresh auto-run for this destructive op (B3)
|
|
351
|
-
|
|
352
|
-
The auto-run path (step 3) sets initiator='auto' and is closed by either
|
|
353
|
-
the next idle-timeout firing on a later destructive op, or an explicit
|
|
354
|
-
end_run call.
|
|
355
|
-
"""
|
|
356
|
-
if isinstance(params, dict):
|
|
357
|
-
rid = params.get("analysis_run_id") or params.get("run_id")
|
|
358
|
-
if rid:
|
|
359
|
-
return str(rid)
|
|
360
|
-
if project_root:
|
|
361
|
-
try:
|
|
362
|
-
timeout = float(_read_preference("versioning_auto_run_idle_timeout_seconds", default=90))
|
|
363
|
-
except Exception:
|
|
364
|
-
timeout = 90.0
|
|
365
|
-
try:
|
|
366
|
-
return analysis_runs.ensure_auto_run_for_destructive(
|
|
367
|
-
project_root=project_root, idle_timeout_seconds=timeout,
|
|
368
|
-
)
|
|
369
|
-
except Exception as exc:
|
|
370
|
-
logger.debug("auto-run open failed; falling back to ephemeral id: %s", exc)
|
|
371
|
-
active = analysis_runs.current_run_id()
|
|
372
|
-
if active:
|
|
373
|
-
analysis_runs.bump_idle_timer()
|
|
374
|
-
return active
|
|
375
|
-
return timeline_versioning.new_analysis_run_id()
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
def _extract_initiator(params: Optional[Dict[str, Any]]) -> Optional[str]:
|
|
379
|
-
"""Provenance label for the row. Explicit param > current run initiator > None."""
|
|
380
|
-
if isinstance(params, dict) and params.get("initiator"):
|
|
381
|
-
return str(params["initiator"])
|
|
382
|
-
return analysis_runs.current_run_initiator()
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
def _extract_metric(params: Optional[Dict[str, Any]]) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
|
386
|
-
if not isinstance(params, dict):
|
|
387
|
-
return (None, None, None)
|
|
388
|
-
return (
|
|
389
|
-
params.get("metric"),
|
|
390
|
-
params.get("direction"),
|
|
391
|
-
params.get("rationale"),
|
|
392
|
-
)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
# ── Decorator ────────────────────────────────────────────────────────────────
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
def destructive_op(tool_name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
399
|
-
"""Wrap a top-level tool function with the version-on-mutate hook.
|
|
400
|
-
|
|
401
|
-
The wrapped function must accept `action: str` as its first positional
|
|
402
|
-
argument and `params: Optional[Dict]` as its second (matches the existing
|
|
403
|
-
compound-tool signature).
|
|
404
|
-
"""
|
|
405
|
-
|
|
406
|
-
def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
407
|
-
@functools.wraps(fn)
|
|
408
|
-
def wrapper(action: str, params: Optional[Dict[str, Any]] = None, *args, **kwargs) -> Any:
|
|
409
|
-
if not is_destructive(tool_name, action, params):
|
|
410
|
-
return fn(action, params, *args, **kwargs)
|
|
411
|
-
|
|
412
|
-
# F4 — token-issuance calls don't mutate; skip the archive entirely
|
|
413
|
-
# so that token preview/cancel paths don't litter the version chain.
|
|
414
|
-
# The wrapper still annotates `_versioning` on the result so callers
|
|
415
|
-
# can see *why* nothing was archived.
|
|
416
|
-
if _PENDING_CONFIRM_CHECK is not None:
|
|
417
|
-
try:
|
|
418
|
-
will_gate = bool(_PENDING_CONFIRM_CHECK(tool_name, action, params))
|
|
419
|
-
except Exception as exc:
|
|
420
|
-
logger.debug("pending-confirm check raised: %s", exc)
|
|
421
|
-
will_gate = False
|
|
422
|
-
if will_gate:
|
|
423
|
-
result = fn(action, params, *args, **kwargs)
|
|
424
|
-
if isinstance(result, dict):
|
|
425
|
-
result.setdefault("_versioning", {
|
|
426
|
-
"analysis_run_id": None,
|
|
427
|
-
"archived": False,
|
|
428
|
-
"skipped_reason": "pending_confirm_token",
|
|
429
|
-
})
|
|
430
|
-
return result
|
|
431
|
-
|
|
432
|
-
strict = is_strict_required(tool_name, action, params)
|
|
433
|
-
|
|
434
|
-
ctx = _resolve_versioning_context()
|
|
435
|
-
if ctx is None:
|
|
436
|
-
if strict:
|
|
437
|
-
return {
|
|
438
|
-
"success": False,
|
|
439
|
-
"error": (
|
|
440
|
-
f"strict mode: '{tool_name}.{action}' refuses to run because the "
|
|
441
|
-
"version-on-mutate context (project_root) couldn't be resolved. "
|
|
442
|
-
"Open a project in Resolve, or pass strict=false to override."
|
|
443
|
-
),
|
|
444
|
-
}
|
|
445
|
-
# No Resolve / no project — let the underlying handler run; it
|
|
446
|
-
# will either succeed (e.g. in dry-run) or surface its own error.
|
|
447
|
-
return fn(action, params, *args, **kwargs)
|
|
448
|
-
|
|
449
|
-
resolve_h, project_h, project_root, project_name = ctx
|
|
450
|
-
run_id = _extract_analysis_run_id(params, project_root=project_root)
|
|
451
|
-
metric, direction, rationale = _extract_metric(params)
|
|
452
|
-
initiator = _extract_initiator(params)
|
|
453
|
-
|
|
454
|
-
# Media-pool destructive ops don't archive a timeline; they log to
|
|
455
|
-
# a separate provenance table. Branch here so the rest of the
|
|
456
|
-
# wrapper only handles the timeline path.
|
|
457
|
-
if tool_name == "media_pool":
|
|
458
|
-
result = fn(action, params, *args, **kwargs)
|
|
459
|
-
try:
|
|
460
|
-
media_pool_changes.log_media_pool_change(
|
|
461
|
-
project_root=project_root,
|
|
462
|
-
analysis_run_id=run_id,
|
|
463
|
-
action=action,
|
|
464
|
-
params=params if isinstance(params, dict) else None,
|
|
465
|
-
after_state=_summarise_result(result),
|
|
466
|
-
initiator=initiator,
|
|
467
|
-
)
|
|
468
|
-
except Exception as exc:
|
|
469
|
-
logger.warning("media_pool_change log failed: %s", exc)
|
|
470
|
-
if isinstance(result, dict):
|
|
471
|
-
result.setdefault("_versioning", {
|
|
472
|
-
"analysis_run_id": run_id,
|
|
473
|
-
"category": "media_pool",
|
|
474
|
-
"initiator": initiator,
|
|
475
|
-
})
|
|
476
|
-
return result
|
|
477
|
-
|
|
478
|
-
before_value: Optional[float] = None
|
|
479
|
-
timeline_before_name: Optional[str] = None
|
|
480
|
-
try:
|
|
481
|
-
current_tl = project_h.GetCurrentTimeline()
|
|
482
|
-
if current_tl is not None:
|
|
483
|
-
timeline_before_name = current_tl.GetName()
|
|
484
|
-
if metric:
|
|
485
|
-
before_value = brain_edits.capture_metric(metric, current_tl)
|
|
486
|
-
except Exception as exc:
|
|
487
|
-
logger.debug("pre-capture failed: %s", exc)
|
|
488
|
-
|
|
489
|
-
archive_result: Dict[str, Any] = {"archived": False}
|
|
490
|
-
archive_exc: Optional[Exception] = None
|
|
491
|
-
auto_save = bool(_read_preference(
|
|
492
|
-
"timeline_versioning_auto_save_after_archive", False,
|
|
493
|
-
))
|
|
494
|
-
try:
|
|
495
|
-
archive_result = timeline_versioning.ensure_versioned_before_mutation(
|
|
496
|
-
resolve=resolve_h,
|
|
497
|
-
project=project_h,
|
|
498
|
-
project_root=project_root,
|
|
499
|
-
analysis_run_id=run_id,
|
|
500
|
-
reason=f"{tool_name}.{action}",
|
|
501
|
-
initiator=initiator,
|
|
502
|
-
auto_save=auto_save,
|
|
503
|
-
)
|
|
504
|
-
except Exception as exc:
|
|
505
|
-
archive_exc = exc
|
|
506
|
-
logger.warning(
|
|
507
|
-
"version-on-mutate hook failed (%sstrict mode): %s",
|
|
508
|
-
"blocking under " if strict else "continuing past in non-",
|
|
509
|
-
exc,
|
|
510
|
-
)
|
|
511
|
-
|
|
512
|
-
# Strict mode: archive must have either landed OR been a legitimate
|
|
513
|
-
# no-op (already_archived_for_run | no_current_timeline). If the
|
|
514
|
-
# hook raised, OR returned archived=False with an unexpected reason,
|
|
515
|
-
# refuse the underlying call.
|
|
516
|
-
if strict:
|
|
517
|
-
skipped_ok = (
|
|
518
|
-
archive_result.get("archived") is False
|
|
519
|
-
and archive_result.get("skipped_reason") in {
|
|
520
|
-
"already_archived_for_run",
|
|
521
|
-
}
|
|
522
|
-
)
|
|
523
|
-
if archive_exc is not None or (
|
|
524
|
-
not archive_result.get("archived") and not skipped_ok
|
|
525
|
-
):
|
|
526
|
-
return {
|
|
527
|
-
"success": False,
|
|
528
|
-
"error": (
|
|
529
|
-
f"strict mode: refused '{tool_name}.{action}' because the "
|
|
530
|
-
"pre-mutation archive could not be created. "
|
|
531
|
-
f"reason={archive_result.get('skipped_reason') or 'archive_failed'} "
|
|
532
|
-
f"exception={type(archive_exc).__name__ if archive_exc else 'none'}"
|
|
533
|
-
),
|
|
534
|
-
"_versioning": {
|
|
535
|
-
"analysis_run_id": run_id,
|
|
536
|
-
"archived": False,
|
|
537
|
-
"strict_block": True,
|
|
538
|
-
},
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
# Run the underlying handler regardless of hook outcome.
|
|
542
|
-
result = fn(action, params, *args, **kwargs)
|
|
543
|
-
|
|
544
|
-
after_value: Optional[float] = None
|
|
545
|
-
timeline_after_name: Optional[str] = timeline_before_name
|
|
546
|
-
try:
|
|
547
|
-
current_tl = project_h.GetCurrentTimeline()
|
|
548
|
-
if current_tl is not None:
|
|
549
|
-
timeline_after_name = current_tl.GetName()
|
|
550
|
-
if metric:
|
|
551
|
-
after_value = brain_edits.capture_metric(metric, current_tl)
|
|
552
|
-
except Exception as exc:
|
|
553
|
-
logger.debug("post-capture failed: %s", exc)
|
|
554
|
-
|
|
555
|
-
try:
|
|
556
|
-
brain_edits.log_brain_edit(
|
|
557
|
-
project_root=project_root,
|
|
558
|
-
analysis_run_id=run_id,
|
|
559
|
-
edit_type=f"{tool_name}.{action}",
|
|
560
|
-
tool_name=tool_name,
|
|
561
|
-
action_name=action,
|
|
562
|
-
timeline_before=timeline_before_name,
|
|
563
|
-
timeline_after=timeline_after_name,
|
|
564
|
-
target_metric=metric,
|
|
565
|
-
metric_direction=direction,
|
|
566
|
-
before_value=before_value,
|
|
567
|
-
after_value=after_value,
|
|
568
|
-
rationale=rationale,
|
|
569
|
-
params=params if isinstance(params, dict) else None,
|
|
570
|
-
result_summary=_summarise_result(result),
|
|
571
|
-
project_name=project_name,
|
|
572
|
-
initiator=initiator,
|
|
573
|
-
)
|
|
574
|
-
except Exception as exc:
|
|
575
|
-
logger.warning("brain_edit log failed: %s", exc)
|
|
576
|
-
|
|
577
|
-
# Annotate the result with versioning context so the caller (and the
|
|
578
|
-
# control panel) can show what happened. Don't shadow existing keys.
|
|
579
|
-
if isinstance(result, dict):
|
|
580
|
-
result.setdefault("_versioning", {
|
|
581
|
-
"analysis_run_id": run_id,
|
|
582
|
-
"archived": archive_result.get("archived"),
|
|
583
|
-
"archived_version": archive_result.get("version"),
|
|
584
|
-
"metric": metric,
|
|
585
|
-
"before_value": before_value,
|
|
586
|
-
"after_value": after_value,
|
|
587
|
-
})
|
|
588
|
-
return result
|
|
589
|
-
|
|
590
|
-
wrapper.__wrapped_tool_name__ = tool_name # type: ignore[attr-defined]
|
|
591
|
-
wrapper.__is_destructive_wrapped__ = True # type: ignore[attr-defined]
|
|
592
|
-
return wrapper
|
|
593
|
-
|
|
594
|
-
return decorator
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
def _summarise_result(result: Any) -> Optional[Dict[str, Any]]:
|
|
598
|
-
"""Compact result summary suitable for the brain_edit row."""
|
|
599
|
-
if not isinstance(result, dict):
|
|
600
|
-
return None
|
|
601
|
-
keys = ("success", "error", "name", "count", "deleted", "inserted", "updated")
|
|
602
|
-
summary: Dict[str, Any] = {}
|
|
603
|
-
for key in keys:
|
|
604
|
-
if key in result:
|
|
605
|
-
summary[key] = result[key]
|
|
606
|
-
return summary or None
|