@hybridlabor-api/aos 4.4.0 → 4.4.2-beta.0

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.
Files changed (306) hide show
  1. package/.agents/skills/firecrawl/SKILL.md +149 -0
  2. package/.agents/skills/firecrawl/rules/install.md +82 -0
  3. package/.agents/skills/firecrawl/rules/security.md +26 -0
  4. package/.agents/skills/firecrawl-agent/SKILL.md +58 -0
  5. package/.agents/skills/firecrawl-build/SKILL.md +39 -0
  6. package/.agents/skills/firecrawl-build-interact/SKILL.md +68 -0
  7. package/.agents/skills/firecrawl-build-onboarding/SKILL.md +103 -0
  8. package/.agents/skills/firecrawl-build-onboarding/references/auth-flow.md +39 -0
  9. package/.agents/skills/firecrawl-build-onboarding/references/project-setup.md +20 -0
  10. package/.agents/skills/firecrawl-build-onboarding/references/sdk-installation.md +17 -0
  11. package/.agents/skills/firecrawl-build-scrape/SKILL.md +69 -0
  12. package/.agents/skills/firecrawl-build-search/SKILL.md +69 -0
  13. package/.agents/skills/firecrawl-crawl/SKILL.md +59 -0
  14. package/.agents/skills/firecrawl-download/SKILL.md +70 -0
  15. package/.agents/skills/firecrawl-interact/SKILL.md +84 -0
  16. package/.agents/skills/firecrawl-map/SKILL.md +51 -0
  17. package/.agents/skills/firecrawl-scrape/SKILL.md +69 -0
  18. package/.agents/skills/firecrawl-search/SKILL.md +60 -0
  19. package/.claude/hooks/memb-inject.mjs +6 -0
  20. package/CLAUDE.md +573 -0
  21. package/README.de.md +0 -8
  22. package/README.md +0 -8
  23. package/README.pt.md +0 -8
  24. package/assets/brand/ao-ant-mark.svg +91 -0
  25. package/assets/brand/bdb-core-mark.svg +34 -0
  26. package/assets/brand/heimdall-guardian-mark.svg +16 -0
  27. package/assets/brand/memb-crystal-mark.svg +18 -0
  28. package/bin/aos-config.mjs +89 -0
  29. package/bin/aos-dashboard.mjs +608 -0
  30. package/bin/aos-uninstall.mjs +227 -0
  31. package/docs/sessions/audit-2026-09-14-silent-failures.md +114 -0
  32. package/docs/skills_table.md +0 -3
  33. package/installer.js +428 -237
  34. package/mcps/RhinoMCP/cc-plugin/.claude/settings.json +10 -0
  35. package/mcps/after-effects-mcp/build/index.js +840 -0
  36. package/mcps/after-effects-mcp/build/scripts/applyEffect.jsx +153 -0
  37. package/mcps/after-effects-mcp/build/scripts/applyEffectTemplate.jsx +218 -0
  38. package/mcps/after-effects-mcp/build/scripts/createComposition.jsx +71 -0
  39. package/mcps/after-effects-mcp/build/scripts/createShapeLayer.jsx +147 -0
  40. package/mcps/after-effects-mcp/build/scripts/createSolidLayer.jsx +114 -0
  41. package/mcps/after-effects-mcp/build/scripts/createTextLayer.jsx +115 -0
  42. package/mcps/after-effects-mcp/build/scripts/getLayerInfo.jsx +192 -0
  43. package/mcps/after-effects-mcp/build/scripts/getProjectInfo.jsx +90 -0
  44. package/mcps/after-effects-mcp/build/scripts/listCompositions.jsx +50 -0
  45. package/mcps/after-effects-mcp/build/scripts/mcp-bridge-auto.jsx +1773 -0
  46. package/mcps/after-effects-mcp/build/scripts/setLayerProperties.jsx +160 -0
  47. package/mcps/computer-use-mcp/dist/client.d.ts +150 -0
  48. package/mcps/computer-use-mcp/dist/client.js +136 -0
  49. package/mcps/computer-use-mcp/dist/entrypoint.d.ts +16 -0
  50. package/mcps/computer-use-mcp/dist/entrypoint.js +26 -0
  51. package/mcps/computer-use-mcp/dist/native.d.ts +212 -0
  52. package/mcps/computer-use-mcp/dist/native.js +50 -0
  53. package/mcps/computer-use-mcp/dist/server.d.ts +32 -0
  54. package/mcps/computer-use-mcp/dist/server.js +342 -0
  55. package/mcps/computer-use-mcp/dist/session.d.ts +101 -0
  56. package/mcps/computer-use-mcp/dist/session.js +2372 -0
  57. package/package.json +13 -4
  58. package/skills/global_config/aos-project-init/SKILL.md +97 -130
  59. package/skills/global_config/aos-project-init/assets/openwikiignore.template +62 -0
  60. package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +38 -1
  61. package/skills/global_config/aos-setup/SKILL.md +110 -11
  62. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +48 -4
  63. package/skills/global_config/ask-tim/SKILL.md +2 -7
  64. package/skills/global_config/openwiki-skill/scripts/openwiki_daemon.py +65 -12
  65. package/bin/setup-saas.mjs +0 -792
  66. package/mcps/blender-mcp-server/.dockerignore +0 -19
  67. package/mcps/blender-mcp-server/.github/workflows/ci.yml +0 -67
  68. package/mcps/blender-mcp-server/.github/workflows/publish-pypi.yml +0 -54
  69. package/mcps/blender-mcp-server/CHANGELOG.md +0 -64
  70. package/mcps/blender-mcp-server/CONTRIBUTING.md +0 -107
  71. package/mcps/blender-mcp-server/Dockerfile +0 -19
  72. package/mcps/blender-mcp-server/LICENSE +0 -21
  73. package/mcps/blender-mcp-server/README.md +0 -409
  74. package/mcps/blender-mcp-server/addon/__init__.py +0 -1440
  75. package/mcps/blender-mcp-server/addon/models.py +0 -223
  76. package/mcps/blender-mcp-server/pyproject.toml +0 -120
  77. package/mcps/blender-mcp-server/scripts/blender_bridge_request.py +0 -71
  78. package/mcps/blender-mcp-server/scripts/blender_create_test_cube.py +0 -49
  79. package/mcps/blender-mcp-server/scripts/blender_scene_info.py +0 -38
  80. package/mcps/blender-mcp-server/scripts/build_addon_zip.sh +0 -29
  81. package/mcps/blender-mcp-server/scripts/demos/README.md +0 -186
  82. package/mcps/blender-mcp-server/scripts/demos/dam_break_scene.py +0 -276
  83. package/mcps/blender-mcp-server/scripts/demos/open_dam_break_gui.py +0 -33
  84. package/mcps/blender-mcp-server/scripts/demos/pipe_movement_study.py +0 -518
  85. package/mcps/blender-mcp-server/scripts/demos/pipe_pressure_failure_scene.py +0 -806
  86. package/mcps/blender-mcp-server/scripts/demos/procedural_dam_break_scene.py +0 -600
  87. package/mcps/blender-mcp-server/scripts/demos/run_dam_break.py +0 -510
  88. package/mcps/blender-mcp-server/scripts/launch_blender_gui.py +0 -41
  89. package/mcps/blender-mcp-server/scripts/library/README.md +0 -64
  90. package/mcps/blender-mcp-server/scripts/library/apply_transforms.py +0 -46
  91. package/mcps/blender-mcp-server/scripts/library/camera.py +0 -51
  92. package/mcps/blender-mcp-server/scripts/library/collections.py +0 -65
  93. package/mcps/blender-mcp-server/scripts/library/create_mesh.py +0 -41
  94. package/mcps/blender-mcp-server/scripts/library/effector.py +0 -48
  95. package/mcps/blender-mcp-server/scripts/library/fluid_domain.py +0 -80
  96. package/mcps/blender-mcp-server/scripts/library/fluid_inflow.py +0 -98
  97. package/mcps/blender-mcp-server/scripts/library/frame_range.py +0 -35
  98. package/mcps/blender-mcp-server/scripts/library/keyframes.py +0 -59
  99. package/mcps/blender-mcp-server/scripts/library/rigid_body.py +0 -52
  100. package/mcps/blender-mcp-server/scripts/library/save_blend.py +0 -40
  101. package/mcps/blender-mcp-server/scripts/server_start.sh +0 -26
  102. package/mcps/blender-mcp-server/src/blender_mcp_server/__init__.py +0 -3
  103. package/mcps/blender-mcp-server/src/blender_mcp_server/headless.py +0 -330
  104. package/mcps/blender-mcp-server/src/blender_mcp_server/server.py +0 -606
  105. package/mcps/davinci-mcp-professional/.bandit +0 -35
  106. package/mcps/davinci-mcp-professional/.claude/commands/security-review.md +0 -191
  107. package/mcps/davinci-mcp-professional/.claude/settings.json +0 -1
  108. package/mcps/davinci-mcp-professional/.editorconfig +0 -27
  109. package/mcps/davinci-mcp-professional/.gitattributes +0 -207
  110. package/mcps/davinci-mcp-professional/.github/CODEOWNERS +0 -2
  111. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  112. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  113. package/mcps/davinci-mcp-professional/.github/PULL_REQUEST_TEMPLATE.md +0 -20
  114. package/mcps/davinci-mcp-professional/.github/SECURITY.md +0 -97
  115. package/mcps/davinci-mcp-professional/.github/dependabot.yml +0 -27
  116. package/mcps/davinci-mcp-professional/.github/workflows/ci.yml +0 -39
  117. package/mcps/davinci-mcp-professional/ATTRIBUTION.md +0 -4
  118. package/mcps/davinci-mcp-professional/AUTHORS.md +0 -9
  119. package/mcps/davinci-mcp-professional/BUGS.md +0 -55
  120. package/mcps/davinci-mcp-professional/CLAUDE.md +0 -229
  121. package/mcps/davinci-mcp-professional/CONTRIBUTING.md +0 -51
  122. package/mcps/davinci-mcp-professional/COPYING.md +0 -228
  123. package/mcps/davinci-mcp-professional/Doxyfile +0 -379
  124. package/mcps/davinci-mcp-professional/README.md +0 -143
  125. package/mcps/davinci-mcp-professional/SECURITY.md +0 -40
  126. package/mcps/davinci-mcp-professional/USING.md +0 -301
  127. package/mcps/davinci-mcp-professional/VERSION.md +0 -13
  128. package/mcps/davinci-mcp-professional/build.py +0 -169
  129. package/mcps/davinci-mcp-professional/claude_desktop_config_template.json +0 -11
  130. package/mcps/davinci-mcp-professional/hooks/ci-check.sh +0 -29
  131. package/mcps/davinci-mcp-professional/hooks/pre-push +0 -46
  132. package/mcps/davinci-mcp-professional/main.py +0 -40
  133. package/mcps/davinci-mcp-professional/mcp_server.py +0 -30
  134. package/mcps/davinci-mcp-professional/pyproject.toml +0 -115
  135. package/mcps/davinci-mcp-professional/security_audit.py +0 -379
  136. package/mcps/davinci-mcp-professional/src/davinci_mcp/__init__.py +0 -19
  137. package/mcps/davinci-mcp-professional/src/davinci_mcp/cli.py +0 -138
  138. package/mcps/davinci-mcp-professional/src/davinci_mcp/resolve_client.py +0 -342
  139. package/mcps/davinci-mcp-professional/src/davinci_mcp/resources/__init__.py +0 -60
  140. package/mcps/davinci-mcp-professional/src/davinci_mcp/server.py +0 -185
  141. package/mcps/davinci-mcp-professional/src/davinci_mcp/tools/__init__.py +0 -146
  142. package/mcps/davinci-mcp-professional/src/davinci_mcp/types.py +0 -125
  143. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/__init__.py +0 -19
  144. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/platform.py +0 -239
  145. package/mcps/davinci-mcp-professional/test.py +0 -186
  146. package/mcps/davinci-mcp-professional/uv.lock +0 -2073
  147. package/mcps/davinci-resolve-mcp/.claude/skills/media-analysis.md +0 -44
  148. package/mcps/davinci-resolve-mcp/.clinerules +0 -17
  149. package/mcps/davinci-resolve-mcp/.cursorrules +0 -17
  150. package/mcps/davinci-resolve-mcp/.github/copilot-instructions.md +0 -17
  151. package/mcps/davinci-resolve-mcp/.github/workflows/npm-publish.yml +0 -109
  152. package/mcps/davinci-resolve-mcp/.windsurfrules +0 -17
  153. package/mcps/davinci-resolve-mcp/AGENTS.md +0 -114
  154. package/mcps/davinci-resolve-mcp/CHANGELOG.md +0 -2648
  155. package/mcps/davinci-resolve-mcp/CLAUDE.md +0 -15
  156. package/mcps/davinci-resolve-mcp/LICENSE +0 -21
  157. package/mcps/davinci-resolve-mcp/README.md +0 -173
  158. package/mcps/davinci-resolve-mcp/SECURITY.md +0 -53
  159. package/mcps/davinci-resolve-mcp/bin/davinci-resolve-mcp.mjs +0 -450
  160. package/mcps/davinci-resolve-mcp/examples/README.md +0 -53
  161. package/mcps/davinci-resolve-mcp/examples/markers/README.md +0 -81
  162. package/mcps/davinci-resolve-mcp/examples/media/README.md +0 -94
  163. package/mcps/davinci-resolve-mcp/examples/timeline/README.md +0 -98
  164. package/mcps/davinci-resolve-mcp/install.py +0 -1687
  165. package/mcps/davinci-resolve-mcp/package.json +0 -52
  166. package/mcps/davinci-resolve-mcp/release-notes/v2.22.0.md +0 -51
  167. package/mcps/davinci-resolve-mcp/release-notes/v2.23.0.md +0 -51
  168. package/mcps/davinci-resolve-mcp/release-notes/v2.23.1.md +0 -35
  169. package/mcps/davinci-resolve-mcp/release-notes/v2.24.1.md +0 -32
  170. package/mcps/davinci-resolve-mcp/scripts/audit_api_parity.py +0 -275
  171. package/mcps/davinci-resolve-mcp/scripts/audit_readwrite_symmetry.py +0 -80
  172. package/mcps/davinci-resolve-mcp/scripts/doctor.py +0 -227
  173. package/mcps/davinci-resolve-mcp/scripts/gen_api_limitations.py +0 -144
  174. package/mcps/davinci-resolve-mcp/scripts/live_media_analysis_polish_probe.py +0 -65
  175. package/mcps/davinci-resolve-mcp/scripts/measure_bridge_cost.py +0 -65
  176. package/mcps/davinci-resolve-mcp/scripts/regen_panel_screenshots.py +0 -76
  177. package/mcps/davinci-resolve-mcp/src/__init__.py +0 -3
  178. package/mcps/davinci-resolve-mcp/src/analysis_dashboard.py +0 -15505
  179. package/mcps/davinci-resolve-mcp/src/batch_cli.py +0 -540
  180. package/mcps/davinci-resolve-mcp/src/control_panel.py +0 -13
  181. package/mcps/davinci-resolve-mcp/src/granular/__init__.py +0 -17
  182. package/mcps/davinci-resolve-mcp/src/granular/common.py +0 -741
  183. package/mcps/davinci-resolve-mcp/src/granular/folder.py +0 -420
  184. package/mcps/davinci-resolve-mcp/src/granular/gallery.py +0 -306
  185. package/mcps/davinci-resolve-mcp/src/granular/graph.py +0 -309
  186. package/mcps/davinci-resolve-mcp/src/granular/media_pool.py +0 -679
  187. package/mcps/davinci-resolve-mcp/src/granular/media_pool_item.py +0 -1020
  188. package/mcps/davinci-resolve-mcp/src/granular/media_storage.py +0 -179
  189. package/mcps/davinci-resolve-mcp/src/granular/project.py +0 -1654
  190. package/mcps/davinci-resolve-mcp/src/granular/resolve_control.py +0 -538
  191. package/mcps/davinci-resolve-mcp/src/granular/timeline.py +0 -1075
  192. package/mcps/davinci-resolve-mcp/src/granular/timeline_item.py +0 -2251
  193. package/mcps/davinci-resolve-mcp/src/resolve_mcp_server.py +0 -43
  194. package/mcps/davinci-resolve-mcp/src/server.py +0 -24116
  195. package/mcps/davinci-resolve-mcp/src/utils/__init__.py +0 -3
  196. package/mcps/davinci-resolve-mcp/src/utils/actor_identity.py +0 -46
  197. package/mcps/davinci-resolve-mcp/src/utils/analysis_caps.py +0 -678
  198. package/mcps/davinci-resolve-mcp/src/utils/analysis_memory.py +0 -755
  199. package/mcps/davinci-resolve-mcp/src/utils/analysis_runs.py +0 -307
  200. package/mcps/davinci-resolve-mcp/src/utils/analysis_store.py +0 -945
  201. package/mcps/davinci-resolve-mcp/src/utils/api_truth.py +0 -525
  202. package/mcps/davinci-resolve-mcp/src/utils/app_control.py +0 -319
  203. package/mcps/davinci-resolve-mcp/src/utils/audio_fairlight_live_probe.py +0 -263
  204. package/mcps/davinci-resolve-mcp/src/utils/brain_edits.py +0 -387
  205. package/mcps/davinci-resolve-mcp/src/utils/bridge_metrics.py +0 -76
  206. package/mcps/davinci-resolve-mcp/src/utils/cdl.py +0 -20
  207. package/mcps/davinci-resolve-mcp/src/utils/clip_query.py +0 -85
  208. package/mcps/davinci-resolve-mcp/src/utils/cloud_operations.py +0 -192
  209. package/mcps/davinci-resolve-mcp/src/utils/color_grade_live_probe.py +0 -444
  210. package/mcps/davinci-resolve-mcp/src/utils/contracts.py +0 -102
  211. package/mcps/davinci-resolve-mcp/src/utils/cut_ir.py +0 -115
  212. package/mcps/davinci-resolve-mcp/src/utils/dctl_templates.py +0 -368
  213. package/mcps/davinci-resolve-mcp/src/utils/deep_vision.py +0 -643
  214. package/mcps/davinci-resolve-mcp/src/utils/destructive_hook.py +0 -606
  215. package/mcps/davinci-resolve-mcp/src/utils/edit_engine.py +0 -737
  216. package/mcps/davinci-resolve-mcp/src/utils/embeddings.py +0 -922
  217. package/mcps/davinci-resolve-mcp/src/utils/entities.py +0 -579
  218. package/mcps/davinci-resolve-mcp/src/utils/extension_authoring_live_probe.py +0 -292
  219. package/mcps/davinci-resolve-mcp/src/utils/failure_tracker.py +0 -119
  220. package/mcps/davinci-resolve-mcp/src/utils/fuse_templates.py +0 -1968
  221. package/mcps/davinci-resolve-mcp/src/utils/fusion_composition_live_probe.py +0 -284
  222. package/mcps/davinci-resolve-mcp/src/utils/fusion_group_settings.py +0 -334
  223. package/mcps/davinci-resolve-mcp/src/utils/layout_presets.py +0 -333
  224. package/mcps/davinci-resolve-mcp/src/utils/mcp_stdio.py +0 -32
  225. package/mcps/davinci-resolve-mcp/src/utils/mcp_transport.py +0 -128
  226. package/mcps/davinci-resolve-mcp/src/utils/media_analysis.py +0 -7690
  227. package/mcps/davinci-resolve-mcp/src/utils/media_analysis_jobs.py +0 -805
  228. package/mcps/davinci-resolve-mcp/src/utils/media_pool_changes.py +0 -121
  229. package/mcps/davinci-resolve-mcp/src/utils/media_pool_ingest_live_probe.py +0 -592
  230. package/mcps/davinci-resolve-mcp/src/utils/multicam.py +0 -393
  231. package/mcps/davinci-resolve-mcp/src/utils/object_inspection.py +0 -301
  232. package/mcps/davinci-resolve-mcp/src/utils/page_lock.py +0 -75
  233. package/mcps/davinci-resolve-mcp/src/utils/platform.py +0 -157
  234. package/mcps/davinci-resolve-mcp/src/utils/proc.py +0 -26
  235. package/mcps/davinci-resolve-mcp/src/utils/project_cleanup.py +0 -73
  236. package/mcps/davinci-resolve-mcp/src/utils/project_lifecycle_live_probe.py +0 -376
  237. package/mcps/davinci-resolve-mcp/src/utils/project_lint.py +0 -131
  238. package/mcps/davinci-resolve-mcp/src/utils/project_properties.py +0 -601
  239. package/mcps/davinci-resolve-mcp/src/utils/project_spec.py +0 -459
  240. package/mcps/davinci-resolve-mcp/src/utils/readback.py +0 -91
  241. package/mcps/davinci-resolve-mcp/src/utils/render_deliver_live_probe.py +0 -384
  242. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_governance.py +0 -214
  243. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_ledger.py +0 -244
  244. package/mcps/davinci-resolve-mcp/src/utils/resolve_busy.py +0 -171
  245. package/mcps/davinci-resolve-mcp/src/utils/resolve_connection.py +0 -77
  246. package/mcps/davinci-resolve-mcp/src/utils/review_annotation_live_probe.py +0 -352
  247. package/mcps/davinci-resolve-mcp/src/utils/script_templates.py +0 -1193
  248. package/mcps/davinci-resolve-mcp/src/utils/shot_relationships.py +0 -544
  249. package/mcps/davinci-resolve-mcp/src/utils/structural_diff.py +0 -175
  250. package/mcps/davinci-resolve-mcp/src/utils/sync_detection.py +0 -889
  251. package/mcps/davinci-resolve-mcp/src/utils/timeline_brain_db.py +0 -826
  252. package/mcps/davinci-resolve-mcp/src/utils/timeline_conform_live_probe.py +0 -280
  253. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_live_probe.py +0 -1092
  254. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_probe.py +0 -185
  255. package/mcps/davinci-resolve-mcp/src/utils/timeline_title_text.py +0 -87
  256. package/mcps/davinci-resolve-mcp/src/utils/timeline_versioning.py +0 -730
  257. package/mcps/davinci-resolve-mcp/src/utils/update_check.py +0 -685
  258. package/mcps/vectorworks-mcp/.dockerignore +0 -8
  259. package/mcps/vectorworks-mcp/Dockerfile +0 -28
  260. package/mcps/vectorworks-mcp/README.md +0 -156
  261. package/mcps/vectorworks-mcp/app/__init__.py +0 -2
  262. package/mcps/vectorworks-mcp/app/api.py +0 -52
  263. package/mcps/vectorworks-mcp/app/chunking.py +0 -56
  264. package/mcps/vectorworks-mcp/app/config.py +0 -29
  265. package/mcps/vectorworks-mcp/app/embeddings.py +0 -28
  266. package/mcps/vectorworks-mcp/app/indexer.py +0 -106
  267. package/mcps/vectorworks-mcp/app/main.py +0 -33
  268. package/mcps/vectorworks-mcp/app/mcp_server.py +0 -71
  269. package/mcps/vectorworks-mcp/app/search.py +0 -54
  270. package/mcps/vectorworks-mcp/app/utils.py +0 -14
  271. package/mcps/vectorworks-mcp/data/.gitkeep +0 -1
  272. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_PluginScript.htm +0 -95
  273. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_ScriptEditor.htm +0 -266
  274. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_Scripting.htm +0 -106
  275. package/mcps/vectorworks-mcp/data/vw-app-help/VW2023_CreatingScriptedPlugins.htm +0 -327
  276. package/mcps/vectorworks-mcp/data/vw-app-help/VW2024_RunningScripts.htm +0 -116
  277. package/mcps/vectorworks-mcp/data/vw-devwiki/VS_Function_Reference_category.html +0 -2129
  278. package/mcps/vectorworks-mcp/data/vw-jp-ref/ObjectsSymbols.html +0 -2035
  279. package/mcps/vectorworks-mcp/data/vw-jp-ref/ScriptFunctionReference.html +0 -29
  280. package/mcps/vectorworks-mcp/design.md +0 -57
  281. package/mcps/vectorworks-mcp/docker-compose.yml +0 -18
  282. package/mcps/vectorworks-mcp/index/.gitkeep +0 -1
  283. package/mcps/vectorworks-mcp/index/meta.jsonl +0 -3956
  284. package/mcps/vectorworks-mcp/index/vw.faiss +0 -0
  285. package/mcps/vectorworks-mcp/requirements.txt +0 -10
  286. package/mcps/vectorworks-mcp/scripts/fetch_docs_minimal.sh +0 -84
  287. package/mcps/vectorworks-mcp/scripts/fetch_github_vectorworks.sh +0 -85
  288. package/mcps/vectorworks-mcp/templates/index.html +0 -40
  289. package/scripts/ecosystem-health-audit.js +0 -257
  290. package/skills/bdb-dev-os-skill/SKILL.md +0 -49
  291. package/skills/bdb-dev-os-skill/package.json +0 -11
  292. package/skills/bdbsaastraining/SKILL.md +0 -316
  293. package/skills/bdbsaastraining/references/exam-pool.md +0 -313
  294. package/skills/bdbsaastraining/references/interaction-modes.md +0 -94
  295. package/skills/bdbsaastraining/references/track-a-ai-agent.md +0 -111
  296. package/skills/bdbsaastraining/references/track-b-wordpress.md +0 -111
  297. package/skills/bdbsaastraining/references/track-c-mailserver.md +0 -107
  298. package/skills/bdbsaastraining/references/track-d-debian.md +0 -109
  299. package/skills/bdbsaastraining/references/track-e-custom.md +0 -83
  300. package/skills/bdbsaastraining/scripts/build_profile.py +0 -529
  301. package/skills/bdbsaastraining/scripts/generate_certificate.py +0 -161
  302. package/skills/bdbsaastraining/scripts/preflight_check.sh +0 -150
  303. package/skills/bdbsaastraining/templates/certificate_template.html +0 -508
  304. package/skills/global_config/bdb-ecosystem-health/SKILL.md +0 -68
  305. package/skills/global_config/bdbsaas-ops/SKILL.md +0 -16
  306. package/skills/global_config/bdbsaashost/SKILL.md +0 -125
