@hybridlabor-api/aos 4.4.1 → 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 (305) 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.md +573 -0
  20. package/README.de.md +0 -8
  21. package/README.md +0 -8
  22. package/README.pt.md +0 -8
  23. package/assets/brand/ao-ant-mark.svg +91 -0
  24. package/assets/brand/bdb-core-mark.svg +34 -0
  25. package/assets/brand/heimdall-guardian-mark.svg +16 -0
  26. package/assets/brand/memb-crystal-mark.svg +18 -0
  27. package/bin/aos-config.mjs +89 -0
  28. package/bin/aos-dashboard.mjs +608 -0
  29. package/bin/aos-uninstall.mjs +227 -0
  30. package/docs/sessions/audit-2026-09-14-silent-failures.md +114 -0
  31. package/docs/skills_table.md +0 -3
  32. package/installer.js +387 -237
  33. package/mcps/RhinoMCP/cc-plugin/.claude/settings.json +10 -0
  34. package/mcps/after-effects-mcp/build/index.js +840 -0
  35. package/mcps/after-effects-mcp/build/scripts/applyEffect.jsx +153 -0
  36. package/mcps/after-effects-mcp/build/scripts/applyEffectTemplate.jsx +218 -0
  37. package/mcps/after-effects-mcp/build/scripts/createComposition.jsx +71 -0
  38. package/mcps/after-effects-mcp/build/scripts/createShapeLayer.jsx +147 -0
  39. package/mcps/after-effects-mcp/build/scripts/createSolidLayer.jsx +114 -0
  40. package/mcps/after-effects-mcp/build/scripts/createTextLayer.jsx +115 -0
  41. package/mcps/after-effects-mcp/build/scripts/getLayerInfo.jsx +192 -0
  42. package/mcps/after-effects-mcp/build/scripts/getProjectInfo.jsx +90 -0
  43. package/mcps/after-effects-mcp/build/scripts/listCompositions.jsx +50 -0
  44. package/mcps/after-effects-mcp/build/scripts/mcp-bridge-auto.jsx +1773 -0
  45. package/mcps/after-effects-mcp/build/scripts/setLayerProperties.jsx +160 -0
  46. package/mcps/computer-use-mcp/dist/client.d.ts +150 -0
  47. package/mcps/computer-use-mcp/dist/client.js +136 -0
  48. package/mcps/computer-use-mcp/dist/entrypoint.d.ts +16 -0
  49. package/mcps/computer-use-mcp/dist/entrypoint.js +26 -0
  50. package/mcps/computer-use-mcp/dist/native.d.ts +212 -0
  51. package/mcps/computer-use-mcp/dist/native.js +50 -0
  52. package/mcps/computer-use-mcp/dist/server.d.ts +32 -0
  53. package/mcps/computer-use-mcp/dist/server.js +342 -0
  54. package/mcps/computer-use-mcp/dist/session.d.ts +101 -0
  55. package/mcps/computer-use-mcp/dist/session.js +2372 -0
  56. package/package.json +13 -4
  57. package/skills/global_config/aos-project-init/SKILL.md +97 -130
  58. package/skills/global_config/aos-project-init/assets/openwikiignore.template +62 -0
  59. package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +38 -1
  60. package/skills/global_config/aos-setup/SKILL.md +98 -8
  61. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +21 -2
  62. package/skills/global_config/ask-tim/SKILL.md +2 -7
  63. package/skills/global_config/openwiki-skill/scripts/openwiki_daemon.py +65 -12
  64. package/bin/setup-saas.mjs +0 -792
  65. package/mcps/blender-mcp-server/.dockerignore +0 -19
  66. package/mcps/blender-mcp-server/.github/workflows/ci.yml +0 -67
  67. package/mcps/blender-mcp-server/.github/workflows/publish-pypi.yml +0 -54
  68. package/mcps/blender-mcp-server/CHANGELOG.md +0 -64
  69. package/mcps/blender-mcp-server/CONTRIBUTING.md +0 -107
  70. package/mcps/blender-mcp-server/Dockerfile +0 -19
  71. package/mcps/blender-mcp-server/LICENSE +0 -21
  72. package/mcps/blender-mcp-server/README.md +0 -409
  73. package/mcps/blender-mcp-server/addon/__init__.py +0 -1440
  74. package/mcps/blender-mcp-server/addon/models.py +0 -223
  75. package/mcps/blender-mcp-server/pyproject.toml +0 -120
  76. package/mcps/blender-mcp-server/scripts/blender_bridge_request.py +0 -71
  77. package/mcps/blender-mcp-server/scripts/blender_create_test_cube.py +0 -49
  78. package/mcps/blender-mcp-server/scripts/blender_scene_info.py +0 -38
  79. package/mcps/blender-mcp-server/scripts/build_addon_zip.sh +0 -29
  80. package/mcps/blender-mcp-server/scripts/demos/README.md +0 -186
  81. package/mcps/blender-mcp-server/scripts/demos/dam_break_scene.py +0 -276
  82. package/mcps/blender-mcp-server/scripts/demos/open_dam_break_gui.py +0 -33
  83. package/mcps/blender-mcp-server/scripts/demos/pipe_movement_study.py +0 -518
  84. package/mcps/blender-mcp-server/scripts/demos/pipe_pressure_failure_scene.py +0 -806
  85. package/mcps/blender-mcp-server/scripts/demos/procedural_dam_break_scene.py +0 -600
  86. package/mcps/blender-mcp-server/scripts/demos/run_dam_break.py +0 -510
  87. package/mcps/blender-mcp-server/scripts/launch_blender_gui.py +0 -41
  88. package/mcps/blender-mcp-server/scripts/library/README.md +0 -64
  89. package/mcps/blender-mcp-server/scripts/library/apply_transforms.py +0 -46
  90. package/mcps/blender-mcp-server/scripts/library/camera.py +0 -51
  91. package/mcps/blender-mcp-server/scripts/library/collections.py +0 -65
  92. package/mcps/blender-mcp-server/scripts/library/create_mesh.py +0 -41
  93. package/mcps/blender-mcp-server/scripts/library/effector.py +0 -48
  94. package/mcps/blender-mcp-server/scripts/library/fluid_domain.py +0 -80
  95. package/mcps/blender-mcp-server/scripts/library/fluid_inflow.py +0 -98
  96. package/mcps/blender-mcp-server/scripts/library/frame_range.py +0 -35
  97. package/mcps/blender-mcp-server/scripts/library/keyframes.py +0 -59
  98. package/mcps/blender-mcp-server/scripts/library/rigid_body.py +0 -52
  99. package/mcps/blender-mcp-server/scripts/library/save_blend.py +0 -40
  100. package/mcps/blender-mcp-server/scripts/server_start.sh +0 -26
  101. package/mcps/blender-mcp-server/src/blender_mcp_server/__init__.py +0 -3
  102. package/mcps/blender-mcp-server/src/blender_mcp_server/headless.py +0 -330
  103. package/mcps/blender-mcp-server/src/blender_mcp_server/server.py +0 -606
  104. package/mcps/davinci-mcp-professional/.bandit +0 -35
  105. package/mcps/davinci-mcp-professional/.claude/commands/security-review.md +0 -191
  106. package/mcps/davinci-mcp-professional/.claude/settings.json +0 -1
  107. package/mcps/davinci-mcp-professional/.editorconfig +0 -27
  108. package/mcps/davinci-mcp-professional/.gitattributes +0 -207
  109. package/mcps/davinci-mcp-professional/.github/CODEOWNERS +0 -2
  110. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  111. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  112. package/mcps/davinci-mcp-professional/.github/PULL_REQUEST_TEMPLATE.md +0 -20
  113. package/mcps/davinci-mcp-professional/.github/SECURITY.md +0 -97
  114. package/mcps/davinci-mcp-professional/.github/dependabot.yml +0 -27
  115. package/mcps/davinci-mcp-professional/.github/workflows/ci.yml +0 -39
  116. package/mcps/davinci-mcp-professional/ATTRIBUTION.md +0 -4
  117. package/mcps/davinci-mcp-professional/AUTHORS.md +0 -9
  118. package/mcps/davinci-mcp-professional/BUGS.md +0 -55
  119. package/mcps/davinci-mcp-professional/CLAUDE.md +0 -229
  120. package/mcps/davinci-mcp-professional/CONTRIBUTING.md +0 -51
  121. package/mcps/davinci-mcp-professional/COPYING.md +0 -228
  122. package/mcps/davinci-mcp-professional/Doxyfile +0 -379
  123. package/mcps/davinci-mcp-professional/README.md +0 -143
  124. package/mcps/davinci-mcp-professional/SECURITY.md +0 -40
  125. package/mcps/davinci-mcp-professional/USING.md +0 -301
  126. package/mcps/davinci-mcp-professional/VERSION.md +0 -13
  127. package/mcps/davinci-mcp-professional/build.py +0 -169
  128. package/mcps/davinci-mcp-professional/claude_desktop_config_template.json +0 -11
  129. package/mcps/davinci-mcp-professional/hooks/ci-check.sh +0 -29
  130. package/mcps/davinci-mcp-professional/hooks/pre-push +0 -46
  131. package/mcps/davinci-mcp-professional/main.py +0 -40
  132. package/mcps/davinci-mcp-professional/mcp_server.py +0 -30
  133. package/mcps/davinci-mcp-professional/pyproject.toml +0 -115
  134. package/mcps/davinci-mcp-professional/security_audit.py +0 -379
  135. package/mcps/davinci-mcp-professional/src/davinci_mcp/__init__.py +0 -19
  136. package/mcps/davinci-mcp-professional/src/davinci_mcp/cli.py +0 -138
  137. package/mcps/davinci-mcp-professional/src/davinci_mcp/resolve_client.py +0 -342
  138. package/mcps/davinci-mcp-professional/src/davinci_mcp/resources/__init__.py +0 -60
  139. package/mcps/davinci-mcp-professional/src/davinci_mcp/server.py +0 -185
  140. package/mcps/davinci-mcp-professional/src/davinci_mcp/tools/__init__.py +0 -146
  141. package/mcps/davinci-mcp-professional/src/davinci_mcp/types.py +0 -125
  142. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/__init__.py +0 -19
  143. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/platform.py +0 -239
  144. package/mcps/davinci-mcp-professional/test.py +0 -186
  145. package/mcps/davinci-mcp-professional/uv.lock +0 -2073
  146. package/mcps/davinci-resolve-mcp/.claude/skills/media-analysis.md +0 -44
  147. package/mcps/davinci-resolve-mcp/.clinerules +0 -17
  148. package/mcps/davinci-resolve-mcp/.cursorrules +0 -17
  149. package/mcps/davinci-resolve-mcp/.github/copilot-instructions.md +0 -17
  150. package/mcps/davinci-resolve-mcp/.github/workflows/npm-publish.yml +0 -109
  151. package/mcps/davinci-resolve-mcp/.windsurfrules +0 -17
  152. package/mcps/davinci-resolve-mcp/AGENTS.md +0 -114
  153. package/mcps/davinci-resolve-mcp/CHANGELOG.md +0 -2648
  154. package/mcps/davinci-resolve-mcp/CLAUDE.md +0 -15
  155. package/mcps/davinci-resolve-mcp/LICENSE +0 -21
  156. package/mcps/davinci-resolve-mcp/README.md +0 -173
  157. package/mcps/davinci-resolve-mcp/SECURITY.md +0 -53
  158. package/mcps/davinci-resolve-mcp/bin/davinci-resolve-mcp.mjs +0 -450
  159. package/mcps/davinci-resolve-mcp/examples/README.md +0 -53
  160. package/mcps/davinci-resolve-mcp/examples/markers/README.md +0 -81
  161. package/mcps/davinci-resolve-mcp/examples/media/README.md +0 -94
  162. package/mcps/davinci-resolve-mcp/examples/timeline/README.md +0 -98
  163. package/mcps/davinci-resolve-mcp/install.py +0 -1687
  164. package/mcps/davinci-resolve-mcp/package.json +0 -52
  165. package/mcps/davinci-resolve-mcp/release-notes/v2.22.0.md +0 -51
  166. package/mcps/davinci-resolve-mcp/release-notes/v2.23.0.md +0 -51
  167. package/mcps/davinci-resolve-mcp/release-notes/v2.23.1.md +0 -35
  168. package/mcps/davinci-resolve-mcp/release-notes/v2.24.1.md +0 -32
  169. package/mcps/davinci-resolve-mcp/scripts/audit_api_parity.py +0 -275
  170. package/mcps/davinci-resolve-mcp/scripts/audit_readwrite_symmetry.py +0 -80
  171. package/mcps/davinci-resolve-mcp/scripts/doctor.py +0 -227
  172. package/mcps/davinci-resolve-mcp/scripts/gen_api_limitations.py +0 -144
  173. package/mcps/davinci-resolve-mcp/scripts/live_media_analysis_polish_probe.py +0 -65
  174. package/mcps/davinci-resolve-mcp/scripts/measure_bridge_cost.py +0 -65
  175. package/mcps/davinci-resolve-mcp/scripts/regen_panel_screenshots.py +0 -76
  176. package/mcps/davinci-resolve-mcp/src/__init__.py +0 -3
  177. package/mcps/davinci-resolve-mcp/src/analysis_dashboard.py +0 -15505
  178. package/mcps/davinci-resolve-mcp/src/batch_cli.py +0 -540
  179. package/mcps/davinci-resolve-mcp/src/control_panel.py +0 -13
  180. package/mcps/davinci-resolve-mcp/src/granular/__init__.py +0 -17
  181. package/mcps/davinci-resolve-mcp/src/granular/common.py +0 -741
  182. package/mcps/davinci-resolve-mcp/src/granular/folder.py +0 -420
  183. package/mcps/davinci-resolve-mcp/src/granular/gallery.py +0 -306
  184. package/mcps/davinci-resolve-mcp/src/granular/graph.py +0 -309
  185. package/mcps/davinci-resolve-mcp/src/granular/media_pool.py +0 -679
  186. package/mcps/davinci-resolve-mcp/src/granular/media_pool_item.py +0 -1020
  187. package/mcps/davinci-resolve-mcp/src/granular/media_storage.py +0 -179
  188. package/mcps/davinci-resolve-mcp/src/granular/project.py +0 -1654
  189. package/mcps/davinci-resolve-mcp/src/granular/resolve_control.py +0 -538
  190. package/mcps/davinci-resolve-mcp/src/granular/timeline.py +0 -1075
  191. package/mcps/davinci-resolve-mcp/src/granular/timeline_item.py +0 -2251
  192. package/mcps/davinci-resolve-mcp/src/resolve_mcp_server.py +0 -43
  193. package/mcps/davinci-resolve-mcp/src/server.py +0 -24116
  194. package/mcps/davinci-resolve-mcp/src/utils/__init__.py +0 -3
  195. package/mcps/davinci-resolve-mcp/src/utils/actor_identity.py +0 -46
  196. package/mcps/davinci-resolve-mcp/src/utils/analysis_caps.py +0 -678
  197. package/mcps/davinci-resolve-mcp/src/utils/analysis_memory.py +0 -755
  198. package/mcps/davinci-resolve-mcp/src/utils/analysis_runs.py +0 -307
  199. package/mcps/davinci-resolve-mcp/src/utils/analysis_store.py +0 -945
  200. package/mcps/davinci-resolve-mcp/src/utils/api_truth.py +0 -525
  201. package/mcps/davinci-resolve-mcp/src/utils/app_control.py +0 -319
  202. package/mcps/davinci-resolve-mcp/src/utils/audio_fairlight_live_probe.py +0 -263
  203. package/mcps/davinci-resolve-mcp/src/utils/brain_edits.py +0 -387
  204. package/mcps/davinci-resolve-mcp/src/utils/bridge_metrics.py +0 -76
  205. package/mcps/davinci-resolve-mcp/src/utils/cdl.py +0 -20
  206. package/mcps/davinci-resolve-mcp/src/utils/clip_query.py +0 -85
  207. package/mcps/davinci-resolve-mcp/src/utils/cloud_operations.py +0 -192
  208. package/mcps/davinci-resolve-mcp/src/utils/color_grade_live_probe.py +0 -444
  209. package/mcps/davinci-resolve-mcp/src/utils/contracts.py +0 -102
  210. package/mcps/davinci-resolve-mcp/src/utils/cut_ir.py +0 -115
  211. package/mcps/davinci-resolve-mcp/src/utils/dctl_templates.py +0 -368
  212. package/mcps/davinci-resolve-mcp/src/utils/deep_vision.py +0 -643
  213. package/mcps/davinci-resolve-mcp/src/utils/destructive_hook.py +0 -606
  214. package/mcps/davinci-resolve-mcp/src/utils/edit_engine.py +0 -737
  215. package/mcps/davinci-resolve-mcp/src/utils/embeddings.py +0 -922
  216. package/mcps/davinci-resolve-mcp/src/utils/entities.py +0 -579
  217. package/mcps/davinci-resolve-mcp/src/utils/extension_authoring_live_probe.py +0 -292
  218. package/mcps/davinci-resolve-mcp/src/utils/failure_tracker.py +0 -119
  219. package/mcps/davinci-resolve-mcp/src/utils/fuse_templates.py +0 -1968
  220. package/mcps/davinci-resolve-mcp/src/utils/fusion_composition_live_probe.py +0 -284
  221. package/mcps/davinci-resolve-mcp/src/utils/fusion_group_settings.py +0 -334
  222. package/mcps/davinci-resolve-mcp/src/utils/layout_presets.py +0 -333
  223. package/mcps/davinci-resolve-mcp/src/utils/mcp_stdio.py +0 -32
  224. package/mcps/davinci-resolve-mcp/src/utils/mcp_transport.py +0 -128
  225. package/mcps/davinci-resolve-mcp/src/utils/media_analysis.py +0 -7690
  226. package/mcps/davinci-resolve-mcp/src/utils/media_analysis_jobs.py +0 -805
  227. package/mcps/davinci-resolve-mcp/src/utils/media_pool_changes.py +0 -121
  228. package/mcps/davinci-resolve-mcp/src/utils/media_pool_ingest_live_probe.py +0 -592
  229. package/mcps/davinci-resolve-mcp/src/utils/multicam.py +0 -393
  230. package/mcps/davinci-resolve-mcp/src/utils/object_inspection.py +0 -301
  231. package/mcps/davinci-resolve-mcp/src/utils/page_lock.py +0 -75
  232. package/mcps/davinci-resolve-mcp/src/utils/platform.py +0 -157
  233. package/mcps/davinci-resolve-mcp/src/utils/proc.py +0 -26
  234. package/mcps/davinci-resolve-mcp/src/utils/project_cleanup.py +0 -73
  235. package/mcps/davinci-resolve-mcp/src/utils/project_lifecycle_live_probe.py +0 -376
  236. package/mcps/davinci-resolve-mcp/src/utils/project_lint.py +0 -131
  237. package/mcps/davinci-resolve-mcp/src/utils/project_properties.py +0 -601
  238. package/mcps/davinci-resolve-mcp/src/utils/project_spec.py +0 -459
  239. package/mcps/davinci-resolve-mcp/src/utils/readback.py +0 -91
  240. package/mcps/davinci-resolve-mcp/src/utils/render_deliver_live_probe.py +0 -384
  241. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_governance.py +0 -214
  242. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_ledger.py +0 -244
  243. package/mcps/davinci-resolve-mcp/src/utils/resolve_busy.py +0 -171
  244. package/mcps/davinci-resolve-mcp/src/utils/resolve_connection.py +0 -77
  245. package/mcps/davinci-resolve-mcp/src/utils/review_annotation_live_probe.py +0 -352
  246. package/mcps/davinci-resolve-mcp/src/utils/script_templates.py +0 -1193
  247. package/mcps/davinci-resolve-mcp/src/utils/shot_relationships.py +0 -544
  248. package/mcps/davinci-resolve-mcp/src/utils/structural_diff.py +0 -175
  249. package/mcps/davinci-resolve-mcp/src/utils/sync_detection.py +0 -889
  250. package/mcps/davinci-resolve-mcp/src/utils/timeline_brain_db.py +0 -826
  251. package/mcps/davinci-resolve-mcp/src/utils/timeline_conform_live_probe.py +0 -280
  252. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_live_probe.py +0 -1092
  253. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_probe.py +0 -185
  254. package/mcps/davinci-resolve-mcp/src/utils/timeline_title_text.py +0 -87
  255. package/mcps/davinci-resolve-mcp/src/utils/timeline_versioning.py +0 -730
  256. package/mcps/davinci-resolve-mcp/src/utils/update_check.py +0 -685
  257. package/mcps/vectorworks-mcp/.dockerignore +0 -8
  258. package/mcps/vectorworks-mcp/Dockerfile +0 -28
  259. package/mcps/vectorworks-mcp/README.md +0 -156
  260. package/mcps/vectorworks-mcp/app/__init__.py +0 -2
  261. package/mcps/vectorworks-mcp/app/api.py +0 -52
  262. package/mcps/vectorworks-mcp/app/chunking.py +0 -56
  263. package/mcps/vectorworks-mcp/app/config.py +0 -29
  264. package/mcps/vectorworks-mcp/app/embeddings.py +0 -28
  265. package/mcps/vectorworks-mcp/app/indexer.py +0 -106
  266. package/mcps/vectorworks-mcp/app/main.py +0 -33
  267. package/mcps/vectorworks-mcp/app/mcp_server.py +0 -71
  268. package/mcps/vectorworks-mcp/app/search.py +0 -54
  269. package/mcps/vectorworks-mcp/app/utils.py +0 -14
  270. package/mcps/vectorworks-mcp/data/.gitkeep +0 -1
  271. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_PluginScript.htm +0 -95
  272. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_ScriptEditor.htm +0 -266
  273. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_Scripting.htm +0 -106
  274. package/mcps/vectorworks-mcp/data/vw-app-help/VW2023_CreatingScriptedPlugins.htm +0 -327
  275. package/mcps/vectorworks-mcp/data/vw-app-help/VW2024_RunningScripts.htm +0 -116
  276. package/mcps/vectorworks-mcp/data/vw-devwiki/VS_Function_Reference_category.html +0 -2129
  277. package/mcps/vectorworks-mcp/data/vw-jp-ref/ObjectsSymbols.html +0 -2035
  278. package/mcps/vectorworks-mcp/data/vw-jp-ref/ScriptFunctionReference.html +0 -29
  279. package/mcps/vectorworks-mcp/design.md +0 -57
  280. package/mcps/vectorworks-mcp/docker-compose.yml +0 -18
  281. package/mcps/vectorworks-mcp/index/.gitkeep +0 -1
  282. package/mcps/vectorworks-mcp/index/meta.jsonl +0 -3956
  283. package/mcps/vectorworks-mcp/index/vw.faiss +0 -0
  284. package/mcps/vectorworks-mcp/requirements.txt +0 -10
  285. package/mcps/vectorworks-mcp/scripts/fetch_docs_minimal.sh +0 -84
  286. package/mcps/vectorworks-mcp/scripts/fetch_github_vectorworks.sh +0 -85
  287. package/mcps/vectorworks-mcp/templates/index.html +0 -40
  288. package/scripts/ecosystem-health-audit.js +0 -257
  289. package/skills/bdb-dev-os-skill/SKILL.md +0 -49
  290. package/skills/bdb-dev-os-skill/package.json +0 -11
  291. package/skills/bdbsaastraining/SKILL.md +0 -316
  292. package/skills/bdbsaastraining/references/exam-pool.md +0 -313
  293. package/skills/bdbsaastraining/references/interaction-modes.md +0 -94
  294. package/skills/bdbsaastraining/references/track-a-ai-agent.md +0 -111
  295. package/skills/bdbsaastraining/references/track-b-wordpress.md +0 -111
  296. package/skills/bdbsaastraining/references/track-c-mailserver.md +0 -107
  297. package/skills/bdbsaastraining/references/track-d-debian.md +0 -109
  298. package/skills/bdbsaastraining/references/track-e-custom.md +0 -83
  299. package/skills/bdbsaastraining/scripts/build_profile.py +0 -529
  300. package/skills/bdbsaastraining/scripts/generate_certificate.py +0 -161
  301. package/skills/bdbsaastraining/scripts/preflight_check.sh +0 -150
  302. package/skills/bdbsaastraining/templates/certificate_template.html +0 -508
  303. package/skills/global_config/bdb-ecosystem-health/SKILL.md +0 -68
  304. package/skills/global_config/bdbsaas-ops/SKILL.md +0 -16
  305. 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
- }