davinci-resolve-mcp 2.87.2 → 2.89.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.
@@ -102,11 +102,13 @@ def at_least(live: Any, required: Any) -> Optional[bool]:
102
102
  # measured from what a reporter measured, because the two carry different
103
103
  # weight and an agent relaying the fact should be able to say which it is.
104
104
  #
105
- # measured — probed live by this project
106
- # reported — a user's live probe, credited, not independently reproduced
107
- # vendor — stated by Blackmagic's shipped Developer/Scripting/README.txt
105
+ # measured — probed live by this project
106
+ # reported — a user's live probe, credited, not independently reproduced
107
+ # vendor — stated by Blackmagic's shipped Developer/Scripting/README.txt
108
+ # documented — a floor this server already enforces at the call site, taken
109
+ # from Blackmagic's release documentation. See CODE_FLOORS.
108
110
 
109
- VERSION_GATES: List[Dict[str, Any]] = [
111
+ _EVIDENCE_GATES: List[Dict[str, Any]] = [
110
112
  {
111
113
  "symbol": "Resolve.GetFairlightPresets",
112
114
  "introduced_in": "20.2.2",
@@ -117,7 +119,11 @@ VERSION_GATES: List[Dict[str, Any]] = [
117
119
  "issue": 128,
118
120
  },
119
121
  {
120
- "symbol": "Timeline.ApplyFairlightPresetToCurrentTimeline",
122
+ # Named on Project, not Timeline: the shipped README lists it in the
123
+ # Project section and src/server.py calls it on the project handle. The
124
+ # method's name says "CurrentTimeline", which is what made the earlier
125
+ # Timeline attribution here look right.
126
+ "symbol": "Project.ApplyFairlightPresetToCurrentTimeline",
121
127
  "introduced_in": "20.2.2",
122
128
  "source": "measured",
123
129
  "note": "Same gate as GetFairlightPresets; the two are only useful together.",
@@ -166,6 +172,90 @@ VERSION_GATES: List[Dict[str, Any]] = [
166
172
  },
167
173
  ]
168
174
 
175
+
176
+ # ── Floors this server already enforces ─────────────────────────────────────
177
+ # `_requires_method(obj, "Method", "X.Y")` in src/server.py and src/granular/
178
+ # refuses a call when the method is absent and names a floor. Those floors were
179
+ # real routing knowledge that `check_version_support` could not see: the ledger
180
+ # above knew seven symbols while the two servers gated thirty-one, so the one
181
+ # call an agent is told to make answered `unknown` for surfaces this repo was
182
+ # already routing on. That is the shape of issue #132 — guidance that does not
183
+ # vary with the build it is connected to — showing up inside our own tooling.
184
+ #
185
+ # The `documented` label is deliberately weaker than `measured`. These floors
186
+ # come from Blackmagic's release documentation (the build whose shipped
187
+ # Developer/Scripting README first carried the method), not from a live bisect
188
+ # by this project, so an agent relaying one should say "documented as requiring
189
+ # 21.0.4+", never "measured absent on your build". A floor being wrong in the
190
+ # vendor's direction is survivable — the call-site guard still probes the live
191
+ # object before refusing, so the floor only ever supplies the *explanation*.
192
+ #
193
+ # tests/test_version_gate_drift.py fails when a call site and this table
194
+ # disagree, which is what keeps the two from drifting apart again.
195
+
196
+ CODE_FLOORS: Dict[str, str] = {
197
+ "MediaPoolItem.LinkProxyMedia": "17.0",
198
+ "MediaPoolItem.LinkFullResolutionMedia": "20.0",
199
+ "MediaPoolItem.MonitorGrowingFile": "20.0",
200
+ "MediaPoolItem.ReplaceClipPreserveSubClip": "20.0",
201
+ "Timeline.GetVoiceIsolationState": "20.1",
202
+ "Timeline.SetVoiceIsolationState": "20.1",
203
+ "TimelineItem.GetVoiceIsolationState": "20.1",
204
+ "TimelineItem.SetVoiceIsolationState": "20.1",
205
+ "MediaPoolItem.SetName": "20.2",
206
+ "TimelineItem.SetName": "20.2",
207
+ "TimelineItem.ResetAllNodeColors": "20.2",
208
+ "Resolve.DisableBackgroundTasksForCurrentResolveSession": "21.0",
209
+ "Project.GenerateSpeech": "21.0",
210
+ "Project.ResetIntellisearchAnalysis": "21.0",
211
+ "MediaPoolItem.PerformAudioClassification": "21.0",
212
+ "MediaPoolItem.ClearAudioClassification": "21.0",
213
+ "MediaPoolItem.AnalyzeForIntellisearch": "21.0",
214
+ "MediaPoolItem.AnalyzeForSlate": "21.0",
215
+ "MediaPoolItem.RemoveMotionBlur": "21.0",
216
+ "Folder.PerformAudioClassification": "21.0",
217
+ "Folder.ClearAudioClassification": "21.0",
218
+ "Folder.AnalyzeForIntellisearch": "21.0",
219
+ "Folder.AnalyzeForSlate": "21.0",
220
+ "Folder.RemoveMotionBlur": "21.0",
221
+ "Resolve.GetLayoutPresetList": "21.0.4",
222
+ "Resolve.GetBurnInPresetList": "21.0.4",
223
+ "Resolve.DeleteBurnInPreset": "21.0.4",
224
+ "Resolve.GetUserPreferencesPresetList": "21.0.4",
225
+ "Resolve.SaveUserPreferencesPreset": "21.0.4",
226
+ "Resolve.LoadUserPreferencesPreset": "21.0.4",
227
+ "Resolve.DeleteUserPreferencesPreset": "21.0.4",
228
+ "Resolve.ImportUserPreferencesPreset": "21.0.4",
229
+ "Resolve.ExportUserPreferencesPreset": "21.0.4",
230
+ "ProjectManager.GetProjectAttributesInCurrentFolder": "21.0.4",
231
+ }
232
+
233
+ #: Methods whose call-site guard names no usable floor. Recorded rather than
234
+ #: silently dropped, because "we gate this and cannot say since when" is itself
235
+ #: worth an agent knowing — it is a candidate for the next live bisect.
236
+ UNDATED_FLOORS: Tuple[str, ...] = ("Project.GetRenderSettings",)
237
+
238
+ _EVIDENCE_SYMBOLS = {gate["symbol"] for gate in _EVIDENCE_GATES}
239
+
240
+ VERSION_GATES: List[Dict[str, Any]] = _EVIDENCE_GATES + [
241
+ {
242
+ "symbol": symbol,
243
+ "introduced_in": floor,
244
+ "source": "documented",
245
+ "note": (
246
+ "Floor enforced at this server's call site, from Blackmagic's "
247
+ "release documentation rather than a live bisect here. The guard "
248
+ "probes the live object first, so this supplies the explanation, "
249
+ "not the refusal."
250
+ ),
251
+ "issue": 132,
252
+ }
253
+ for symbol, floor in sorted(CODE_FLOORS.items())
254
+ # An evidence-backed entry always wins: it says how we know, and these
255
+ # would overwrite that with the weaker label.
256
+ if symbol not in _EVIDENCE_SYMBOLS
257
+ ]
258
+
169
259
  _GATES_BY_SYMBOL = {gate["symbol"].lower(): gate for gate in VERSION_GATES}
170
260
 
171
261
  AVAILABLE = "available"
@@ -174,12 +264,24 @@ UNKNOWN = "unknown"
174
264
 
175
265
 
176
266
  def gate_for(symbol: str) -> Optional[Dict[str, Any]]:
177
- """The recorded gate for `symbol`, matched loosely. None if we have none."""
267
+ """The recorded gate for `symbol`, matched loosely. None if we have none.
268
+
269
+ Tried in order — exact, then bare method name, then substring — because the
270
+ registry now carries the same method on several classes (`SetName` is gated
271
+ on four). Iteration order decided which one a bare `"SetName"` lookup hit
272
+ when the registry was small enough for that not to matter; it now is not.
273
+ A bare name matching several classes only stays honest while their floors
274
+ agree, which `test_version_gate_drift` asserts.
275
+ """
178
276
  if not symbol:
179
277
  return None
180
278
  needle = symbol.strip().lower()
181
279
  if needle in _GATES_BY_SYMBOL:
182
280
  return _GATES_BY_SYMBOL[needle]
281
+ suffix = "." + needle
282
+ for key, gate in _GATES_BY_SYMBOL.items():
283
+ if key.endswith(suffix):
284
+ return gate
183
285
  for key, gate in _GATES_BY_SYMBOL.items():
184
286
  if needle in key or key in needle:
185
287
  return gate
@@ -17,6 +17,8 @@ import platform
17
17
  import subprocess
18
18
  import sys
19
19
  import tempfile
20
+
21
+ from src.utils.resolve_probe import has_method
20
22
  import time
21
23
  import types
22
24
  import traceback
@@ -153,7 +155,7 @@ def _frame_int(value) -> int:
153
155
 
154
156
 
155
157
  def _source_start(item) -> int:
156
- if hasattr(item, "GetSourceStartFrame"):
158
+ if has_method(item, "GetSourceStartFrame"):
157
159
  try:
158
160
  value = item.GetSourceStartFrame()
159
161
  if value is not None:
@@ -30,6 +30,8 @@ import os
30
30
  import re
31
31
  import time
32
32
  import uuid
33
+
34
+ from src.utils.resolve_probe import has_method
33
35
  from typing import Any, Dict, List, Optional, Tuple
34
36
 
35
37
  from src.utils import actor_identity, timeline_brain_db
@@ -280,12 +282,12 @@ def _resolve_media_pool_item_id(item: Any) -> Optional[str]:
280
282
  """
281
283
  try:
282
284
  mpi = item.GetMediaPoolItem()
283
- if mpi is not None and hasattr(mpi, "GetUniqueId"):
285
+ if has_method(mpi, "GetUniqueId"):
284
286
  return str(mpi.GetUniqueId())
285
287
  except Exception:
286
288
  pass
287
289
  try:
288
- return str(item.GetUniqueId()) if hasattr(item, "GetUniqueId") else None
290
+ return str(item.GetUniqueId()) if has_method(item, "GetUniqueId") else None
289
291
  except Exception:
290
292
  return None
291
293