davinci-resolve-mcp 2.78.0 → 2.79.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,128 @@
2
2
 
3
3
  Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
4
4
 
5
+ ## What's New in v2.79.0
6
+
7
+ Six silent failures in the conform path, found by conforming a real Avid picture turnover
8
+ and cross-checking every step against Resolve's own behaviour on 19.1.3. The common shape:
9
+ a call that reports success, or reports nothing at all, and leaves you with a timeline you
10
+ believe is right.
11
+
12
+ ### Fixed
13
+
14
+ - **`convert_to_interchange` promised speed survives the DRT target. It does not.** The DRT
15
+ spec builder read four fields per event — start, duration, in, mediaFilePath — and never
16
+ read `speed` or `reverse`. One level down is why: the DRT clip schema has no per-clip speed
17
+ field at all, so there is nothing to write a retime into. A 200% clip and a reversed clip
18
+ both landed at 100% forward and the caller was told the conversion succeeded — inside the
19
+ module whose own description opens by naming "flattened retime → flag, skip-not-fake" as
20
+ its guarantee. Since carrying speed through was not available, the `drt` target now returns
21
+ **`flattened`**: one entry per retimed or reversed event, with its index, recIn, speed,
22
+ reverse, source and reason. It is always an array (empty when there are no retimes), so a
23
+ caller can tell "none to lose" from "this build is too old to report". `otio` (LinearTimeWarp)
24
+ and `edl` (M2) still carry retimes and are now named as the targets to use for a cut that
25
+ has them.
26
+ - **The vendored FCP7 emitter produced XML that Resolve 19.1.3 imports as nothing.** Three
27
+ defects, all failing the same silent way — a clean-looking file and a Resolve that does
28
+ nothing. (1) The file def had no `<timecode>` element; Resolve rejects the ENTIRE import
29
+ over one absent block, not the one clip. (2) `<pathurl>` got XML escaping where it needed
30
+ URL escaping, now percent-encoded per segment so `/` separators survive. (3) The sequence
31
+ had no `<rate>`, which bisection showed was the single element standing between "no
32
+ timeline" and a working import. Because a *wrong* timecode block is as fatal as a missing
33
+ one, the emitter never guesses: clips whose media timecode is unknown emit no block and are
34
+ reported through `fcp7TimecodeCoverage()`, which `buildPackage` attaches as `fcp7Timecode`
35
+ with an `importable` flag.
36
+ - **Importing a timeline into the never-saved `Untitled Project` silently no-opped.** Resolve
37
+ accepted the call, created nothing, and named no cause; the generic "Resolve created no
38
+ timeline" error that came back pointed at missing media and `sanitize_media` — the wrong
39
+ road, because the file is fine. `import_timeline_checked` now refuses before the call with
40
+ a remediation naming the project state. **Behaviour change:** a call that previously
41
+ returned a generic error now refuses earlier with a different message. The refusal is hard
42
+ and has no override flag, because the call cannot succeed either way.
43
+ - **OTIO authored by `convert_to_interchange` would not import into Resolve.** Filed as a
44
+ scripting-API limitation; it was not one. Exporting a timeline with `EXPORT_OTIO` and
45
+ feeding Resolve's own file back proved the API imports OTIO fine — what it refuses is a
46
+ document that is valid OTIO but not Resolve-shaped. The decisive requirement is that source
47
+ frames be **timecode-absolute**: media starting at 01:00:00:00 has an available range at
48
+ frame 86400, and 0-based source offsets put the clip outside it. The emitter now mirrors
49
+ Resolve's own shape (Clip.2 with a `media_references` map, `available_range`, bare
50
+ `target_url`, `global_start_time`) and takes each event's origin via `mediaStartTcFrame` or
51
+ an absolute `srcTcFrame`. Events whose origin had to be assumed come back in
52
+ **`mediaOriginAssumed`** rather than producing a file that imports as nothing.
53
+ - **`.otio` failures were misdiagnosed as missing media.** A `.otio` is JSON, so the
54
+ sanitize/relink pass cannot parse it and its advice never applied. `sanitize_media` is now
55
+ N/A for `.otio` as it already was for `.aaf`, and the no-timeline remediation names the
56
+ document's shape and the frame origin instead.
57
+
58
+ ### Documentation
59
+
60
+ - `api_truth`: the retime entry now records that **the read side is as dead as the write
61
+ side** — `GetProperty('Speed'|'PlaybackSpeed'|'RetimeSpeed'|'ClipSpeed')` all return None on
62
+ 19.1.3.7, and the keyless property dict carries no speed value at all. It also records that
63
+ the interchange route is closed (scalar speed filter ignored, `graphdict` dead in four
64
+ shapes, `reverse` dropped, in↔pproTicks inconsistency rejected in both orientations) and the
65
+ trap that makes it expensive to find: Resolve's own FCP7 export writes a degenerate Time
66
+ Remap, so `EXPORT_FCP_7_XML` cannot witness a speed. Each claim now says which Resolve
67
+ version it was measured on.
68
+ - `api_truth`: **`AppendToTimeline` does not overwrite an overlapping record** — the earlier
69
+ item wins and the later append is dropped, leaving a silently short timeline.
70
+ - `api_truth`: **placements from an errored append chunk are not durable across a save** — a
71
+ timeline verified at 573 items held 500 afterward. Every in-session read agrees with the
72
+ wrong number; only a post-save read catches it.
73
+ - `api_truth`: `CreateTimelineFromClips`' clipInfo has **no track field** while
74
+ `AppendToTimeline`'s does, with the empty-timeline + `add_track` + per-clip append workaround.
75
+ - `api_truth`: what Resolve's OTIO importer actually requires, and how to debug a refusal by
76
+ diffing against an `EXPORT_OTIO` file.
77
+ - **New guide — `docs/guides/conforming-an-avid-aaf.md`.** Three Resolve-native mechanisms
78
+ measured against one real turnover; all three fail. The most convincing-looking one fails
79
+ worst: "Link to source camera files" links 878 of 882 items with **only 144 correct — 734
80
+ wrong takes, 84%** — and then renders as a fully conformed timeline, with no offline media
81
+ and no warning. The guide explains why the matching lands on adjacent takes and says plainly
82
+ that the only witness which catches it is a frame comparison against a reference.
83
+
84
+ ### Validation
85
+
86
+ - 2417 Python unit tests, 744 Node advanced tests, plus 8 fixture-free packaging tests, all
87
+ green. `packaging.test.js` also no longer fails to load on a fresh clone: it read a
88
+ git-ignored fixture at module scope, which took the fixture-free tests down with it.
89
+ - Live-validated on DaVinci Resolve Studio 19.1.3.7 with synthetic media: the fixed FCP7 XML
90
+ imports 2 items / 2 linked where the same file with its timecode blocks stripped imports
91
+ nothing; the authored `.otio` imports 3 items / 3 linked and re-reads frame-exact.
92
+
93
+ ## What's New in v2.78.1
94
+
95
+ `Timeline.DeleteClips` is page-gated. Contributed by
96
+ [@billcarroll](https://github.com/billcarroll) in
97
+ [#117](https://github.com/samuelgursky/davinci-resolve-mcp/pull/117).
98
+
99
+ ### Fixed
100
+
101
+ - **Every delete-capable action failed whenever the UI was left on another page.**
102
+ `Timeline.DeleteClips` deterministically returns False and deletes nothing off
103
+ the Edit page (verified: Fairlight), retries included — the readback-and-retry
104
+ helper from v2.71.1 handles a *flaky* False correctly, but retrying cannot clear
105
+ a page gate. Live verification on Studio 21.0: three identical retries against
106
+ 132 valid, unlocked, present items all returned False with all 132 still on the
107
+ track; one `OpenPage("edit")` and the same call returned True and left 0. Track
108
+ lock and enable state were clear throughout — a page gate, not a lock.
109
+ `timeline` `delete_clips` / `move_clips` / `overwrite_range` / `lift_range` /
110
+ `apply_cuts` and `edit_engine execute_swap` now hold the Edit page for the call
111
+ and restore the caller's page after.
112
+
113
+ The guard is serialized through `page_lock`, alongside the existing Color-page
114
+ guard for thumbnail capture: Resolve has one globally-active page, so an
115
+ unserialized switch-work-restore races every other page-switching operation, and
116
+ under the threaded dispatch from v2.62.0 a concurrent thumbnail capture flipping
117
+ to Color mid-delete would land the delete on the wrong page — reintroducing this
118
+ same bug. `apply_cuts` holds the guard once around its loop rather than per cut,
119
+ which would otherwise cost 2N page flips for N cuts.
120
+
121
+ The `api_truth` entry is restructured into the two distinct failures now known to
122
+ share this call: wrong page (deterministic, mechanism identified, verified) and
123
+ flaky first attempt (one observation, cause not established — and the entry now
124
+ records that the page state at the time was not captured, so it cannot be ruled in
125
+ or out as the first failure in disguise).
126
+
5
127
  ## What's New in v2.78.0
6
128
 
7
129
  The AAF probe reports where in the *source* an event actually lives, not where it
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # DaVinci Resolve MCP Server
2
2
 
3
- [![Version](https://img.shields.io/badge/version-2.78.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.79.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
4
4
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
5
5
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
6
6
  [![Tools](https://img.shields.io/badge/MCP%20Tools-34%20(341%20full)-blue.svg)](#server-modes)
@@ -273,6 +273,7 @@ For method-by-method status, see [API Coverage and Test Results](docs/reference/
273
273
  | [Media Analysis Guide](docs/guides/media-analysis-guide.md) | Source-safe FFprobe, FFmpeg, Whisper, sidecar, and analysis-root workflows |
274
274
  | [Multicam Setup Helper Guide](docs/guides/multicam-setup-guide.md) | Stacked timeline prep, helper/API boundary, and Resolve UI conversion steps |
275
275
  | [Editorial Decision Guide](docs/guides/editorial-decision-guide.md) | Project-owned editorial craft guidance for analysis and timeline decisions |
276
+ | [Conforming an Avid AAF](docs/guides/conforming-an-avid-aaf.md) | Why all three Resolve-native routes fail on a consolidated turnover, and which one is dangerous |
276
277
  | [Color Decision Guide](docs/guides/color-decision-guide.md) | Project-owned color correction guidance and Resolve color API boundaries |
277
278
  | [Contributing and Project Layout](docs/contributing.md) | Contribution workflow, platform support, security notes, repository structure |
278
279
  | [Security Policy](SECURITY.md) | Local stdio trust boundary, tool metadata, confirmation guidance, reporting |
@@ -0,0 +1,91 @@
1
+ # Conforming a consolidated Avid AAF: what Resolve can and cannot do for you
2
+
3
+ You have an Avid picture turnover — an AAF plus consolidated media — and a folder
4
+ of camera originals. You want Resolve to relink the cut to those originals.
5
+
6
+ Three Resolve-native mechanisms look like they will do this. All three were
7
+ measured against one real 83-minute turnover (878 events, 882 timeline items,
8
+ multi-layer, consolidated media) on Resolve 19.1.3. **All three fail, and the one
9
+ that looks most convincing fails worst.**
10
+
11
+ This guide is about Resolve's own behaviour. It is reproducible by anyone with an
12
+ Avid AAF and a folder of camera originals.
13
+
14
+ ## The short answer
15
+
16
+ | mechanism | what it does | verdict |
17
+ | --- | --- | --- |
18
+ | API `ImportTimelineFromFile` | `importSourceClips: true` creates **no timeline** at all. `importSourceClips: false` creates 882 items with **zero** media-pool items | unusable as a conform |
19
+ | **Reconform from Bins** (timecode-only) | **678 / 882 linked** — and the links are the **wrong takes** | silently wrong |
20
+ | **UI Import AAF + "Link to source camera files"** | **878 / 882 linked, only 144 correct — 734 wrong takes (84%)** | 🚨 **dangerous** |
21
+
22
+ The camera-file link is the option whose name promises exactly what you want. It
23
+ is the one to avoid.
24
+
25
+ ## Why the camera-file link is the dangerous option
26
+
27
+ It links almost everything — 878 of 882 items — and **84% of those links are the
28
+ wrong take.** Four consecutive cuts pointed at the same wrong file. A head slate
29
+ linked to unrelated archival footage.
30
+
31
+ And then it **renders as a fully conformed timeline.** There is no offline media,
32
+ no red frames, no error, no warning. Every signal a conform is normally checked
33
+ against says the job is done. You have to compare against a picture reference,
34
+ frame by frame, to discover that most of the cut is showing the wrong moment.
35
+
36
+ A conform that is 16% correct and looks 100% correct is worse than one that
37
+ obviously fails, because the failure survives review.
38
+
39
+ ## Why the matching goes wrong
40
+
41
+ Two reasons, and neither is a bug you can configure away.
42
+
43
+ **Name matching is impossible.** A consolidated Avid turnover names its clips
44
+ with instance names like `<something>.new.01` — names that belong to Avid's own
45
+ media database and match no file on disk. There is nothing to match on.
46
+
47
+ **Timecode alone is ambiguous.** Fall back to timecode and you get adjacent-take
48
+ drift: the matcher lands on a *neighbouring take from the same roll* — one
49
+ camera-roll index away from the right one. Those takes were often shot minutes
50
+ apart, overlap in timecode, and are visually similar enough to pass a glance.
51
+ That is the shape of the 678 Reconform-from-Bins links and the 734 wrong
52
+ camera-file links alike.
53
+
54
+ Adjacent takes are the worst possible failure mode: close enough to look
55
+ plausible, wrong enough to be unusable.
56
+
57
+ ## What to do instead
58
+
59
+ **Conform against the consolidated media the AAF actually references, not against
60
+ camera originals.** The AAF's source references describe the consolidated
61
+ fragments; those match. Relinking to camera originals is a *second*, separate
62
+ problem — one that needs the source position and source timecode of each event,
63
+ not just its take name.
64
+
65
+ If you must reach the camera originals, you need per-event evidence beyond the
66
+ AAF's own clip names:
67
+
68
+ - the **physical source position and timecode** of each cut (`aaf_probe.py` emits
69
+ `srcPos`, `srcTcFrame`, `srcTc`, `srcTcFps`, `srcTcDrop` per event — a
70
+ fragment-relative source offset is not a position in the take, which is its own
71
+ well-documented trap);
72
+ - the sequence **start timecode** (`startTimecode` / `startFrame`), because a cut
73
+ built at Resolve's 01:00:00:00 default when the AAF starts at 00:59:50:00 is ten
74
+ seconds out everywhere and only visible against a linked reference;
75
+ - an independent **picture reference** to verify against.
76
+
77
+ **Verify against a reference, always.** Whatever route you take, the only witness
78
+ that catches adjacent-take drift is a frame comparison against a reference render
79
+ or picture reference. Item counts, "linked" counts, and the absence of offline
80
+ media all agree with a conform that is 84% wrong. A witness derived from the same
81
+ state as the thing it is checking cannot contradict it.
82
+
83
+ ## Related
84
+
85
+ - `docs/reference/api-limitations.md` — the scripting-API entries behind this,
86
+ including `MediaPool.ImportTimelineFromFile` and the `AppendToTimeline`
87
+ placement and durability limits you will hit if you build the timeline
88
+ yourself.
89
+ - `docs/guides/headless-edit-loop.md` — which interchange formats relink at all
90
+ when media has moved (DRT, OTIO and EDL do not).
91
+ </content>
@@ -12,7 +12,7 @@ that none exists).
12
12
 
13
13
  **Verified on:** DaVinci Resolve Studio 21.0.2
14
14
 
15
- **Totals:** 25 missing capabilities, 25 bugs / unreliable behaviors.
15
+ **Totals:** 25 missing capabilities, 28 bugs / unreliable behaviors.
16
16
 
17
17
  The authoritative source is the runtime-queryable `api_truth` ledger
18
18
  (`resolve_control api_truth "<query>"`); this document is generated from
@@ -117,9 +117,9 @@ equivalent, blocking full automation.
117
117
  ### Clip speed / retime ratio and speed ramps
118
118
 
119
119
  - **Object:** `TimelineItem`
120
- - **Behavior:** SetProperty exposes only retime *quality* (RetimeProcess, MotionEstimation) and transform/crop/composite/opacity keys — not the speed value itself. There is no way to set a clip to a given % speed, reverse it, or author a speed ramp. Verified against the documented SetProperty key list AND by live mutating attempt on 21.0.0: SetProperty('Speed'|'PlaybackSpeed'|'RetimeSpeed'|'ClipSpeed', 50) all return False, while SetProperty('RetimeProcess', 1) returns True.
121
- - **Workaround / current handling:** Set clip speed/retime in the Resolve UI; no scripted equivalent exists.
122
- - **Tags:** missing-method, timeline, retime, speed
120
+ - **Behavior:** SetProperty exposes only retime *quality* (RetimeProcess, MotionEstimation) and transform/crop/composite/opacity keys — not the speed value itself. There is no way to set a clip to a given % speed, reverse it, or author a speed ramp. Verified against the documented SetProperty key list AND by live mutating attempt on 21.0.0: SetProperty('Speed'|'PlaybackSpeed'|'RetimeSpeed'|'ClipSpeed', 50) all return False, while SetProperty('RetimeProcess', 1) returns True. THE READ SIDE IS AS DEAD AS THE WRITE SIDE, which is easy to miss: re-measured on Studio 19.1.3.7 against a placed item, GetProperty('Speed'), GetProperty('PlaybackSpeed'), GetProperty('RetimeSpeed') and GetProperty('ClipSpeed') ALL return None, and the keyless GetProperty() dict (26 keys on that item) carries no speed value at all — its only retime key is RetimeProcess, which is quality, not ratio. SetProperty('Speed', 1.75) returned False on 19.1.3.7 too, so the write refusal is not specific to 21.0.0. Note the 21.0.0 stamp above covers the SetProperty measurements only. THE INTERCHANGE ROUTE IS ALSO CLOSED — carrying a retime in through FCP7 XML does not work either, measured on 19.1.3 with three real files, clips at 100/200/50% on V1, each variant imported as its own timeline: (a) the importer IGNORES the scalar Time Remap speed filter and the clips arrive at 100%; (b) `graphdict` is dead in FOUR separate shapes — the full Premiere form (variablespeed=0 + speed + reverse=FALSE + frameblending + 4-keyframe graphdict + FCPCurve), graphdict alone with no <speed> param, a 100% REVERSE (reverse=TRUE plus a descending graphdict, the exact shape a real Premiere export carries), and variablespeed=1 + graphdict — 0 of 2 retimes landed in every one; (c) `reverse` did not survive either; (d) any <in>/<pproTicksIn> inconsistency is silently REJECTED, measured in BOTH orientations. The emitted maps were verified constant-slope against real Premiere exports before importing, and every imported clip came back consuming exactly its record span of source (a 200% clip emitted in 200 / out 296 was clamped to out 248 — 48 source frames over a 48-frame record span). Placement is NOT the problem: the same route imported 573 clips with 572 of 573 matching by track and record position with source frames exact, and the importer BUILT a 59-frame dissolve. The retime gap is specific, not general. TRAP: Resolve's own FCP7 export cannot witness a speed. It writes a DEGENERATE Time Remap on every clip — `speed` value 0 (not 100) and a graphdict whose keyframe `value`s are all 0 while its `when`s carry the clip's source in/out — so anyone verifying a retime by round-tripping through EXPORT_FCP_7_XML is reading furniture, and the identity Time Remap blocks present on every clip are what make the route look like it should work.
121
+ - **Workaround / current handling:** Set clip speed/retime in the Resolve UI; no scripted equivalent exists, and no interchange route carries one in. Do NOT read speed back with GetProperty (None) or witness it via EXPORT_FCP_7_XML (degenerate). Read the clip's GEOMETRY instead — GetLeftOffset / GetRightOffset, which on the 200% clip read 200 / 248 and agreed with the export's in/out. Caveat worth stating: no positive control has been run — no clip KNOWN to be retimed has been read back through those two witnesses, because there is no scripting path to create one. So the geometry witness is the best available, not a proven one. Either way the interchange route is rejected: no retime was built, and none could have been verified.
122
+ - **Tags:** missing-method, timeline, retime, speed, interchange, silent-failure, unreliable-return
123
123
 
124
124
  ### Color node graph editing and primary grade values
125
125
 
@@ -281,7 +281,7 @@ values, or automation-hostile modal prompts.
281
281
  - **Object:** `MediaPool`
282
282
  - **Signature:** `(filePath, {importOptions}) -> Timeline`
283
283
  - **Behavior:** Returns None — no error, no exception — when the requested `timelineName` already exists. Measured: importing one file three times with the same name succeeded once and returned None twice. An iterative loop that reuses a fixed name therefore works exactly once and then silently does nothing. DRT ignores importOptions entirely (timelineName, importSourceClips and sourceClipsFolders are all invalid for it): the timeline is named after the FILE, repeats auto-uniquify ('iter', 'iter 2', 'iter 3'), and because importSourceClips cannot be disabled each DRT import adds another copy of the source media to the pool. OTIO is the one format that will NOT relink from the pool — with importSourceClips=False its timeline rebuilds with correct structure and every item OFFLINE, in both GUI and headless.
284
- - **Workaround / current handling:** For a repeatable loop use FCP7 XML or AAF with a UNIQUE timelineName per iteration plus importSourceClips=False and sourceClipsFolders=[root] — verified frame-exact over five consecutive imports with no media duplicated. Use DRT for one-shot hand-offs only. For OTIO, pass importSourceClips=True with a sourceClipsPath. See docs/guides/headless-edit-loop.md.
284
+ - **Workaround / current handling:** For a repeatable loop use FCP7 XML or AAF with a UNIQUE timelineName per iteration plus importSourceClips=False and sourceClipsFolders=[root] — verified frame-exact over five consecutive imports with no media duplicated. Use DRT for one-shot hand-offs only. For OTIO, pass importSourceClips=True with a sourceClipsPath. See docs/guides/headless-edit-loop.md. For a consolidated Avid AAF specifically, do NOT expect this call (or Reconform from Bins, or the UI's 'Link to source camera files') to conform it to camera originals — all three were measured against a real turnover and all three fail, the UI option linking 878 of 882 items with only 144 correct while rendering as a fully conformed timeline. See docs/guides/conforming-an-avid-aaf.md.
285
285
  - **Tags:** timeline, import, interchange, silent-failure, conform
286
286
 
287
287
  ### Timeline.Export(EXPORT_FCPXML_1_10)
@@ -295,9 +295,33 @@ values, or automation-hostile modal prompts.
295
295
  ### MediaPool.CreateTimelineFromClips
296
296
 
297
297
  - **Object:** `MediaPool`
298
- - **Behavior:** Fails with a bare 'Failed to create timeline from clip_infos' — naming nothing actionable — when the Media Pool's CURRENT folder is not the folder holding the clips, even though every clip_id passed is valid and resolvable. Reported against Resolve Studio in PR #99.
299
- - **Workaround / current handling:** Call MediaPool.SetCurrentFolder() to the clips' bin before creating the timeline (media_pool set_current_folder). Valid ids are not sufficient.
300
- - **Tags:** media-pool, timeline, unhelpful-error
298
+ - **Behavior:** Fails with a bare 'Failed to create timeline from clip_infos' — naming nothing actionable — when the Media Pool's CURRENT folder is not the folder holding the clips, even though every clip_id passed is valid and resolvable. Reported against Resolve Studio in PR #99. SEPARATELY, its clipInfo HAS NO TRACK FIELD: there is no way to say which video or audio track a clip should land on, so this call cannot build a multi-track timeline. The asymmetry is the surprise — MediaPool.AppendToTimeline's clipInfo DOES take `trackIndex`, so the two clipInfo shapes are not the same shape, and code that works against one silently loses track assignment against the other.
299
+ - **Workaround / current handling:** Call MediaPool.SetCurrentFolder() to the clips' bin before creating the timeline (media_pool set_current_folder). Valid ids are not sufficient. For multi-track placement do not use this call at all: create an EMPTY timeline (media_pool create_timeline), add the tracks you need (timeline add_track), then place each clip with MediaPool.AppendToTimeline passing clipInfo `trackIndex` (media_pool append_to_timeline with track_index).
300
+ - **Tags:** media-pool, timeline, unhelpful-error, missing-method, conform
301
+
302
+ ### MediaPool.AppendToTimeline (overlapping records — earlier item wins)
303
+
304
+ - **Object:** `MediaPool`
305
+ - **Signature:** `([{mediaPoolItem, startFrame, endFrame, recordFrame, trackIndex, mediaType}]) -> [TimelineItem]`
306
+ - **Behavior:** AppendToTimeline does NOT overwrite an overlapping record. When two clipInfos resolve to record ranges that overlap on the same track, the EARLIER item wins and the later append is dropped — measured while placing a real turnover. There is no overwrite edit mode to reach for either: AppendToTimeline is the only programmatic placement the API offers, so a consumer that assumes overwrite semantics gets a timeline that is silently SHORT by the number of colliding events, with no error to say which ones lost.
307
+ - **Workaround / current handling:** Resolve collisions BEFORE appending — the API will not do it for you. Cap each event's placed duration at the next event's recordFrame on the same track, or place the colliding events on separate tracks. Verify by comparing the timeline's item count against the number of clipInfos you sent, per track, and finish with detect_gaps_overlaps; a count that matches is the only evidence nothing was dropped.
308
+ - **Tags:** timeline, edit, silent-failure, conform, media-pool
309
+
310
+ ### MediaPool.AppendToTimeline (errored-chunk placements are not durable across a save)
311
+
312
+ - **Object:** `MediaPool`
313
+ - **Signature:** `([clipInfos]) -> [TimelineItem]`
314
+ - **Behavior:** Placements made by an append call whose response ERRORED are not durable: they appear in the timeline, every in-session read agrees they are there, and THE SAVE DISCARDS THEM. Measured on a real turnover — a timeline verified at 573 items immediately after construction held 500 after the save, a loss of exactly one errored append chunk's worth. The API reported the failure, the items appeared anyway, and nothing between construction and the save disagreed with the wrong number. This is the dangerous shape: the witness is derived from the same unsaved state as the thing it is checking, so it cannot contradict it. Only a POST-SAVE read can.
315
+ - **Workaround / current handling:** Treat the save as the verification boundary, not the end of the job. Save, RE-READ the timeline's item count from the saved project, and re-append whatever is missing (bounded — two rounds is enough in practice); only then report success. A timeline that is still short after that, or that will not save, is a FAILURE — report it as one rather than returning the in-session count. Never claim a construction succeeded on a pre-save read alone, and treat any errored append chunk as suspect even when its items are visibly present.
316
+ - **Tags:** timeline, edit, silent-failure, data-loss, conform, unreliable-return, media-pool
317
+
318
+ ### MediaPool.ImportTimelineFromFile (.otio document shape)
319
+
320
+ - **Object:** `MediaPool`
321
+ - **Signature:** `(filePath, {importOptions}) -> Timeline`
322
+ - **Behavior:** Resolve DOES import OTIO through the scripting API — but only a Resolve-shaped document, and it rejects anything else by creating NO timeline and returning None, with no error naming a cause. Established on 19.1.3.7 by exporting a timeline with Timeline.Export(..., EXPORT_OTIO) and feeding Resolve's own file straight back: it re-imports cleanly (3 items, 3 linked), while a valid hand-authored OTIO of the same cut, same project, same session, same three online media files, produced nothing. So a refusal is a SHAPE problem, not a media problem. The requirement that actually decides it is the SOURCE FRAME ORIGIN: a clip's source_range must be expressed against the media's own timecode range. Media carrying an embedded start TC of 01:00:00:00 gets an available_range starting at frame 86400, and the clip's source_range must start there too — 0-based source offsets, the natural reading of 'source in point', put the clip outside the media's real range and the import dies. Isolated by bisection: 0-based fails and absolute succeeds whether or not the stack/track source_range is null. Resolve also writes (and expects) Clip.2 with a `media_references` MAP plus `active_media_reference_key`, not Clip.1 with a singular `media_reference`; an `available_range` on each reference; a BARE path in `target_url`, not a file:// URL; `enabled` and `metadata` throughout; `global_start_time`; and track names in its own form ('Video 1').
323
+ - **Workaround / current handling:** Author OTIO for Resolve by mirroring what Resolve itself exports, and give every event its media timecode origin. editorial.convert_to_interchange (target 'otio') does this and reports any event whose origin had to be assumed in `mediaOriginAssumed` — a non-empty list means the file will only import if that media really starts at 00:00:00:00. To debug a refusal, export any timeline with EXPORT_OTIO and diff your document against it; do NOT chase missing media or reach for sanitize_media, which cannot even parse a .otio (it is JSON, not XML).
324
+ - **Tags:** timeline, import, interchange, otio, silent-failure, conform
301
325
 
302
326
  ### TimelineItem.AddFusionComp / LoadFusionCompByName
303
327
 
@@ -374,13 +398,17 @@ values, or automation-hostile modal prompts.
374
398
  - **Reference:** [issue #77](https://github.com/samuelgursky/davinci-resolve-mcp/issues/77)
375
399
  - **Tags:** unreliable-return, silent-failure, metadata, reel-name
376
400
 
377
- ### Timeline.DeleteClips (flaky first attempt)
401
+ ### Timeline.DeleteClips (requires the Edit page; flaky first attempt)
378
402
 
379
403
  - **Object:** `Timeline`
380
404
  - **Signature:** `([TimelineItem], ripple) -> bool`
381
- - **Behavior:** Can return False on the first call even when every item in the list is a valid, present TimelineItem; an identical immediate retry succeeds. Observed once, on Studio 21.0 during a cut-video edit session (items confirmed still present after the False, deleted cleanly on retry). Cause unknown — do NOT read this as the ProjectManager.DeleteProject shape: that one has an identified mechanism (the project being, or recently having been, current) that retrying does not clear, whereas a single retry cleared this in the one instance seen. One observation is not a mechanism; if a retry is ever seen to fail repeatedly here, this entry needs revisiting.
382
- - **Workaround / current handling:** Treat a False return as advisory: re-list the track and check whether the items are actually gone; if still present, retry the identical call once before failing. A readback that raised, enumerated nothing, or covered items whose unique ID cannot be read is UNKNOWN, not gone — never report an unverifiable delete as success, and do not spend a second destructive call on an outcome you equally cannot read.
383
- - **Tags:** unreliable-return, flaky, timeline, edit
405
+ - **Behavior:** Two distinct failures share this call.
406
+
407
+ **1. Wrong page (deterministic, has a mechanism).** With the UI on the Fairlight page, DeleteClips returns False and deletes nothing, no matter how many times it is retried. Verified 2026-08-04 on Studio 21.0: three identical retries against 132 valid, unlocked, present TimelineItems all returned False with all 132 still on the track; a single `Resolve.OpenPage("edit")` followed by the same call returned True and left 0 items. Track lock and enable state were confirmed clear before and after, so this is a page gate, not a lock. This is the case the previous entry's falsification condition anticipated — a retry seen to fail repeatedly — and revisiting it found the mechanism.
408
+
409
+ **2. Flaky first attempt (one observation, no mechanism).** Independently of the page, the call has been seen once to return False with every item still present, where an identical immediate retry succeeded (Studio 21.0, cut-video edit session). Whether the UI was on the Edit page at the time was not recorded, so it cannot be ruled in or out as failure 1 in disguise. Do NOT read this as the ProjectManager.DeleteProject shape: that one has an identified mechanism (the project being, or recently having been, current) that retrying does not clear. One observation is still not a mechanism; if an immediate retry is ever seen to fail repeatedly while the UI is confirmed on the Edit page, this sub-entry needs revisiting.
410
+ - **Workaround / current handling:** Open the Edit page first (`Resolve.OpenPage("edit")`) — a caller that deletes clips from a script must not assume the user left the UI on Edit, and a Fairlight or Color session is a completely ordinary place for them to be. Then treat a False return as advisory: re-list the track and check whether the items are actually gone; if still present, retry the identical call once before failing. A readback that raised, enumerated nothing, or covered items whose unique ID cannot be read is UNKNOWN, not gone — never report an unverifiable delete as success, and do not spend a second destructive call on an outcome you equally cannot read.
411
+ - **Tags:** unreliable-return, flaky, silent-failure, page-dependent, timeline, edit
384
412
 
385
413
  ### MediaPool.AppendToTimeline with mixed-fps sources (duration floor)
386
414
 
package/install.py CHANGED
@@ -36,7 +36,7 @@ from src.utils.update_check import (
36
36
 
37
37
  # ─── Version ──────────────────────────────────────────────────────────────────
38
38
 
39
- VERSION = "2.78.0"
39
+ VERSION = "2.79.0"
40
40
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
41
41
  # Resolve's scripting bridge loads into newer interpreters on recent builds
42
42
  # (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.78.0",
3
+ "version": "2.79.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -98,7 +98,9 @@ Each dispatches on an `action`. Highlights:
98
98
  **.prproj via gunzip+XML** — pass the file PATH for the binary ones), `list_sequences` (one picker entry
99
99
  point across xml/edl/otio/drt/drp/aaf/prproj), `convert_to_interchange` (author OTIO/EDL/DRT that Resolve
100
100
  imports, from events or a parsed source — **the .prproj→Resolve conform bridge**, no Premiere needed;
101
- editorial timing/cuts/transitions/speed carry, per-clip effects/color do not),
101
+ editorial timing/cuts/transitions carry, per-clip effects/color do not; speed/reverse carry on
102
+ the `otio` and `edl` targets only — `drt` has no per-clip speed field and reports every retime it
103
+ flattens in `flattened`),
102
104
  `turnover_changelist` (moved/retimed/replaced/new/gone + timing silent-lie guards), `conform_manifest`,
103
105
  `marker_roundtrip`.
104
106
  - **`provenance`** — provenance / audit: `gallery_lineage`, `grade_provenance` ("why is this graded this
@@ -28,12 +28,71 @@ const byTrack = (events) => {
28
28
  return groups;
29
29
  };
30
30
 
31
+ /**
32
+ * Events whose media timecode ORIGIN had to be assumed as 0 when authoring OTIO. Resolve
33
+ * measures a clip's source_range against the media's real timecode range, so an event with
34
+ * no origin only imports if its media genuinely starts at 00:00:00:00 — otherwise the file
35
+ * looks fine and Resolve creates no timeline. Supply `mediaStartTcFrame` (or an absolute
36
+ * `srcTcFrame`) per event to clear it. Always an array, empty when every event carried one.
37
+ */
38
+ export function otioMediaOriginAssumed(events) {
39
+ const out = [];
40
+ (events || []).forEach((e, index) => {
41
+ if (e.srcTcFrame != null || e.mediaStartTcFrame != null || e.startTcFrame != null) return;
42
+ out.push({
43
+ index,
44
+ recIn: e.recIn ?? 0,
45
+ srcIn: e.srcIn ?? 0,
46
+ source: e.source || '',
47
+ reason: 'no media timecode origin — assumed 00:00:00:00; Resolve rejects the import if the media starts elsewhere',
48
+ });
49
+ });
50
+ return out;
51
+ }
52
+
53
+ const rt = (value, rate) => ({ OTIO_SCHEMA: 'RationalTime.1', rate: Number(rate), value: Number(value) });
54
+ const tr = (start, duration, rate) => ({ OTIO_SCHEMA: 'TimeRange.1', duration: rt(duration, rate), start_time: rt(start, rate) });
55
+ const basename = (p) => String(p || '').split('/').pop() || 'UNKNOWN';
56
+
31
57
  /**
32
58
  * Build an OTIO timeline doc (plain object) from normalized events. Inserts gaps so record
33
59
  * positions are exact; emits LinearTimeWarp for speed/reverse and Transition items where present.
60
+ *
61
+ * The emitted shape mirrors what Resolve's own EXPORT_OTIO writes, because Resolve's importer
62
+ * is far pickier than the OTIO spec. Measured on 19.1.3: a Resolve-authored .otio re-imports
63
+ * through MediaPool.ImportTimelineFromFile (3 items, 3 linked) while the shape this function
64
+ * used to emit produced NO TIMELINE — same project, same session, same three online media
65
+ * files. So the scripting API imports OTIO fine; what it rejects is a document that is valid
66
+ * OTIO but not Resolve-shaped. The parts that matter:
67
+ *
68
+ * - `Clip.2` with a `media_references` MAP + `active_media_reference_key`, not `Clip.1`
69
+ * with a singular `media_reference`. This is the one that most looks like a free choice
70
+ * and is not.
71
+ * - `available_range` on the external reference, and a clip `name` that is the media
72
+ * BASENAME rather than its full path.
73
+ * - SOURCE FRAMES ARE TIMECODE-ABSOLUTE. This is the one that decides the import. The
74
+ * media used to measure this carries an embedded start TC of 01:00:00:00, so Resolve
75
+ * writes its `available_range` starting at frame 86400 and the clip's `source_range`
76
+ * at 86400 too. Emitting the same cut with 0-based source offsets — the natural
77
+ * reading of "source in point" — produced NO TIMELINE, because frame 0 is outside the
78
+ * media's real range. Bisected: 0-based fails and absolute succeeds whether or not the
79
+ * stack/track `source_range` is null, so it is the frame origin that matters and not
80
+ * the surrounding shape. Same lesson as srcIn being fragment-relative on consolidated
81
+ * media: a source position means nothing without the origin it is measured from.
82
+ * - `enabled: true` on stack, tracks and clips; `metadata` objects present (Resolve stamps
83
+ * its own `Resolve_OTIO` block and reads the tracks' back).
84
+ * - `global_start_time` on the timeline, and track names in Resolve's own form ("Video 1").
85
+ *
86
+ * Give the media's TC origin per event as `mediaStartTcFrame` (or an already-absolute
87
+ * `srcTcFrame`). Without it this falls back to a 0 origin, which only imports when the media
88
+ * really does start at 00:00:00:00 — so the events that had to be assumed are reported back
89
+ * in `mediaOriginAssumed` rather than silently producing a file that imports as nothing.
90
+ *
91
+ * Handy inverse: this repo's parseOTIO still reads what we emit, so the bridge round-trips.
34
92
  */
35
93
  export function eventsToOTIO(events, opts = {}) {
36
94
  const fps = opts.fps || events.find((e) => e.fps)?.fps || 24;
95
+ const startFrame = opts.startFrame ?? opts.globalStartFrame ?? 0;
37
96
  const groups = byTrack(events);
38
97
  const tracks = [];
39
98
  for (const [kind, list] of [
@@ -48,40 +107,88 @@ export function eventsToOTIO(events, opts = {}) {
48
107
  if (recIn > rec) {
49
108
  children.push({
50
109
  OTIO_SCHEMA: 'Gap.1',
51
- source_range: { OTIO_SCHEMA: 'TimeRange.1', duration: { OTIO_SCHEMA: 'RationalTime.1', value: recIn - rec, rate: fps } },
110
+ metadata: {},
111
+ source_range: { OTIO_SCHEMA: 'TimeRange.1', duration: rt(recIn - rec, fps), start_time: rt(0, fps) },
112
+ effects: [],
113
+ markers: [],
114
+ enabled: true,
52
115
  });
53
116
  rec = recIn;
54
117
  }
55
118
  const recDur = (e.recOut ?? recIn) - recIn;
119
+ const clipFps = e.fps || fps;
120
+ const srcIn = e.srcIn ?? 0;
121
+ // The media's timecode origin. srcTcFrame (already absolute) wins; otherwise the
122
+ // media start TC plus the in-point. A 0 origin is an ASSUMPTION, and it is recorded.
123
+ const mediaStart = e.mediaStartTcFrame ?? e.startTcFrame ?? null;
124
+ const srcStart = e.srcTcFrame ?? (mediaStart ?? 0) + srcIn;
125
+ const availStart = e.srcTcFrame != null ? e.srcTcFrame - srcIn : (mediaStart ?? 0);
56
126
  const clip = {
57
- OTIO_SCHEMA: 'Clip.1',
58
- name: e.source || 'UNKNOWN',
59
- source_range: {
60
- OTIO_SCHEMA: 'TimeRange.1',
61
- start_time: { OTIO_SCHEMA: 'RationalTime.1', value: e.srcIn ?? 0, rate: e.fps || fps },
62
- duration: { OTIO_SCHEMA: 'RationalTime.1', value: recDur, rate: e.fps || fps },
63
- },
64
- media_reference: { OTIO_SCHEMA: 'ExternalReference.1', target_url: e.source || '' },
127
+ OTIO_SCHEMA: 'Clip.2',
128
+ metadata: {},
129
+ name: basename(e.source),
130
+ source_range: tr(srcStart, recDur, clipFps),
65
131
  effects: [],
66
132
  markers: [],
133
+ enabled: true,
134
+ media_references: {
135
+ DEFAULT_MEDIA: {
136
+ OTIO_SCHEMA: 'ExternalReference.1',
137
+ metadata: {},
138
+ name: basename(e.source),
139
+ // Resolve writes a BARE path here, not a file:// URL.
140
+ target_url: e.source || '',
141
+ available_range: tr(availStart, e.mediaDuration ?? e.srcAvailDuration ?? Math.max(recDur, srcIn + recDur), clipFps),
142
+ available_image_bounds: null,
143
+ },
144
+ },
145
+ active_media_reference_key: 'DEFAULT_MEDIA',
67
146
  };
68
147
  if ((e.speed ?? 100) !== 100 || e.reverse) {
69
- clip.effects.push({ OTIO_SCHEMA: 'LinearTimeWarp.1', name: 'Speed', time_scalar: (e.reverse ? -1 : 1) * ((e.speed ?? 100) / 100) });
148
+ clip.effects.push({ OTIO_SCHEMA: 'LinearTimeWarp.1', name: 'Speed', metadata: {}, effect_name: 'LinearTimeWarp', time_scalar: (e.reverse ? -1 : 1) * ((e.speed ?? 100) / 100) });
70
149
  }
71
150
  if (e.transition) {
72
151
  children.push({
73
152
  OTIO_SCHEMA: 'Transition.1',
153
+ metadata: {},
154
+ name: 'Cross Dissolve',
74
155
  transition_type: 'SMPTE_Dissolve',
75
- in_offset: { OTIO_SCHEMA: 'RationalTime.1', value: Math.ceil((e.transition.duration || 0) / 2), rate: fps },
76
- out_offset: { OTIO_SCHEMA: 'RationalTime.1', value: Math.floor((e.transition.duration || 0) / 2), rate: fps },
156
+ in_offset: rt(Math.ceil((e.transition.duration || 0) / 2), fps),
157
+ out_offset: rt(Math.floor((e.transition.duration || 0) / 2), fps),
77
158
  });
78
159
  }
79
160
  children.push(clip);
80
161
  rec = recIn + recDur;
81
162
  }
82
- tracks.push({ OTIO_SCHEMA: 'Track.1', name: `${kind[0]}1`, kind, children });
163
+ tracks.push({
164
+ OTIO_SCHEMA: 'Track.1',
165
+ metadata: {},
166
+ name: `${kind} 1`,
167
+ source_range: tr(0, rec, fps),
168
+ effects: [],
169
+ markers: [],
170
+ enabled: true,
171
+ children,
172
+ kind,
173
+ });
83
174
  }
84
- return { OTIO_SCHEMA: 'Timeline.1', name: opts.name || 'Conformed', tracks: { OTIO_SCHEMA: 'Stack.1', name: 'tracks', children: tracks } };
175
+ const stackDur = tracks.reduce((m, t) => Math.max(m, t.source_range.duration.value), 0);
176
+ return {
177
+ OTIO_SCHEMA: 'Timeline.1',
178
+ metadata: {},
179
+ name: opts.name || 'Conformed',
180
+ global_start_time: rt(startFrame, fps),
181
+ tracks: {
182
+ OTIO_SCHEMA: 'Stack.1',
183
+ metadata: {},
184
+ name: 'tracks',
185
+ source_range: tr(0, stackDur, fps),
186
+ effects: [],
187
+ markers: [],
188
+ enabled: true,
189
+ children: tracks,
190
+ },
191
+ };
85
192
  }
86
193
 
87
194
  /** Build a CMX3600 EDL string (cuts + M2 speed). Video events only, per EDL convention. */
@@ -108,6 +215,35 @@ export function eventsToEDL(events, opts = {}) {
108
215
  return lines.join('\n') + '\n';
109
216
  }
110
217
 
218
+ const isRetimed = (e) => (e.speed ?? 100) !== 100 || Boolean(e.reverse);
219
+
220
+ /**
221
+ * Which events lose their retime on the DRT target. The DRT clip schema
222
+ * (vendor/drp-format/seq-container-builder.js) carries start / duration / in /
223
+ * mediaFilePath / mediaStartTime / mediaFrameRate and nothing else — there is no per-clip
224
+ * speed field to write a retime into, so a retimed event lands at 100% forward. OTIO
225
+ * (LinearTimeWarp) and EDL (M2) do carry it; only DRT flattens.
226
+ *
227
+ * Reporting it is this cluster's skip-not-fake contract: a flattened retime that nobody is
228
+ * told about is a timeline the caller believes is conformed and is not. `index` is the
229
+ * event's position in the ORIGINAL events array, so a caller can map straight back.
230
+ */
231
+ export function drtFlattenedRetimes(events) {
232
+ const out = [];
233
+ (events || []).forEach((e, index) => {
234
+ if (!isRetimed(e)) return;
235
+ out.push({
236
+ index,
237
+ recIn: e.recIn ?? 0,
238
+ speed: e.speed ?? 100,
239
+ reverse: Boolean(e.reverse),
240
+ source: e.source || '',
241
+ reason: 'DRT clip schema has no per-clip speed field — retime flattened to 100% forward',
242
+ });
243
+ });
244
+ return out;
245
+ }
246
+
111
247
  /** Build a buildDRT spec (Resolve-native .drt) from normalized events. */
112
248
  export function eventsToDrtSpec(events, opts = {}) {
113
249
  const fps = opts.fps || events.find((e) => e.fps)?.fps || 24;
@@ -133,12 +269,14 @@ export function eventsToDrtSpec(events, opts = {}) {
133
269
  /**
134
270
  * Author `events` into `target` interchange. Returns { target, content?, spec?, bytes? }.
135
271
  * For 'drt', when outputPath is given the .drt bytes are written; otherwise the spec is returned.
272
+ * The 'drt' target additionally returns `flattened` — ALWAYS an array, empty when the cut
273
+ * carries no retimes, so a caller can tell "none to lose" from "this build is too old to say".
136
274
  */
137
275
  export async function authorInterchange(events, target, opts = {}) {
138
276
  const t = String(target || 'otio').toLowerCase();
139
277
  if (t === 'otio') {
140
278
  const doc = eventsToOTIO(events, opts);
141
- return { target: 'otio', content: JSON.stringify(doc, null, 2), doc };
279
+ return { target: 'otio', content: JSON.stringify(doc, null, 2), doc, mediaOriginAssumed: otioMediaOriginAssumed(events) };
142
280
  }
143
281
  if (t === 'edl') {
144
282
  return { target: 'edl', content: eventsToEDL(events, opts) };
@@ -146,7 +284,7 @@ export async function authorInterchange(events, target, opts = {}) {
146
284
  if (t === 'drt') {
147
285
  const spec = eventsToDrtSpec(events, opts);
148
286
  const buf = await drt().buildDRT(spec);
149
- return { target: 'drt', spec, buffer: buf, bytes: buf.length };
287
+ return { target: 'drt', spec, buffer: buf, bytes: buf.length, flattened: drtFlattenedRetimes(events) };
150
288
  }
151
289
  throw new Error(`authorInterchange: unknown target '${target}' (otio|edl|drt)`);
152
290
  }
@@ -82,7 +82,7 @@ const markerSchema = z.object({
82
82
  export const editorialTool = {
83
83
  name: 'editorial',
84
84
  description:
85
- 'Editorial integrity (Cluster E) — turnover interchange → normalized events → changelist + conform manifest with TIMING silent-lie guards (flattened retime / dropped J/L-cut audio / framerate-pulldown slip / reverse dropped / transition-handle starvation → flag, skip-not-fake). Report-only (gate: review). Actions: parse_interchange (EDL/OTIO/XMEML natively + AAF via pyaaf2 + PRPROJ via gunzip+XML → normalized events; for AAF/PRPROJ pass the file PATH as content; AAF also returns per-sequence startTimecode/startFrame — build the timeline at THAT start, not the Resolve 01:00:00:00 default — and per-clip `geometry` for Avid transform effects), list_sequences (ONE offline picker entry point across xml/edl/otio/drt/drp/aaf/prproj → [{id,name,eventCount}], plus startTimecode/startFrame for AAF), convert_to_interchange (author OTIO/EDL/DRT Resolve CAN import from events or a parsed source — the .prproj→Resolve conform bridge, no Premiere needed; editorial timing/transitions/speed survive, per-clip effects/color do not), turnover_changelist (diff old vs new → moved/retimed/replaced/new/gone + timing flags), conform_manifest (per-event assert: source resolved/handles/retime/reverse/TC-base), marker_roundtrip (markers with provenance tags). Offline (AAF needs pyaaf2; live AAF/DRP import is on the Python davinci-resolve MCP).',
85
+ 'Editorial integrity (Cluster E) — turnover interchange → normalized events → changelist + conform manifest with TIMING silent-lie guards (flattened retime / dropped J/L-cut audio / framerate-pulldown slip / reverse dropped / transition-handle starvation → flag, skip-not-fake). Report-only (gate: review). Actions: parse_interchange (EDL/OTIO/XMEML natively + AAF via pyaaf2 + PRPROJ via gunzip+XML → normalized events; for AAF/PRPROJ pass the file PATH as content; AAF also returns per-sequence startTimecode/startFrame — build the timeline at THAT start, not the Resolve 01:00:00:00 default — and per-clip `geometry` for Avid transform effects), list_sequences (ONE offline picker entry point across xml/edl/otio/drt/drp/aaf/prproj → [{id,name,eventCount}], plus startTimecode/startFrame for AAF), convert_to_interchange (author OTIO/EDL/DRT Resolve CAN import from events or a parsed source — the .prproj→Resolve conform bridge, no Premiere needed; editorial timing/transitions survive and per-clip effects/color do not. SPEED/REVERSE survive on the otio (LinearTimeWarp) and edl (M2) targets ONLY — the DRT clip schema has no per-clip speed field, so target `drt` flattens every retime to 100% forward and returns `flattened`/`flattenedCount` naming each event that lost one; `flattened` is always present on `drt`, empty when there were none), turnover_changelist (diff old vs new → moved/retimed/replaced/new/gone + timing flags), conform_manifest (per-event assert: source resolved/handles/retime/reverse/TC-base), marker_roundtrip (markers with provenance tags). Offline (AAF needs pyaaf2; live AAF/DRP import is on the Python davinci-resolve MCP).',
86
86
  async handler({ action, args }) {
87
87
  if (action === 'parse_interchange') {
88
88
  const p = parseSchema.parse(args);
@@ -125,6 +125,10 @@ export const editorialTool = {
125
125
  eventCount: events.length,
126
126
  ...(written || { content: authored.content }),
127
127
  ...(authored.spec ? { spec: authored.spec } : {}),
128
+ // skip-not-fake: the DRT clip schema has no per-clip speed field, so retimes cannot
129
+ // ride into a .drt. Name every event that lost one rather than returning a timeline
130
+ // the caller believes is conformed. Always present on 'drt' (empty = none to lose).
131
+ ...(authored.flattened ? { flattened: authored.flattened, flattenedCount: authored.flattened.length } : {}),
128
132
  };
129
133
  }
130
134
  if (action === 'turnover_changelist') {