@@ -1,945 +0,0 @@
1
- """DB-canonical clip-analysis store (C1 / Phase A of the analysis program).
2
-
3
- The per-project SQLite DB (timeline_brain.sqlite, schema v9+) is the source of
4
- truth for clip analysis. analysis.json on disk is a derived export written in
5
- lockstep after every ingest. Shape is hybrid:
6
-
7
- - ``analysis_reports.report_json`` — the canonical full payload per clip.
8
- - Normalized tables for what downstream phases query: ``clips`` (identity +
9
- headline columns), ``clip_aliases`` (any stable id → clip_uuid), ``shots``,
10
- ``subjective_fields``/``field_changelog`` (per-field provenance),
11
- ``transcript_segments``, ``frames``, ``qc_observations``.
12
-
13
- Export contract: export = report blob + current *human* subjective fields
14
- overlaid. Machine values are already inside the blob because the blob is
15
- rewritten on every ingest; human rows always win and survive re-analysis.
16
-
17
- Reader contract: readers go DB-first and fall back to analysis.json when the
18
- clip has no rows (reports written before v9, or job-linked report dirs that
19
- live under another project root).
20
-
21
- This module must not import media_analysis at module level (media_analysis
22
- calls into here from the analysis write path) — helpers are imported lazily.
23
- """
24
-
25
- from __future__ import annotations
26
-
27
- import json
28
- import logging
29
- import os
30
- import sqlite3
31
- import time
32
- from typing import Any, Dict, List, Optional, Tuple
33
-
34
- from src.utils import timeline_brain_db
35
-
36
- logger = logging.getLogger("resolve-mcp.analysis-store")
37
-
38
- # Source label for machine-derived subjective rows written at ingest time.
39
- MACHINE_SOURCE = "vision_v0.2"
40
- HUMAN_SOURCE = "human"
41
-
42
- # Clip-level subjective groups inside report["visual"] that get flattened into
43
- # subjective_fields rows. Structural/computed visual keys stay blob-only.
44
- _SUBJECTIVE_CLIP_GROUPS = (
45
- "clip_summary",
46
- "clip_summary_oneliner",
47
- "editorial_classification",
48
- "content",
49
- "shot_and_style",
50
- "slate",
51
- "editing_notes",
52
- )
53
-
54
- # Shot keys that are structural (identity/geometry), not subjective fields.
55
- _SHOT_STRUCTURAL_KEYS = {
56
- "shot_index",
57
- "shot_uuid",
58
- "time_seconds_start",
59
- "time_seconds_end",
60
- "frame_indices_used",
61
- "frame_indices",
62
- "qc_flags",
63
- }
64
-
65
-
66
- def _now() -> str:
67
- return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
68
-
69
-
70
- def _dumps(value: Any) -> str:
71
- return json.dumps(value, sort_keys=True, default=str)
72
-
73
-
74
- def _ma():
75
- """Lazy import of media_analysis helpers (avoids a circular import)."""
76
- from src.utils import media_analysis
77
-
78
- return media_analysis
79
-
80
-
81
- def shot_uuid_for(clip_uuid: str, start: Any, end: Any) -> str:
82
- """Stable shot id: clip + time region rounded to the nearest second.
83
-
84
- Survives small boundary jitter on re-analysis so corrections and timeline
85
- references keep pointing at the same shot.
86
- """
87
- try:
88
- start_r = int(round(float(start)))
89
- except (TypeError, ValueError):
90
- start_r = -1
91
- try:
92
- end_r = int(round(float(end)))
93
- except (TypeError, ValueError):
94
- end_r = -1
95
- return _ma().short_hash(f"shot:{clip_uuid}:{start_r}:{end_r}", 12)
96
-
97
-
98
- def _flatten_fields(value: Any, prefix: str, out: Dict[str, Any]) -> None:
99
- """Flatten nested dicts to dot-path leaves; lists/scalars are leaves."""
100
- if isinstance(value, dict):
101
- for key, child in value.items():
102
- child_prefix = f"{prefix}.{key}" if prefix else str(key)
103
- _flatten_fields(child, child_prefix, out)
104
- else:
105
- if prefix:
106
- out[prefix] = value
107
-
108
-
109
- def clip_subjective_fields(visual: Dict[str, Any]) -> Dict[str, Any]:
110
- """Flatten the clip-level subjective groups of a visual block."""
111
- out: Dict[str, Any] = {}
112
- if not isinstance(visual, dict):
113
- return out
114
- for group in _SUBJECTIVE_CLIP_GROUPS:
115
- if group in visual:
116
- _flatten_fields(visual.get(group), group, out)
117
- return out
118
-
119
-
120
- def shot_subjective_fields(shot_entry: Dict[str, Any]) -> Dict[str, Any]:
121
- """Flatten a shot entry's subjective fields (everything non-structural)."""
122
- out: Dict[str, Any] = {}
123
- if not isinstance(shot_entry, dict):
124
- return out
125
- for key, value in shot_entry.items():
126
- if key in _SHOT_STRUCTURAL_KEYS:
127
- continue
128
- _flatten_fields(value, str(key), out)
129
- return out
130
-
131
-
132
- # ── identity ──────────────────────────────────────────────────────────────────
133
-
134
-
135
- def clip_identity(report: Dict[str, Any], *, clip_dir: Optional[str] = None) -> Tuple[str, List[Tuple[str, str]]]:
136
- """Return (clip_uuid, [(alias, kind), ...]) for a report.
137
-
138
- clip_uuid is the canonical rename-stable hash (normalized file_path basis);
139
- aliases cover legacy hashes, clip_id, media_id, raw/normalized file path,
140
- and the report folder's name + embedded hash.
141
- """
142
- ma = _ma()
143
- record = report.get("clip") if isinstance(report.get("clip"), dict) else {}
144
- hashes = ma.stable_clip_match_hashes(record)
145
- aliases: List[Tuple[str, str]] = []
146
- folder_hash = None
147
- folder_name = os.path.basename(str(clip_dir).rstrip("/\\")) if clip_dir else None
148
- if folder_name:
149
- folder_hash = ma.clip_directory_hash(folder_name)
150
- clip_uuid = hashes[0] if hashes else (folder_hash or ma.short_hash(folder_name or "clip", 12))
151
- for h in hashes:
152
- aliases.append((h, "hash"))
153
- if folder_hash:
154
- aliases.append((folder_hash, "hash"))
155
- if folder_name:
156
- aliases.append((folder_name, "clip_dir"))
157
- for key, kind in (("clip_id", "clip_id"), ("media_id", "media_id")):
158
- value = record.get(key)
159
- if value:
160
- aliases.append((str(value), kind))
161
- file_path = record.get("file_path")
162
- if file_path:
163
- aliases.append((ma.normalize_path(file_path), "file_path"))
164
- aliases.append((str(file_path), "file_path"))
165
- deduped: List[Tuple[str, str]] = []
166
- seen = set()
167
- for alias, kind in aliases:
168
- if alias and alias not in seen:
169
- seen.add(alias)
170
- deduped.append((alias, kind))
171
- return clip_uuid, deduped
172
-
173
-
174
- def resolve_clip_uuid(conn: sqlite3.Connection, ref: Any) -> Optional[str]:
175
- """Resolve any clip reference (uuid, hash, clip_id, path, folder) to clip_uuid."""
176
- if not ref:
177
- return None
178
- candidate = str(ref)
179
- row = conn.execute("SELECT clip_uuid FROM clips WHERE clip_uuid = ?", (candidate,)).fetchone()
180
- if row:
181
- return str(row["clip_uuid"])
182
- row = conn.execute("SELECT clip_uuid FROM clip_aliases WHERE alias = ?", (candidate,)).fetchone()
183
- if row:
184
- return str(row["clip_uuid"])
185
- # Folder names carry the hash as a suffix; absolute paths reduce to basename.
186
- base = os.path.basename(candidate.rstrip("/\\"))
187
- if base != candidate:
188
- for probe in (base, _ma().clip_directory_hash(base) or ""):
189
- if not probe:
190
- continue
191
- row = conn.execute("SELECT clip_uuid FROM clip_aliases WHERE alias = ?", (probe,)).fetchone()
192
- if row:
193
- return str(row["clip_uuid"])
194
- else:
195
- probe = _ma().clip_directory_hash(base)
196
- if probe:
197
- row = conn.execute("SELECT clip_uuid FROM clip_aliases WHERE alias = ?", (probe,)).fetchone()
198
- if row:
199
- return str(row["clip_uuid"])
200
- return None
201
-
202
-
203
- # ── ingest ────────────────────────────────────────────────────────────────────
204
-
205
-
206
- def _duration_seconds(report: Dict[str, Any]) -> Optional[float]:
207
- for path in (
208
- ("clip_analysis_markers", "duration_seconds"),
209
- ("cut_analysis", "duration_seconds"),
210
- ):
211
- node: Any = report
212
- for key in path:
213
- node = node.get(key) if isinstance(node, dict) else None
214
- if isinstance(node, (int, float)):
215
- return float(node)
216
- technical = report.get("technical") if isinstance(report.get("technical"), dict) else {}
217
- fmt = technical.get("format") if isinstance(technical.get("format"), dict) else {}
218
- if isinstance(fmt.get("duration_seconds"), (int, float)):
219
- return float(fmt["duration_seconds"])
220
- return None
221
-
222
-
223
- def _current_subjective(conn: sqlite3.Connection, entity_type: str, entity_uuid: str) -> Dict[str, sqlite3.Row]:
224
- rows = conn.execute(
225
- """
226
- SELECT * FROM subjective_fields
227
- WHERE entity_type = ? AND entity_uuid = ? AND superseded_at IS NULL
228
- """,
229
- (entity_type, entity_uuid),
230
- ).fetchall()
231
- return {str(r["field_path"]): r for r in rows}
232
-
233
-
234
- def _write_subjective(
235
- conn: sqlite3.Connection,
236
- entity_type: str,
237
- entity_uuid: str,
238
- fields: Dict[str, Any],
239
- *,
240
- source: str,
241
- author: str,
242
- confidence: Optional[str] = None,
243
- reason: Optional[str] = None,
244
- overwrite_human: bool = False,
245
- ) -> Dict[str, int]:
246
- """Upsert current values for an entity. Machine writes never replace a
247
- current human row unless overwrite_human is set (explicit revert path)."""
248
- now = _now()
249
- current = _current_subjective(conn, entity_type, entity_uuid)
250
- written = 0
251
- skipped_human = 0
252
- for field_path, value in fields.items():
253
- value_json = _dumps(value)
254
- existing = current.get(field_path)
255
- if existing is not None:
256
- # Identical value → keep the existing row and its provenance. A
257
- # different source re-deriving the same value must not re-attribute
258
- # it (a deep pass would otherwise claim every unchanged field).
259
- if str(existing["value_json"]) == value_json:
260
- continue
261
- if (
262
- str(existing["source"]) == HUMAN_SOURCE
263
- and source != HUMAN_SOURCE
264
- and not overwrite_human
265
- ):
266
- skipped_human += 1
267
- continue
268
- conn.execute(
269
- "UPDATE subjective_fields SET superseded_at = ? WHERE id = ?",
270
- (now, existing["id"]),
271
- )
272
- conn.execute(
273
- """
274
- INSERT INTO subjective_fields
275
- (entity_type, entity_uuid, field_path, value_json, confidence,
276
- source, author, timestamp)
277
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
278
- """,
279
- (entity_type, entity_uuid, field_path, value_json, confidence, source, author, now),
280
- )
281
- conn.execute(
282
- """
283
- INSERT INTO field_changelog
284
- (entity_type, entity_uuid, field_path, previous_value_json,
285
- new_value_json, previous_source, new_source, previous_author,
286
- new_author, change_reason, timestamp)
287
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
288
- """,
289
- (
290
- entity_type,
291
- entity_uuid,
292
- field_path,
293
- existing["value_json"] if existing is not None else None,
294
- value_json,
295
- existing["source"] if existing is not None else None,
296
- source,
297
- existing["author"] if existing is not None else None,
298
- author,
299
- reason,
300
- now,
301
- ),
302
- )
303
- written += 1
304
- return {"written": written, "skipped_human": skipped_human}
305
-
306
-
307
- def ingest_report(
308
- project_root: str,
309
- report: Dict[str, Any],
310
- *,
311
- clip_dir: Optional[str] = None,
312
- author: str = "system",
313
- source: str = MACHINE_SOURCE,
314
- ) -> Dict[str, Any]:
315
- """Write one analysis report into the DB (rows + canonical blob).
316
-
317
- Idempotent: re-ingesting the same report leaves identical state. Human
318
- subjective rows are never replaced by machine values.
319
- """
320
- if not isinstance(report, dict):
321
- return {"success": False, "error": "report must be a dict"}
322
- clip_uuid, aliases = clip_identity(report, clip_dir=clip_dir)
323
- record = report.get("clip") if isinstance(report.get("clip"), dict) else {}
324
- visual = report.get("visual") if isinstance(report.get("visual"), dict) else {}
325
- shot_entries = visual.get("shot_descriptions") if isinstance(visual.get("shot_descriptions"), list) else []
326
- motion = report.get("motion") if isinstance(report.get("motion"), dict) else {}
327
- cut_analysis = report.get("cut_analysis") if isinstance(report.get("cut_analysis"), dict) else {}
328
- transcription = report.get("transcription") if isinstance(report.get("transcription"), dict) else {}
329
- signature = report.get("analysis_signature") if isinstance(report.get("analysis_signature"), dict) else {}
330
- profile = report.get("analysis_profile") if isinstance(report.get("analysis_profile"), dict) else {}
331
- now = _now()
332
-
333
- with timeline_brain_db.transaction(project_root) as conn:
334
- existing = conn.execute(
335
- "SELECT created_at FROM clips WHERE clip_uuid = ?", (clip_uuid,)
336
- ).fetchone()
337
- created_at = str(existing["created_at"]) if existing else now
338
- conn.execute(
339
- """
340
- INSERT OR REPLACE INTO clips
341
- (clip_uuid, clip_dir, resolve_clip_id, media_id, clip_name,
342
- file_path, bin_path, duration_seconds, fps, resolution,
343
- media_type, summary, overall_motion_level, cut_count,
344
- shot_count, analysis_version, depth, signature_hash,
345
- analyzed_at, vision_status, vision_committed_at,
346
- created_at, updated_at)
347
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
348
- """,
349
- (
350
- clip_uuid,
351
- os.path.basename(str(clip_dir).rstrip("/\\")) if clip_dir else None,
352
- record.get("clip_id"),
353
- record.get("media_id"),
354
- record.get("clip_name"),
355
- record.get("file_path"),
356
- record.get("bin_path"),
357
- _duration_seconds(report),
358
- record.get("fps") if isinstance(record.get("fps"), (int, float)) else None,
359
- record.get("resolution"),
360
- record.get("media_type"),
361
- report.get("summary"),
362
- motion.get("overall_motion_level"),
363
- cut_analysis.get("cut_count") if isinstance(cut_analysis.get("cut_count"), int) else None,
364
- len(shot_entries),
365
- report.get("analysis_version"),
366
- profile.get("depth"),
367
- signature.get("signature_hash"),
368
- report.get("analyzed_at"),
369
- report.get("vision_status"),
370
- report.get("vision_committed_at"),
371
- created_at,
372
- now,
373
- ),
374
- )
375
- for alias, kind in aliases:
376
- conn.execute(
377
- "INSERT OR IGNORE INTO clip_aliases (alias, clip_uuid, kind) VALUES (?, ?, ?)",
378
- (alias, clip_uuid, kind),
379
- )
380
- conn.execute(
381
- """
382
- INSERT OR REPLACE INTO analysis_reports
383
- (clip_uuid, report_json, signature_hash, analyzed_at, written_at)
384
- VALUES (?, ?, ?, ?, ?)
385
- """,
386
- (clip_uuid, _dumps(report), signature.get("signature_hash"), report.get("analyzed_at"), now),
387
- )
388
-
389
- # Shots: rebuild rows, preserving created_at for stable shot_uuids.
390
- prior_shots = {
391
- str(r["shot_uuid"]): r
392
- for r in conn.execute("SELECT * FROM shots WHERE clip_uuid = ?", (clip_uuid,)).fetchall()
393
- }
394
- conn.execute("DELETE FROM shots WHERE clip_uuid = ?", (clip_uuid,))
395
- frame_to_shot: Dict[int, str] = {}
396
- subj_written = 0
397
- subj_skipped_human = 0
398
- for entry in shot_entries:
399
- if not isinstance(entry, dict):
400
- continue
401
- start = entry.get("time_seconds_start")
402
- end = entry.get("time_seconds_end")
403
- s_uuid = shot_uuid_for(clip_uuid, start, end)
404
- prior = prior_shots.get(s_uuid)
405
- frame_indices = entry.get("frame_indices_used") or entry.get("frame_indices") or []
406
- if not isinstance(frame_indices, list):
407
- frame_indices = []
408
- extra = {
409
- k: v
410
- for k, v in entry.items()
411
- if k not in _SHOT_STRUCTURAL_KEYS and k != "description"
412
- }
413
- conn.execute(
414
- """
415
- INSERT OR REPLACE INTO shots
416
- (shot_uuid, clip_uuid, shot_index, time_seconds_start,
417
- time_seconds_end, description, qc_flags_json,
418
- frame_indices_json, extra_json, created_at, updated_at)
419
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
420
- """,
421
- (
422
- s_uuid,
423
- clip_uuid,
424
- int(entry.get("shot_index") or 0),
425
- start if isinstance(start, (int, float)) else None,
426
- end if isinstance(end, (int, float)) else None,
427
- entry.get("description"),
428
- _dumps(entry.get("qc_flags") or []),
429
- _dumps(frame_indices),
430
- _dumps(extra) if extra else None,
431
- str(prior["created_at"]) if prior else now,
432
- now,
433
- ),
434
- )
435
- for raw in frame_indices:
436
- try:
437
- frame_to_shot[int(raw)] = s_uuid
438
- except (TypeError, ValueError):
439
- continue
440
- stats = _write_subjective(
441
- conn, "shot", s_uuid, shot_subjective_fields(entry),
442
- source=source, author=author,
443
- )
444
- subj_written += stats["written"]
445
- subj_skipped_human += stats["skipped_human"]
446
-
447
- stats = _write_subjective(
448
- conn, "clip", clip_uuid, clip_subjective_fields(visual),
449
- source=source, author=author,
450
- )
451
- subj_written += stats["written"]
452
- subj_skipped_human += stats["skipped_human"]
453
-
454
- # Transcript segments.
455
- conn.execute("DELETE FROM transcript_segments WHERE clip_uuid = ?", (clip_uuid,))
456
- segments = transcription.get("segments") if isinstance(transcription.get("segments"), list) else []
457
- for idx, seg in enumerate(segments):
458
- if not isinstance(seg, dict):
459
- continue
460
- conn.execute(
461
- """
462
- INSERT OR REPLACE INTO transcript_segments
463
- (clip_uuid, segment_index, start_seconds, end_seconds, text, speaker_id)
464
- VALUES (?, ?, ?, ?, ?, ?)
465
- """,
466
- (
467
- clip_uuid,
468
- idx,
469
- seg.get("start") if isinstance(seg.get("start"), (int, float)) else None,
470
- seg.get("end") if isinstance(seg.get("end"), (int, float)) else None,
471
- seg.get("text"),
472
- seg.get("speaker") or seg.get("speaker_id"),
473
- ),
474
- )
475
-
476
- # Sampled frames. Shot mapping uses frame_indices_used when present,
477
- # with a time-containment fallback (some commit paths don't record
478
- # which frame indices fed which shot).
479
- shot_intervals: List[Tuple[float, float, str]] = []
480
- for entry in shot_entries:
481
- if not isinstance(entry, dict):
482
- continue
483
- s, e = entry.get("time_seconds_start"), entry.get("time_seconds_end")
484
- if isinstance(s, (int, float)) and isinstance(e, (int, float)):
485
- shot_intervals.append((float(s), float(e), shot_uuid_for(clip_uuid, s, e)))
486
-
487
- def _shot_for_time(t: Any) -> Optional[str]:
488
- if not isinstance(t, (int, float)):
489
- return None
490
- for s, e, uuid_ in shot_intervals:
491
- if s <= float(t) < e:
492
- return uuid_
493
- return None
494
-
495
- conn.execute("DELETE FROM frames WHERE clip_uuid = ?", (clip_uuid,))
496
- keyframes = motion.get("analysis_keyframes") if isinstance(motion.get("analysis_keyframes"), list) else []
497
- for kf in keyframes:
498
- if not isinstance(kf, dict):
499
- continue
500
- try:
501
- frame_index = int(kf.get("index"))
502
- except (TypeError, ValueError):
503
- continue
504
- conn.execute(
505
- """
506
- INSERT OR REPLACE INTO frames
507
- (clip_uuid, shot_uuid, frame_index, time_seconds, frame_path,
508
- selection_reason, motion_peak)
509
- VALUES (?, ?, ?, ?, ?, ?, ?)
510
- """,
511
- (
512
- clip_uuid,
513
- frame_to_shot.get(frame_index) or _shot_for_time(kf.get("time_seconds")),
514
- frame_index,
515
- kf.get("time_seconds") if isinstance(kf.get("time_seconds"), (int, float)) else None,
516
- kf.get("frame_path") or kf.get("path"),
517
- kf.get("selection_reason"),
518
- 1 if kf.get("motion_peak") else 0,
519
- ),
520
- )
521
-
522
- # QC observations: machine rows rebuild; human-resolved rows persist.
523
- conn.execute(
524
- "DELETE FROM qc_observations WHERE clip_uuid = ? AND resolved = 0 AND source = ?",
525
- (clip_uuid, source),
526
- )
527
- resolved_keys = {
528
- (str(r["observation_type"]), str(r["message"]))
529
- for r in conn.execute(
530
- "SELECT observation_type, message FROM qc_observations WHERE clip_uuid = ? AND resolved = 1",
531
- (clip_uuid,),
532
- ).fetchall()
533
- }
534
- qc = visual.get("qc") if isinstance(visual.get("qc"), dict) else {}
535
-
536
- def _insert_qc(obs_type: str, severity: str, message: Any, related: Any = None, confidence: Any = None) -> None:
537
- msg = str(message or "").strip()
538
- if not msg or (obs_type, msg) in resolved_keys:
539
- return
540
- conn.execute(
541
- """
542
- INSERT INTO qc_observations
543
- (clip_uuid, observation_type, severity, message,
544
- related_shot_indices_json, confidence, source, created_at)
545
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
546
- """,
547
- (
548
- clip_uuid,
549
- obs_type,
550
- severity,
551
- msg,
552
- _dumps(related) if related else None,
553
- str(confidence) if confidence else None,
554
- source,
555
- now,
556
- ),
557
- )
558
-
559
- for warning in qc.get("warnings") or []:
560
- _insert_qc("warning", "warn", warning)
561
- for obs in qc.get("continuity_observations") or []:
562
- if isinstance(obs, dict):
563
- _insert_qc(
564
- str(obs.get("kind") or "continuity"),
565
- "info",
566
- obs.get("observation"),
567
- related=obs.get("shot_indices"),
568
- confidence=obs.get("confidence"),
569
- )
570
- for gap in qc.get("coverage_gaps") or []:
571
- _insert_qc("coverage_gap", "info", gap)
572
-
573
- return {
574
- "success": True,
575
- "clip_uuid": clip_uuid,
576
- "shot_count": len(shot_entries),
577
- "subjective_fields_written": subj_written,
578
- "subjective_fields_preserved_human": subj_skipped_human,
579
- }
580
-
581
-
582
- # ── export / readers ─────────────────────────────────────────────────────────
583
-
584
-
585
- def _overlay_human_fields(conn: sqlite3.Connection, clip_uuid: str, report: Dict[str, Any]) -> int:
586
- """Apply current human subjective rows onto a report dict. Returns count."""
587
- ma = _ma()
588
- visual = report.get("visual")
589
- if not isinstance(visual, dict):
590
- return 0
591
- shots_by_uuid: Dict[str, int] = {}
592
- for row in conn.execute(
593
- "SELECT shot_uuid, shot_index FROM shots WHERE clip_uuid = ?", (clip_uuid,)
594
- ).fetchall():
595
- shots_by_uuid[str(row["shot_uuid"])] = int(row["shot_index"])
596
- shot_entries = visual.get("shot_descriptions") if isinstance(visual.get("shot_descriptions"), list) else []
597
- applied = 0
598
-
599
- rows = conn.execute(
600
- """
601
- SELECT entity_type, entity_uuid, field_path, value_json
602
- FROM subjective_fields
603
- WHERE superseded_at IS NULL AND source = ?
604
- AND (
605
- (entity_type = 'clip' AND entity_uuid = ?)
606
- OR (entity_type = 'shot' AND entity_uuid IN
607
- (SELECT shot_uuid FROM shots WHERE clip_uuid = ?))
608
- )
609
- """,
610
- (HUMAN_SOURCE, clip_uuid, clip_uuid),
611
- ).fetchall()
612
- for row in rows:
613
- try:
614
- value = json.loads(row["value_json"])
615
- except (TypeError, ValueError):
616
- continue
617
- path_parts = [p for p in str(row["field_path"]).split(".") if p]
618
- if not path_parts:
619
- continue
620
- if str(row["entity_type"]) == "clip":
621
- target: Any = visual
622
- else:
623
- shot_index = shots_by_uuid.get(str(row["entity_uuid"]))
624
- target = None
625
- if shot_index is not None:
626
- target = ma._find_shot_entry(shot_entries, str(shot_index))
627
- if target is None:
628
- target = ma._find_shot_entry(shot_entries, str(row["entity_uuid"]))
629
- if target is None:
630
- continue
631
- if ma._walk_set(target, path_parts, value):
632
- applied += 1
633
- return applied
634
-
635
-
636
- def export_report(project_root: str, clip_ref: Any) -> Optional[Dict[str, Any]]:
637
- """Return the canonical report (blob + human overlay) or None if absent."""
638
- try:
639
- conn = timeline_brain_db.connect(project_root)
640
- clip_uuid = resolve_clip_uuid(conn, clip_ref)
641
- if not clip_uuid:
642
- return None
643
- row = conn.execute(
644
- "SELECT report_json FROM analysis_reports WHERE clip_uuid = ?", (clip_uuid,)
645
- ).fetchone()
646
- if not row:
647
- return None
648
- report = json.loads(row["report_json"])
649
- if not isinstance(report, dict):
650
- return None
651
- _overlay_human_fields(conn, clip_uuid, report)
652
- return report
653
- except (sqlite3.Error, ValueError, OSError) as exc:
654
- logger.debug("export_report fell back to JSON for %r: %s", clip_ref, exc)
655
- return None
656
-
657
-
658
- def load_db_report(
659
- project_root: str,
660
- *,
661
- clip_dir: Optional[str] = None,
662
- clip_id: Optional[str] = None,
663
- clip_uuid: Optional[str] = None,
664
- file_path: Optional[str] = None,
665
- ) -> Optional[Dict[str, Any]]:
666
- """DB-first reader used by panel/MCP readers. None means: fall back to JSON."""
667
- for ref in (clip_uuid, clip_id, clip_dir, file_path):
668
- if not ref:
669
- continue
670
- report = export_report(project_root, ref)
671
- if report is not None:
672
- return report
673
- return None
674
-
675
-
676
- def export_report_file(project_root: str, clip_ref: Any, path: str) -> Optional[str]:
677
- """Write the derived analysis.json export for a clip. Returns the path."""
678
- report = export_report(project_root, clip_ref)
679
- if report is None:
680
- return None
681
- _ma()._write_json(path, report)
682
- return path
683
-
684
-
685
- def list_clip_rows(project_root: str) -> List[Dict[str, Any]]:
686
- """Headline clip rows for list endpoints (no blobs)."""
687
- conn = timeline_brain_db.connect(project_root)
688
- rows = conn.execute("SELECT * FROM clips ORDER BY clip_name COLLATE NOCASE").fetchall()
689
- return [dict(r) for r in rows]
690
-
691
-
692
- def db_status(project_root: str) -> Dict[str, Any]:
693
- """Counts + schema version for the analysis store of a project."""
694
- try:
695
- conn = timeline_brain_db.connect(project_root)
696
- version = timeline_brain_db._read_schema_version(conn)
697
- counts = {}
698
- for table in (
699
- "clips",
700
- "analysis_reports",
701
- "shots",
702
- "subjective_fields",
703
- "transcript_segments",
704
- "frames",
705
- "qc_observations",
706
- ):
707
- counts[table] = int(conn.execute(f"SELECT COUNT(*) AS n FROM {table}").fetchone()["n"])
708
- human = conn.execute(
709
- "SELECT COUNT(*) AS n FROM subjective_fields WHERE source = ? AND superseded_at IS NULL",
710
- (HUMAN_SOURCE,),
711
- ).fetchone()
712
- return {
713
- "success": True,
714
- "project_root": project_root,
715
- "db_path": timeline_brain_db.db_path_for_project(project_root),
716
- "schema_version": version,
717
- "canonical": version >= 9,
718
- "counts": counts,
719
- "current_human_fields": int(human["n"]),
720
- }
721
- except (sqlite3.Error, OSError) as exc:
722
- return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
723
-
724
-
725
- # ── corrections (row level) ──────────────────────────────────────────────────
726
-
727
-
728
- def record_human_correction(
729
- project_root: str,
730
- *,
731
- clip_ref: Any,
732
- entity_type: str,
733
- entity_uuid: Any,
734
- field_path: str,
735
- value: Any,
736
- author: str = "unknown",
737
- reason: Optional[str] = None,
738
- confidence: Optional[str] = None,
739
- ) -> Dict[str, Any]:
740
- """Row-level human correction: supersede current value, append changelog.
741
-
742
- `entity_uuid` accepts a shot_uuid OR a shot_index (resolved against the
743
- clip's shots), matching the sidecar contract.
744
- """
745
- with timeline_brain_db.transaction(project_root) as conn:
746
- clip_uuid = resolve_clip_uuid(conn, clip_ref)
747
- if not clip_uuid:
748
- return {"success": False, "error": f"clip not found in DB: {clip_ref}"}
749
- target_uuid = clip_uuid
750
- if entity_type == "shot":
751
- candidate = str(entity_uuid)
752
- row = conn.execute(
753
- "SELECT shot_uuid FROM shots WHERE clip_uuid = ? AND shot_uuid = ?",
754
- (clip_uuid, candidate),
755
- ).fetchone()
756
- if row is None:
757
- try:
758
- row = conn.execute(
759
- "SELECT shot_uuid FROM shots WHERE clip_uuid = ? AND shot_index = ?",
760
- (clip_uuid, int(candidate)),
761
- ).fetchone()
762
- except (TypeError, ValueError):
763
- row = None
764
- if row is None:
765
- return {"success": False, "error": f"shot not found in DB: {entity_uuid}"}
766
- target_uuid = str(row["shot_uuid"])
767
- stats = _write_subjective(
768
- conn,
769
- entity_type,
770
- target_uuid,
771
- {field_path: value},
772
- source=HUMAN_SOURCE,
773
- author=author,
774
- confidence=confidence,
775
- reason=reason,
776
- overwrite_human=True,
777
- )
778
- return {
779
- "success": True,
780
- "clip_uuid": clip_uuid,
781
- "entity_type": entity_type,
782
- "entity_uuid": target_uuid,
783
- "field_path": field_path,
784
- "written": stats["written"],
785
- }
786
-
787
-
788
- def clear_human_field(
789
- project_root: str,
790
- *,
791
- clip_ref: Any,
792
- entity_type: str,
793
- entity_uuid: Any,
794
- field_path: str,
795
- author: str = "unknown",
796
- reason: Optional[str] = None,
797
- ) -> Dict[str, Any]:
798
- """Revert a field to machine-derived: supersede the current human row.
799
-
800
- The machine value lives in the report blob, so superseding the human row
801
- is all the export overlay needs to fall back to it.
802
- """
803
- now = _now()
804
- with timeline_brain_db.transaction(project_root) as conn:
805
- clip_uuid = resolve_clip_uuid(conn, clip_ref)
806
- if not clip_uuid:
807
- return {"success": False, "error": f"clip not found in DB: {clip_ref}"}
808
- target_uuid = clip_uuid
809
- if entity_type == "shot":
810
- candidate = str(entity_uuid)
811
- row = conn.execute(
812
- "SELECT shot_uuid FROM shots WHERE clip_uuid = ? AND shot_uuid = ?",
813
- (clip_uuid, candidate),
814
- ).fetchone()
815
- if row is None:
816
- try:
817
- row = conn.execute(
818
- "SELECT shot_uuid FROM shots WHERE clip_uuid = ? AND shot_index = ?",
819
- (clip_uuid, int(candidate)),
820
- ).fetchone()
821
- except (TypeError, ValueError):
822
- row = None
823
- if row is None:
824
- return {"success": False, "error": f"shot not found in DB: {entity_uuid}"}
825
- target_uuid = str(row["shot_uuid"])
826
- existing = conn.execute(
827
- """
828
- SELECT * FROM subjective_fields
829
- WHERE entity_type = ? AND entity_uuid = ? AND field_path = ?
830
- AND superseded_at IS NULL AND source = ?
831
- """,
832
- (entity_type, target_uuid, field_path, HUMAN_SOURCE),
833
- ).fetchone()
834
- if existing is None:
835
- return {"success": True, "cleared": 0, "note": "no current human row"}
836
- conn.execute(
837
- "UPDATE subjective_fields SET superseded_at = ? WHERE id = ?",
838
- (now, existing["id"]),
839
- )
840
- conn.execute(
841
- """
842
- INSERT INTO field_changelog
843
- (entity_type, entity_uuid, field_path, previous_value_json,
844
- new_value_json, previous_source, new_source, previous_author,
845
- new_author, change_reason, timestamp)
846
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
847
- """,
848
- (
849
- entity_type,
850
- target_uuid,
851
- field_path,
852
- existing["value_json"],
853
- _dumps(None),
854
- existing["source"],
855
- "machine_revert",
856
- existing["author"],
857
- author,
858
- reason or "reverted to machine-derived",
859
- now,
860
- ),
861
- )
862
- return {"success": True, "cleared": 1, "clip_uuid": clip_uuid, "entity_uuid": target_uuid}
863
-
864
-
865
- def _ingest_corrections_sidecar(project_root: str, clip_dir_path: str, clip_uuid: str) -> int:
866
- """Import human rows from a corrections.json sidecar (db_ingest path)."""
867
- path = os.path.join(clip_dir_path, "corrections.json")
868
- if not os.path.isfile(path):
869
- return 0
870
- try:
871
- with open(path, "r", encoding="utf-8") as handle:
872
- data = json.load(handle)
873
- except (OSError, json.JSONDecodeError):
874
- return 0
875
- current = data.get("current") if isinstance(data.get("current"), dict) else {}
876
- imported = 0
877
- for key, entry in current.items():
878
- if not isinstance(entry, dict) or entry.get("source") != HUMAN_SOURCE:
879
- continue
880
- parts = str(key).split(":", 2)
881
- if len(parts) != 3:
882
- continue
883
- entity_type, entity_uuid, field_path = parts
884
- if entity_type not in ("clip", "shot"):
885
- continue
886
- result = record_human_correction(
887
- project_root,
888
- clip_ref=clip_uuid,
889
- entity_type=entity_type,
890
- entity_uuid=clip_uuid if entity_type == "clip" else entity_uuid,
891
- field_path=field_path,
892
- value=entry.get("value"),
893
- author=str(entry.get("author") or "unknown"),
894
- reason="imported from corrections.json",
895
- confidence=entry.get("confidence"),
896
- )
897
- if result.get("success") and result.get("written"):
898
- imported += 1
899
- return imported
900
-
901
-
902
- def ingest_project(project_root: str) -> Dict[str, Any]:
903
- """Walk clips/ and ingest every analysis.json + corrections.json sidecar.
904
-
905
- The migration entry point for projects analyzed before v9. Idempotent.
906
- """
907
- ma = _ma()
908
- root = ma.normalize_path(project_root)
909
- clips_root = os.path.join(root, "clips")
910
- ingested: List[Dict[str, Any]] = []
911
- errors: List[Dict[str, Any]] = []
912
- corrections_imported = 0
913
- if os.path.isdir(clips_root):
914
- for entry in sorted(os.listdir(clips_root)):
915
- clip_dir_path = os.path.join(clips_root, entry)
916
- report_path = os.path.join(clip_dir_path, "analysis.json")
917
- if not os.path.isfile(report_path):
918
- continue
919
- try:
920
- with open(report_path, "r", encoding="utf-8") as handle:
921
- report = json.load(handle)
922
- except (OSError, json.JSONDecodeError) as exc:
923
- errors.append({"clip_dir": entry, "error": f"{type(exc).__name__}: {exc}"})
924
- continue
925
- try:
926
- result = ingest_report(root, report, clip_dir=clip_dir_path)
927
- except (sqlite3.Error, ValueError) as exc:
928
- errors.append({"clip_dir": entry, "error": f"{type(exc).__name__}: {exc}"})
929
- continue
930
- if result.get("success"):
931
- corrections_imported += _ingest_corrections_sidecar(
932
- root, clip_dir_path, str(result["clip_uuid"])
933
- )
934
- ingested.append({"clip_dir": entry, "clip_uuid": result["clip_uuid"]})
935
- else:
936
- errors.append({"clip_dir": entry, "error": result.get("error")})
937
- return {
938
- "success": not errors,
939
- "project_root": root,
940
- "ingested_count": len(ingested),
941
- "ingested": ingested,
942
- "corrections_imported": corrections_imported,
943
- "errors": errors,
944
- "status": db_status(root),
945
- }