davinci-resolve-mcp 2.98.7 → 2.99.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.
@@ -427,23 +427,21 @@ API_TRUTH: List[Dict[str, Any]] = [
427
427
  "suppresses the parameter-change invalidation that a value "
428
428
  "write depends on. Lock() is widely recommended for "
429
429
  "batching Fusion edits, which is how this reaches "
430
- "production code. PRECONDITION (2026-08-22): the "
431
- "suppression only reproduces on a graph that was BUILT "
432
- "through lock-wrapped AddTool/ConnectInput calls. The same "
430
+ "production code. MECHANISM (settled 2026-08-22). "
431
+ "PRECONDITION: it only reproduces on a comp whose graph was "
432
+ "BUILT through lock-wrapped AddTool/ConnectInput; the same "
433
433
  "locked write against a graph wired by plain attribute "
434
- "assignment renders normally, so 'a lock around a value "
435
- "write' is necessary but not sufficient — the comp has to "
436
- "have been through prior lock cycles. RESCUERS, each "
437
- "verified by rendering: Comp.EndUndo() after the locked "
438
- "write restores it, and so does any subsequent UNLOCKED "
439
- "value write. NOT rescuers: a structural ConnectInput "
440
- "inside the same lock, and a GetInput readback after the "
441
- "Unlock — both still render the stale frame. This explains "
442
- "why undo-wrapped bulk writes escape the bug; two further "
443
- "call shapes (a mask whose inputs are written right after "
444
- "AddTool, and a StyledText string write) escape it for "
445
- "reasons still unidentified. Keep value writes outside the "
446
- "lock everywhere rather than relying on an escape.",
434
+ "assignment renders normally. TRIGGER: the locked write is "
435
+ "lost when it is the FIRST value write to that comp since "
436
+ "the build. PRIMING: any unlocked value write anywhere in "
437
+ "the comp clears the condition permanently — even writing a "
438
+ "DEFAULT value to an unrelated tool — and so does "
439
+ "StartUndo/EndUndo around the write. A structural "
440
+ "ConnectInput inside the same lock does NOT clear it, and "
441
+ "neither does a GetInput readback. Priming makes false "
442
+ "negatives easy: a test that sets anything up with a plain "
443
+ "SetInput before the call under test will pass even with the "
444
+ "bug present.",
447
445
  "recommended": "Never hold a comp lock across a value write. Lock only "
448
446
  "structural work (AddTool/ConnectInput) and set inputs "
449
447
  "outside it. Because every readback the API offers "
@@ -497,7 +495,19 @@ API_TRUTH: List[Dict[str, Any]] = [
497
495
  "NOT this entry at all but the Composition.Lock bug above "
498
496
  "— a parameter written under a comp lock reads back "
499
497
  "correctly and never reaches the render, which looks "
500
- "identical from the API side.",
498
+ "identical from the API side. That is what an independent "
499
+ "2026-08-20 report on Studio 21.0.4.5 was measuring: a "
500
+ "wired MediaIn -> Blur -> MediaOut comp, and a Transform "
501
+ "variant, both delivered renders bit-identical to the "
502
+ "no-comp baseline (PSNR inf) when built and set through "
503
+ "this server. It was read at the time as a version "
504
+ "regression (19.1.3.7 honours a wired comp, 21.0.4.5 does "
505
+ "not); with the lock bug identified, the simpler reading is "
506
+ "that the same defect reproduces on 21.0.4.5 — and since "
507
+ "the lock bug has only been isolated on 19.1.3.7, that "
508
+ "report is the only evidence it is not build-specific. "
509
+ "Treat 'a wired comp renders' as established for 19.1.3.7 "
510
+ "and unverified elsewhere.",
501
511
  "recommended": "Wire the graph so MediaOut descends from MediaIn — that "
502
512
  "is the difference between a comp that renders and one "
503
513
  "that is silently bypassed, and it is what made this look "
@@ -511,8 +521,16 @@ API_TRUTH: List[Dict[str, Any]] = [
511
521
  "chosen from the API (see the Track Selector entry), so "
512
522
  "overlaying onto an existing clip's track is not currently "
513
523
  "reachable end-to-end. Building the comp in the Fusion page "
514
- "UI works; only the API-created comp is ignored.",
515
- "tags": ["fusion", "silent-failure", "render"],
524
+ "UI works; only the API-created comp is ignored. "
525
+ "Never claim a Fusion effect from comp readback alone — "
526
+ "prove it with a rendered frame (gallery_stills "
527
+ "grab_and_export before/after, or a delivered render). On "
528
+ "setups where even the wired comp does not render (see the "
529
+ "2026-08-20 measurement), treat per-item Fusion effects as "
530
+ "HITL (a human builds or activates the comp in the UI) and "
531
+ "bake stills motion with ffmpeg when unattended output is "
532
+ "required.",
533
+ "tags": ["fusion", "silent-failure", "render", "readback", "hitl"],
516
534
  "submit": "bug",
517
535
  },
518
536
  {
@@ -2146,6 +2164,69 @@ API_TRUTH: List[Dict[str, Any]] = [
2146
2164
  "tags": ["gallery", "stills", "headless", "unreliable-return"],
2147
2165
  "submit": "bug",
2148
2166
  },
2167
+ {
2168
+ "symbol": "MediaPool.AppendToTimeline (null-id timeline items)",
2169
+ "object": "MediaPool",
2170
+ "signature": "([{clipInfo}, ...]) -> [TimelineItem]",
2171
+ "reality": "The returned TimelineItem objects can have an unreadable/empty "
2172
+ "GetUniqueId, notably when the clipInfo recordFrame lands in a "
2173
+ "span still occupied by another item (any duplicate-then-delete "
2174
+ "'move' whose offset is smaller than the item duration hits "
2175
+ "this). The item may not actually exist on the timeline. A "
2176
+ "session that trusted the non-empty return and deleted the "
2177
+ "sources lost 26 clips (Portugal timeline, 2026-08-19).",
2178
+ "recommended": "Never treat AppendToTimeline's return as proof of "
2179
+ "placement. Re-enumerate the track and match on record "
2180
+ "frame + duration before any dependent delete; never "
2181
+ "shift items by duplicate-then-delete into occupied "
2182
+ "spans — use timeline.ripple_insert, which rebuilds the "
2183
+ "tail into free space and verifies by readback.",
2184
+ "tags": ["editorial", "silent-failure", "unreliable-return", "timeline"],
2185
+ "submit": "bug",
2186
+ "mitigation": ["_append_and_recover_timeline_item duplicate_verified gate",
2187
+ "_timeline_ripple_insert_impl"],
2188
+ },
2189
+ {
2190
+ "symbol": "TimelineItem.CreateMagicMask (needs operator clicks)",
2191
+ "object": "TimelineItem",
2192
+ "signature": "(mode) -> bool",
2193
+ "reality": "Returns False when the item carries no Magic Mask clicks. "
2194
+ "Magic Mask v2 is click-driven (manual ch. 139; strokes are "
2195
+ "the legacy v1 interface) and the scripting API has no way to "
2196
+ "place a click, so on a fresh item the call can never isolate "
2197
+ "anything — it only tracks a mask the operator already seeded. "
2198
+ "Mode strings are 'F', 'B', 'BI'; long spellings like "
2199
+ "'Forward' are rejected.",
2200
+ "recommended": "Treat CreateMagicMask as track-only: have the operator "
2201
+ "click the subject (Color page > Magic Mask palette) and "
2202
+ "then call it, or surface the HITL steps instead of a "
2203
+ "bare False. Verify isolation with a rendered frame "
2204
+ "(gallery_stills grab_and_export), never a thumbnail.",
2205
+ "tags": ["ai", "magic-mask", "hitl", "silent-failure"],
2206
+ "submit": "missing",
2207
+ "mitigation": ["_magic_mask_hitl_result"],
2208
+ },
2209
+ {
2210
+ "symbol": "TimelineItem.SetCDL (write-only, no GetCDL)",
2211
+ "object": "TimelineItem",
2212
+ "signature": "({NodeIndex, Slope, Offset, Power, Saturation}) -> bool",
2213
+ "reality": "There is no GetCDL, so applied CDL values cannot be read "
2214
+ "back; the bool is the only signal and it returns False "
2215
+ "with no reason (missing node, still/generator item, values "
2216
+ "silently rejected). NodeIndex is 1-based (README line 6) "
2217
+ "and must not exceed Graph.GetNumNodes() — note "
2218
+ "TimelineItem.GetNumNodes is deprecated; the count lives on "
2219
+ "item.GetNodeGraph().",
2220
+ "recommended": "Read item.GetNodeGraph().GetNumNodes() before SetCDL "
2221
+ "and diagnose a False against the node count and clip "
2222
+ "type. Prove the applied look with a rendered frame "
2223
+ "(gallery_stills grab_and_export or "
2224
+ "Project.ExportCurrentFrameAsStill), not the return "
2225
+ "value.",
2226
+ "tags": ["color", "cdl", "readback", "silent-failure"],
2227
+ "submit": "missing",
2228
+ "mitigation": ["_cdl_node_preflight", "_cdl_failure_diagnosis"],
2229
+ },
2149
2230
  ]
2150
2231
 
2151
2232
 
@@ -78,6 +78,7 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
78
78
  "move_clips",
79
79
  "duplicate_clips",
80
80
  "copy_clips",
81
+ "ripple_insert",
81
82
  "copy_range",
82
83
  "duplicate_range",
83
84
  "overwrite_range",
@@ -225,6 +226,31 @@ STRICT_DEFAULT_ACTIONS: frozenset = frozenset({
225
226
  })
226
227
 
227
228
 
229
+ # ── Plan-only (dry-run) actions ─────────────────────────────────────────────
230
+ #
231
+ # Actions whose payload can declare the call non-mutating. `ripple_insert` is
232
+ # the only destructive action that DEFAULTS to a plan-only call, so without
233
+ # this every routine dry-run plan archives a timeline version — the same waste
234
+ # the pending-confirm skip (F4) exists to prevent, except the pending-confirm
235
+ # skip only fires while the confirm-token preference is ON. With it off, the
236
+ # read-only path of the action littered the version chain.
237
+
238
+ DRY_RUN_DEFAULT_TRUE_ACTIONS: frozenset = frozenset({
239
+ ("timeline", "ripple_insert"),
240
+ })
241
+
242
+
243
+ def _payload_is_plan_only(
244
+ tool_name: str, action: str, params: Optional[Dict[str, Any]],
245
+ ) -> bool:
246
+ """True iff this call only produces a plan and mutates nothing."""
247
+ if (tool_name, action) not in DRY_RUN_DEFAULT_TRUE_ACTIONS:
248
+ return False
249
+ if not isinstance(params, dict):
250
+ return True # dry_run defaults to True for these actions
251
+ return bool(params.get("dry_run", params.get("dryRun", True)))
252
+
253
+
228
254
  def _payload_only_touches_no_archive_keys(
229
255
  tool_name: str, action: str, params: Optional[Dict[str, Any]],
230
256
  ) -> bool:
@@ -249,6 +275,8 @@ def is_destructive(tool_name: str, action: str, params: Optional[Dict[str, Any]]
249
275
  return False
250
276
  if _payload_only_touches_no_archive_keys(tool_name, action, params):
251
277
  return False
278
+ if _payload_is_plan_only(tool_name, action, params):
279
+ return False
252
280
  return True
253
281
 
254
282