davinci-resolve-mcp 2.136.0 → 2.137.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,531 @@
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.137.0 — the sweep reaches the database tier
6
+
7
+ ### Fixed
8
+
9
+ - **Fairlight DB row selection** (adapted the v2.133 container-pin lesson to
10
+ the vendor layer): `readFromDatabase` took the FIRST `Sm2Sequence` row — in
11
+ any project holding a compound clip that can be a compound's embedded
12
+ sequence with no `FLStudioModelBA`, so bus reads failed while the model sat
13
+ in the next row (measured live). Worse, `applyTemplate` wrote its new blob
14
+ into EVERY blob-bearing sequence row, clobbering compound `SeqRef` links
15
+ project-wide. Reads now pick the model-bearing row; writes scope to exactly
16
+ that `Sm2Sequence_id` and refuse ambiguous multi-timeline projects without
17
+ an explicit target. `read_buses_from_db` also stops dumping the ~430KB
18
+ decompressed model into the tool response.
19
+ - **`provenance.cdl_diff` silently identity-defaulted array-shaped CDLs**:
20
+ `[r,g,b]` slope/offset/power (the common interchange form) read as unity
21
+ through the `{r,g,b}` accessor, so two different grades diffed as
22
+ saturation-only (measured). Both shapes are accepted now; unrecognizable
23
+ shapes refuse loudly.
24
+ - **Node-floor enforcement**: the advanced server refuses to start below
25
+ Node 20.9 with the exact fix named (measured live: a client config's node
26
+ resolved to an nvm v18, where pure-JS tools limp and better-sqlite3 dies
27
+ with a cryptic ABI mismatch). `install.py` now writes an absolute,
28
+ version-checked node path into client configs, and the better-sqlite3
29
+ loader distinguishes "not installed" from "built for a different Node".
30
+ - **Windows UTF-8, rounds two and three** — adapted from
31
+ [PR #175](https://github.com/samuelgursky/davinci-resolve-mcp/pull/175) and
32
+ [PR #176](https://github.com/samuelgursky/davinci-resolve-mcp/pull/176) by
33
+ @Chosen-3: the six unencoded call sites in `src/` (brain-edits registry,
34
+ page lock) and `install.py`'s client-config `read_json`/`write_json`. The
35
+ UTF-8 discipline guard now covers `src/` and `install.py` too.
36
+
37
+ ### Verified through the MCP tool layer (E60)
38
+
39
+ `project_read` (introspect/report/audit/timeline_clips), `offline_ref.list_in_project`,
40
+ `color_trace.plan` (6/6 exact-name matches), and both fairlight bus readers,
41
+ all against a live scratch project database.
42
+
43
+ ## What's New in v2.136.1 — the changelog catches up
44
+
45
+ ### Fixed
46
+
47
+ - **README badges and this changelog had silently frozen at v2.108.0** while
48
+ 28 releases shipped with notes only on GitHub Releases. All entries
49
+ v2.109.0-v2.136.0 are now ported here verbatim, both README badges (and the
50
+ zh-CN correspondence line) track the released version again, and a new
51
+ drift guard (`tests/test_release_surface_drift.py`) fails the suite if any
52
+ of these surfaces lag a version bump — the release-process mandate is now
53
+ enforceable instead of aspirational.
54
+ - **Windows `UnicodeDecodeError` in tests/scripts** — adapted from
55
+ [PR #174](https://github.com/samuelgursky/davinci-resolve-mcp/pull/174) by
56
+ @Chosen-3: 151 `open()`/`read_text()`/`write_text()` call sites across 47
57
+ files gained `encoding="utf-8"` (locale-encoding fallback crashes on
58
+ cp1252 the moment a read target holds non-ASCII bytes; binary-mode and
59
+ `PIL.Image.open` sites correctly exempt). A companion AST guard
60
+ (`tests/test_utf8_encoding_discipline.py`) keeps new unencoded text-mode
61
+ calls out of `tests/` and `scripts/` — its first catch was this repo's own
62
+ day-old sync script.
63
+
64
+ ## What's New in v2.136.0 — portable agent assets
65
+
66
+ Adapted from [PR #173](https://github.com/samuelgursky/davinci-resolve-mcp/pull/173) by @jin386 — the portable `.agents/` layout lands, with the review findings folded in rather than waiting on a revision round.
67
+
68
+ ### What's new
69
+
70
+ - **`.agents/` is the host-neutral canonical layer**: `.agents/skills` (the skill corpus, now what the `knowledge` MCP tool serves and what Codex reads directly), `.agents/roles` (reviewer bodies), `.agents/hooks` (canonical guard logic with a shared `hook_runtime`).
71
+ - **Codex support as contributed**: `.codex/hooks.json` wiring, hook shims, and native agent TOMLs, plus the cross-host portability test suite.
72
+ - **Claude Code loses nothing**: `.claude/skills` adapters are content-complete, byte-identical copies — never pointer stubs — so the rich "Apply when…" trigger descriptions, named craft-skill references, and `user-invocable` flags survive verbatim (Claude routes on the frontmatter at selection time). `.claude/agents` keep their frontmatter, including the deliberate `model: opus` pins. `CLAUDE.md` stays intentionally short.
73
+ - **Safety kept narrow**: `source_media_guard`'s scratch-exemption prefixes remain `claude-`/`codex-` only — the proposed `agent-` prefix would have exempted any real `agent-*` directory from the source-media deny.
74
+ - **Drift cannot land**: `scripts/agent-rules/sync_portable_assets.py` (+`--check`) restores the invariant from either edit point, and `tests/test_portable_asset_parity.py` fails the suite on divergence, missing adapters (the "new skill silently never loads" failure mode), lost frontmatter pins, or widened scratch prefixes.
75
+
76
+ Suites: Node 853, Python 3123 + 845 subtests (portability + parity families added).
77
+
78
+ ## What's New in v2.135.0 — audio.trim never trimmed
79
+
80
+ The E59 protocol-layer sweep — the v2.133.0 smoke-test harness pointed at the *other* 16 advanced tools — found one real silent lie and confirmed the rest of the surface healthy.
81
+
82
+ ### Fixed
83
+
84
+ **`audio.trim` never trimmed.** Two stacked failures, both invisible to success-shaped output: the non-strict schema silently stripped mistyped window keys (so `{start, duration}` copied the whole file and reported success), and the tool's own advertised `durationFrames` was never in the vendored module's vocabulary (`{startTime, endTime, duration}` in seconds) — even a correct call returned the full file as a "trim". Schemas across the audio tool are now `.strict()` (unknown keys refuse; extra ffmpeg knobs belong in `opts`), `durationFrames` is required (a windowless trim is a no-op copy wearing a trim's name — use `convert`), and a new optional `fps` (default 24) converts it to seconds. Live-verified through the MCP layer: `durationFrames: 24` → exactly 1.000 s of output.
85
+
86
+ ### Swept clean
87
+
88
+ All 18 dispatchers refuse unknown actions with structured errors; offline happy paths verified for drp, drx, fusion, audio_plan, pipeline, editorial, conform, media, deliverable, and capabilities.
89
+
90
+ Suites: Node 853, Python 3111 + 799 subtests.
91
+
92
+ ## What's New in v2.134.1 — the nesting envelope extends
93
+
94
+ A follow-up measurement to v2.134.0: depth-3 nesting also renders (E58 — a compound inside a compound inside a compound, triple-nested white measured at 234 through the full spec → import → render route on 19.1.3.7). The SequenceSetup fix generalizes; nesting depth is no longer the boundary. Tool doc, guide, and code comments updated from "deeper unverified" to the measured envelope. Suites: Node 852, Python 3111+799.
95
+
96
+ ## What's New in v2.134.0 — freeze frames and nested compounds
97
+
98
+ Two measured boundaries — both previously closed as "not authorable" — reopened with new harvest angles and closed for real, each render-proven on Studio 19.1.3.7.
99
+
100
+ ### Freeze frames: authored offline (`cuts[].freeze`)
101
+
102
+ The old finding said no harvest path existed. One did: Resolve's EDL importer honors `M2 <reel> 000.0` motion memos, giving the first real frozen clip whose bytes could be read (E55: reads back source N..N **and** renders frozen — freezedetect-proven, the direction the earlier synthetic always failed in). The real `Sm2TimeMap` is flat in **seconds**, not frames: `YMin = YMax = Y = frozenFrame/fps`, `XMax = 60000` (a sentinel domain), and the clip's `<In>` stays empty. `buildFreezeTimemapKeyed` reproduces the harvest byte-exactly; `cuts[].freeze: true` (or `speed: 0`) authors it, and `assemble_from_interchange` now **authors** zero-speed events (EDL M2 freezes, zero-speed warps) instead of flattening them with a reason. Proof: an offline-authored freeze at a *different* source frame holds luma 125.09 for exactly 2.000 s, then the following cut resumes motion.
103
+
104
+ ### Nested compounds: depth-2 black solved (`compounds[].compounds`)
105
+
106
+ `Timeline.CreateCompoundClip` works on 19.1.3, so a real doubly nested compound was made live and its archive diffed against the synthetic one that rendered black. Exactly one delta mattered: a Resolve-made compound's embedded pool `Sm2Sequence` FieldsBlob carries a **`SequenceSetup`** key (a 347-byte constant project-format blob) the donor template lacked. With it added, doubly nested synthetic content renders — bisect confirmed `SequenceSetup` alone flips it (E56), and the full tool-layer route proves it end to end (E57: white 234 through two nesting levels with flanking cuts intact). `spec.compounds` now nests recursively; depth-2 playback is render-verified, deeper composes structurally but is unverified.
107
+
108
+ ### Verification
109
+
110
+ - Node (vendor + server): 852 passed (5 new tests incl. a byte-exact freeze-harvest fixture and a SequenceSetup template guard)
111
+ - Python: 3111 passed + 799 subtests
112
+ - Live: E55 harvests, E56 freeze + depth-2 bisect renders, E57 nested-spec render — all measured by frame luma / freezedetect
113
+
114
+ ## What's New in v2.133.0 — the tool layer meets its own surface
115
+
116
+ The first end-to-end pass of the entire v2.106–v2.132 native-DRT authoring surface **through the MCP protocol layer** (every earlier proof drove the modules directly). A kitchen-sink spec — media cuts, cross-dissolve, 0.5x retime, V2 stacking, explicit audio placement, a compound clip, markers, and SRT subtitles in ONE assemble — was authored offline, imported, read back, and render-verified on Studio 19.1.3.7 (frame luma 122.9 / 181.6 mid-dissolve / 234 / 125.8 retime / 125.5 compound-inner; tone at the mono-strip -24.1 dB, then silence). The smoke test caught three real defects; all are fixed and regression-tested.
117
+
118
+ ### Fixed
119
+
120
+ - **Subtitles (and marker ownership) vanished when compounds were in the spec.** Both placement steps ran after compound insertion but still targeted the first name-sorted `SeqContainer` — and a compound's inner container matches that pattern. Measured live: the imported timeline had no subtitle track; the cues sat inside the compound. The parent container id is now pinned once, before any compound exists, and threaded through subtitle placement and the marker blob's owner.
121
+ - **`editorial.verify_roundtrip` could not close an EDL loop.** The zero-duration outgoing dissolve leg was paired as a real event (count mismatch), and EDL reel names (`CUTSRC`) had no way to match the re-export's file basenames (`cut_src`). Zero-length events are dropped, and a new `sourceMap` parameter — the same map that drove the assemble — derives the reel→basename aliases. EDL → assemble → import → OTIO-export → verify now passes with fitted per-source offsets.
122
+ - **`assemble_from_interchange` result note contradicted itself**, appending the stale pre-v2.111 "transitions become cuts" text after the authored-ledger sentence.
123
+ - **Headless recovery (#172):** a `-nogui` boot that never becomes scriptable still holds the one-per-machine singleton, wedging the GUI too. `resolve_headless.py start` now kills the instance it spawned when its readiness check fails; `stop --force` escalates TERM→KILL for an unanswering instance (unclean — expect project locks and a slow next boot); the headless-edit-loop guide names the precondition and a 30-second preflight.
124
+
125
+ ### Verification
126
+
127
+ - Python: 3111 passed + 799 subtests (6 new recovery-path tests)
128
+ - Node (vendor + server): 847 passed (2 new regression tests)
129
+ - Live: kitchen-sink render probe + EDL round-trip pass on 19.1.3.7
130
+
131
+ ## What's New in v2.132.1 — the nesting boundary
132
+
133
+ Knowledge release. **Depth-2 compound nesting renders black**: a compound placed inside another compound's inner container composes structurally — imports fully linked, reads back — but the doubly nested content renders black (the readback-blind class again). Depth-1, multiple parallel compounds per archive, remains the render-verified envelope; the tool doc now states the boundary.
134
+
135
+ Also corrected during cleanup: the crash-window "phantom projects" never existed — a project created moments before a Resolve crash dies with the instance (no DB row, no folder), and `DeleteProject` returning `False` afterwards means *nothing to delete*. Lesson recorded: re-list after a crash before diagnosing project state.
136
+
137
+ Suites: Node 845 pass / 0 fail; Python 3105 passed + 799 subtests.
138
+
139
+ ## What's New in v2.132.0 — multiple compounds compose
140
+
141
+ ### The one-per-archive restriction falls
142
+
143
+ Three separate dangling references each hard-crash Resolve's importer — mapped one crash at a time, then confirmed with an all-encodings reference sweep:
144
+
145
+ 1. the pool element's `<MpFolder>` (v2.131)
146
+ 2. the embedded sequence blob's keyed **`SeqRef`** — it names the inner *container's* uuid, patched through the keyed-dict codec
147
+ 3. the embedded sequence's **`<Parent>`** — pointing back at the compound's own pool id
148
+
149
+ With all three rewired, every cluster identity freshens safely and **multiple compounds compose in one archive**. Also fixed en route: container listing is name-sorted, so an inner container could alphabetically precede the parent and swallow the next compound's item — the parent is now pinned explicitly.
150
+
151
+ **Render proof:** parent cut 124.5 → CMP_A's inner white 234 → CMP_B's inner cut 125.3 — two offline-authored nested timelines playing back to back on 19.1.3.
152
+
153
+ Suites: Node 845 pass / 0 fail; Python 3105 passed + 799 subtests.
154
+
155
+ ## What's New in v2.131.0 — compound clips authored offline
156
+
157
+ ### Nested timelines, fully offline
158
+
159
+ `drt.assemble` gains `spec.compounds`: author a compound clip — a nested timeline with its own inner edit — entirely offline, and it **renders** after import.
160
+
161
+ **Render proof (fresh project, 19.1.3):** parent cut (124.5) → the compound's inner cut at source offset 96 (125.3) → the compound's inner white (234). An offline-authored nested edit, playing.
162
+
163
+ **Two crash laws paid for the summit** (Resolve died twice mapping them):
164
+ - The compound cluster's identities ride **verbatim** — the embedded `Sm2Sequence` FieldsBlob encodes them, and freshening the XML ids around the unchanged blob crashes the importer outright. Hence: one compound per archive for now.
165
+ - A dangling `<MpFolder>` reference in the pool element also crashes the importer — it's rewired to the target pool's folder.
166
+
167
+ Inner content uses the ordinary cuts machinery on the inner container (origin frame 0), cloning the sources' captured native clips — `cut-media` now supports donor-less tracks when every cut carries one.
168
+
169
+ Suites: Node 845 pass / 0 fail; Python 3105 passed + 799 subtests.
170
+
171
+ ## What's New in v2.130.0 — compound clips survive extraction
172
+
173
+ ### The hollow-compound bug
174
+
175
+ A compound clip in a `.drp` is a pool `Sm2MpCompoundClip` embedding a full `Sm2Sequence` — whose actual tracks live in their **own SeqContainer**. The extraction recipe kept only the target timeline's container, so any timeline containing a compound extracted into a `.drt` whose compound imported, read back… and was **hollow**.
176
+
177
+ `extract_from_drp` now walks the kept container's `MediaRef`s → compound pool elements → embedded sequence ids → keeps the inner containers too, recursively (compounds nest).
178
+
179
+ **Live proof:** the fixed extraction imports 3/3 linked with the compound intact, and the archive **renders the compound's inner content** (cut 125.3 → white 234, audio −21.1 dB) — compound clips fully survive the `.drt` route on 19.1.3.
180
+
181
+ Also banked: the full `.drp` anatomy of compounds (embedded sequence identity, Fairlight blob, the hidden `000_Archive` pool location, the generic item blob) — the map for offline compound *authoring* later.
182
+
183
+ Suites: Node 844 pass / 0 fail; Python 3105 passed + 799 subtests.
184
+
185
+ ## What's New in v2.129.0 — sidecar SRT in the conform route
186
+
187
+ Turnover packages usually ship a sidecar `.srt` next to the edit. `assemble_from_interchange` now takes `subtitlesSrtPath` and authors the cues onto the subtitle track in the same call — EDL/OTIO/AAF/XML/prproj in, picture + audio + subtitles out.
188
+
189
+ Suites: Node 843 pass / 0 fail; Python 3105 passed + 799 subtests.
190
+
191
+ ## What's New in v2.128.0 — subtitles authored; track matrix complete
192
+
193
+ ### The last track type falls
194
+
195
+ Subtitles turn out to be the **simplest item in the whole schema**: a plain `Sm2TiGenerator` with `PrettyType Subtitle` and the cue text in `<Name>` — no blobs at all, on a Type-2 track. No Fusion comp means the byte-keyed cache law doesn't apply, and the payload is API-visible after import.
196
+
197
+ `drt.assemble` gains `spec.subtitles` (frame-addressed cues) and `spec.subtitlesSrt` (**raw SRT in, cues out** — composes with `spec.startFrame`). Overlapping cues refuse; the track vec is synthesized from the harvested shape.
198
+
199
+ **Live proof:** SRT cues plus a spec-level cue import and read back at exact frames with their text. One measured caveat, documented: Resolve reads angle-bracket runs in cue text as SRT formatting markup and strips unknown tags from display (standard subtitle semantics — the authored XML carries them escaped and intact).
200
+
201
+ With this, the native authoring matrix covers **every track type**: video (cuts, stacking, dissolves, retimes), audio (placements, crossfades), and subtitles — plus markers, start TC, generators, and five interchange formats in.
202
+
203
+ Suites: Node 842 pass / 0 fail; Python 3105 passed + 799 subtests.
204
+
205
+ ## What's New in v2.127.1 — audio ignores timemaps (measured)
206
+
207
+ Knowledge release closing the audio-retime question: a 50% keyed `Sm2TimeMap` on an imported **audio** clip *reads back* retimed (source 0..48 over 96 record frames) but **renders at 100%** — pitch and spectrum identical to the 1× reference. The audio engine ignores clip timemaps entirely while readback honors them: the readback/render divergence class, audio edition. Audio retimes remain honestly skipped, with the ledger reason now carrying the measurement.
208
+
209
+ Suites: Node 841 pass / 0 fail; Python 3105 passed + 799 subtests.
210
+
211
+ ## What's New in v2.127.0 — sequence picker and reel aliasing
212
+
213
+ Two conform-ergonomics upgrades surfaced by real turnover shapes:
214
+
215
+ - **Multi-sequence containers**: `assemble_from_interchange` gains `sequenceName` / `sequenceIndex` for AAF and `.prproj`. When exactly one sequence carries events it auto-picks; when several do, it refuses and lists them (`index:name`) instead of flattening into an overlap refusal.
216
+ - **Reel aliasing**: sources now group **by file**, not by reel — multiple reels mapped to one `mediaFilePath` (Avid mob vs tape names, re-linked dailies) merge into a single source with combined cuts, instead of demanding a captured template per reel name.
217
+
218
+ Suites: Node 841 pass / 0 fail; Python 3105 passed + 799 subtests.
219
+
220
+ ## What's New in v2.126.0 — AAF route fixed at the tool layer; harness parity
221
+
222
+ ### The last gap in the AAF story
223
+
224
+ Two closures:
225
+
226
+ **A since-birth bug, fixed.** `assemble_from_interchange` with `format: 'aaf'` fell through to the sync parser — which throws for AAF — so the tool-layer AAF route had *never* worked (every earlier proof called the parser library directly). The handler now awaits the async `parseAAF`, and `aaf.mjs` falls back to the repo venv's Python (where `pyaaf2` lives), so the route works with zero environment setup. A stubbed regression test pins it.
227
+
228
+ **Harness parity.** The shipped `capture_media_template` ran live for both fixture sources — capturing `mediaStartTime` 3600 and the native clip elements through the real code path — and the tool-handler route produced renders **identical** to the hand-verified E36 run (126.376 / 95.965 / 95.964 / 126.373, audio −21.08 dB). Nothing hand-rolled remains in the chain.
229
+
230
+ Suites: Node 840 pass / 0 fail; Python 3105 passed + 799 subtests.
231
+
232
+ ## What's New in v2.125.0 — the round-trip QC loop closes
233
+
234
+ ### Prove the conform, don't trust it
235
+
236
+ New `editorial.verify_roundtrip`: parse the original turnover, parse Resolve's own re-export of the timeline you authored from it, and get a verdict — normalized for the three conventions that otherwise drown the diff in noise:
237
+
238
+ - track labels (`V` ≡ `V1`)
239
+ - source naming (AAF mob name vs file basename, extension-stripped)
240
+ - source frames (Resolve's OTIO export is **timecode-absolute** — a constant per-source offset is fitted, reported, and enforced)
241
+
242
+ **Live proof:** rich AAF → `assemble_from_interchange` → import → Resolve's own OTIO export → `pass: true`, 4 pairs, `srcOffsets` = 86400 for both sources — exactly their 01:00:00:00 TC bases. The record geometry survives the entire loop to the frame.
243
+
244
+ Real drift still trips it: a 5-frame source slip or a 2-frame record slip returns `pass: false` with the mismatch kind and location.
245
+
246
+ Suites: Node 839 pass / 0 fail; Python 3105 passed + 799 subtests.
247
+
248
+ ## What's New in v2.124.0 — the Premiere leg; five formats proven
249
+
250
+ ### .prproj in, frames out — no Premiere required
251
+
252
+ `assemble_from_interchange` gains `format: 'prproj'`: the Premiere project is read **offline** (gunzip + object-graph walk), converted through the same authoring bridge, and lands as a linked, rendering `.drt`.
253
+
254
+ **Live proof through the actual tool handler:** a schema-faithful synthetic `.prproj` (two sources on V1 + an audio event) → `.drt` → import (3/3 linked, fresh project) → render: 122.99 / 234, audio −21.1 dB.
255
+
256
+ That makes **all five interchange formats route-proven end-to-end**: EDL, OTIO, AAF, FCP7 XML, and `.prproj` — parse → assemble → import → measured frames and RMS.
257
+
258
+ Also: the result note that still claimed "retimes are flattened and transitions become cuts" (stale since v2.111/v2.113) now states the authored-ledger truth.
259
+
260
+ Suites: Node 837 pass / 0 fail; Python 3105 passed + 799 subtests.
261
+
262
+ ## What's New in v2.123.0 — four formats proven; cross-link guard
263
+
264
+ ### Route coverage complete
265
+
266
+ **All four interchange formats — EDL, OTIO, AAF, and FCP7 XML — are now route-proven end-to-end**: parse → `assemble_from_interchange` → `.drt` → import → measured frames and RMS. The XMEML leg (E37): two sources cut on V1 (122.99 / 234) with an explicit A1 audio event continuing at −21.1 dB under the second cut.
267
+
268
+ **And the guard the merge law demands:** `import_timeline_checked` now cross-checks `.drt`/`.drp` imports — the archive's `<MediaFilePath>` set vs the files the imported items *actually* link to. A missing expected file returns `cross_link_warning` with the full `{expected, actual, missing}` comparison. This catches the coarse-identity cross-link that `linked == total` is provably blind to (the wrongly-linked items read back fully linked, wrong clip name and all).
269
+
270
+ Suites: Node 836 pass / 0 fail; Python 3105 passed + 799 subtests.
271
+
272
+ ## What's New in v2.122.0 — the AAF route, coast to coast
273
+
274
+ ### AAF in, frames out
275
+
276
+ The full route is proven: a rich Resolve-exported AAF → `assemble_from_interchange` → `.drt` → import → render, **every window frame-accurate** (Studio 19.1.3.7, headless):
277
+
278
+ | Window | Expected | Measured |
279
+ |---|---|---|
280
+ | V1 rt_source_1 | ~126 | 126.4 |
281
+ | V2 rt_source_2 stacked over V1 | ~96 | **95.97** |
282
+ | V1's rt_source_2 cut | ~96 | **95.96** |
283
+ | V1 rt_source_1 tail | ~126 | 126.4 |
284
+ | Audio (both source windows) | tone | −21 dB |
285
+
286
+ Channel-leg merge, V2 stacking, and TC-bearing sources (embedded 01:00:00:00 via `MediaStartTime`) verified in one render. The v2.120 native-donor clone path is now **render-verified**.
287
+
288
+ **New law (`api_truth`):** `ImportTimelineFromFile` merges pool media by a *coarse* identity across imports — two different files (different names and sizes, mtimes 1 s apart) carried identity blobs byte-identical except uuids, and in a non-empty project the second file's clips silently played the first file's picture. Fresh projects materialize both correctly; verify per-item paths (or render probes) after importing into non-empty projects.
289
+
290
+ Also recorded: the modal-wedge failure mode and its recovery (force-kill + headless relaunch; headless is *not* modal-immune — a would-be dialog hangs the call; a hard-wedged render has no API exit).
291
+
292
+ Suites: Node 836 pass / 0 fail; Python 3101 passed + 799 subtests.
293
+
294
+ ## What's New in v2.121.0 — one marker codec everywhere
295
+
296
+ Offline consolidation release (live validation is paused on a stuck Resolve dialog — see v2.120.0). All marker paths now share the single measured codec:
297
+
298
+ - `seq-container-builder` encodes lockable-blob markers with `timeline-markers-blob` (byte-exact vs a live Resolve export) instead of the deprecated simplified encoder
299
+ - `editorial.marker_roundtrip` adds a **binary** round-trip through the real codec, with provenance riding in `customData` — the result gains `blobRoundTrip`
300
+ - `parseOTIO` picks up **track-level** markers (record-time `marked_range`) alongside clip-level ones
301
+
302
+ Suites: Node 836 pass / 0 fail; Python 3101 passed + 799 subtests.
303
+
304
+ ## What's New in v2.120.0 — AAF channel-leg merge; native-donor path staged
305
+
306
+ ### The AAF leg, part one
307
+
308
+ Driving a real Resolve-exported AAF through `assemble_from_interchange` surfaced two truths and staged one architecture change:
309
+
310
+ - **AAF duplicates audio per channel.** Every A-track event in a rich Resolve 19 export arrives twice (one per channel leg). The bridge now merges identical legs instead of refusing them as a same-track overlap (`report.audioChannelLegsMerged`, tested); skipped-audio accounting corrected.
311
+ - **Embedded source timecode matters.** A `.mov` with embedded 01:00:00:00 fails the render with *"Full resolution media not found at 01:00:00:00"* — the native clip stores `<MediaStartTime>` in seconds where the template donor has 0. `capture_media_template` now harvests `mediaStartTime` plus the source's native timeline-clip elements.
312
+ - **Native-donor clone path (staged, live-unverified).** `cut-media` can clone the source's own captured clip (per track type, wrapper kept). Only caches carrying the new fields reach it — every existing capture keeps the proven donor path. Live verification is pending: a stuck Resolve modal (import-failure dialog) wedged the session mid-expedition — after it, even previously-proven files refused to import, so every later measurement was of the wedge, not the code. The resume plan is recorded.
313
+
314
+ Suites: Node 835 pass / 0 fail; Python 3101 passed + 799 subtests.
315
+
316
+ ## What's New in v2.119.0 — turnover markers ride the conform
317
+
318
+ ### Locators survive the trip
319
+
320
+ Editorial marks up a cut; the conform should keep those marks. Now it does: **EDL `* LOC:` locators** (the Avid convention) and **OTIO `Marker` objects** parse into the normalized event stream and come out the other end as real timeline markers in the assembled `.drt` — names, colors, exact frames.
321
+
322
+ **Full-route proof:** an EDL with two `LOC` lines imports as a timeline whose markers read back at exactly frames 24 and 60 with their names and mapped colors (Red / Green) through the marker API.
323
+
324
+ ### Changes
325
+ - `parseEDL`: `* LOC:` lines → `track: 'MARKER'` pseudo-events (never miscounted as skipped audio)
326
+ - `parseOTIO`: clip markers → record-position MARKER events
327
+ - `eventsToAssembleSpec`: authors `spec.markers`; interchange colors map onto the measured 16-color Resolve palette (MAGENTA→Fuchsia, ORANGE→Sand, WHITE→Cream, BLACK→Cocoa; unknown→Blue); `report.authoredMarkers`
328
+ - 2 new bridge tests
329
+
330
+ Suites after last edit: Node 834 pass / 0 fail; Python 3101 passed + 799 subtests.
331
+
332
+ ## What's New in v2.118.0 — timeline markers authored offline
333
+
334
+ ### Markers ride the .drt now
335
+
336
+ `drt.assemble` gains `spec.markers` — timeline markers with all 16 colors, names, notes, durations, and `customData`, authored fully offline and verified by API readback after import.
337
+
338
+ **The decode:** markers live in `project.xml` as a `Sm2SequenceLockableBlob` (owner = the timeline's `Sm2Sequence` DbId) wrapping a zstd-framed protobuf. Resolve itself emits **raw-block zstd** for small payloads and accepts it on import — so the codec needs no zstd library. The new `timeline-markers-blob.js` encoder is **byte-exact** against Resolve 19.1.3.7's own export (fixture checked in).
339
+
340
+ **The correction:** the legacy `marker-encoder.js` color map was wrong (Yellow is 16, not 8; Purple is 128, not 131072) and its output never matched a real export — now deprecated with a pointer. The full 16-color bit map was harvested live, one marker per color.
341
+
342
+ **Proof:** offline-authored markers (Red with note + duration 12; Mint with `customData`) read back perfectly through the marker API after import.
343
+
344
+ Suites after last edit: Node 832 pass / 0 fail; Python 3101 passed + 799 subtests.
345
+
346
+ ## What's New in v2.117.0 — start-timecode fidelity
347
+
348
+ ### The conform emulator keeps the real start TC
349
+
350
+ AAF/EDL turnovers rarely start at 01:00:00:00 — and until now the assembled timeline silently did. `assemble_from_interchange` gains `preserveStartTimecode: true`: the timeline starts at the turnover's **real first record frame** (the long-standing AAF rule "build at THAT start" — now automated).
351
+
352
+ **The discovery:** a timeline's start timecode lives in exactly one non-cosmetic place in a `.drp`/`.drt` — the pool `Sm2MpTimelineClip`'s `MediaExtents` blob, 16 bytes of LE doubles `[startSeconds, durationSeconds]`. Patch it offline, keep clips at absolute frames ≥ the new origin, and the import lands at the new start TC and renders.
353
+
354
+ **Proof:** offline patch to 02:00:00:00 → readback `02:00:00:00`, live frame; full route: a 00:59:52:00 EDL → timeline at 00:59:52:00 (86208–86304) with both sources rendering correctly.
355
+
356
+ ### Changes
357
+ - `drt.assemble`: `spec.startFrame` (frames @24; before-origin cuts still refuse, against the new origin)
358
+ - `assemble_from_interchange`: `preserveStartTimecode`
359
+ - `api_truth` MediaExtents entry; 2 new tests
360
+
361
+ Suites after last edit: Node 830 pass / 0 fail; Python 3101 passed + 799 subtests.
362
+
363
+ ## What's New in v2.116.2 — flat-target wording routed to assemble
364
+
365
+ Doc-clarity release from a post-release drift review (which found everything else clean — generated files, tool counts, api-limitations, version stamps). `convert_to_interchange`'s flat DRT target still flattens retimes by design, but the claim "the DRT clip schema has no per-clip speed field" read misleadingly now that `drt.assemble` authors retimes via `Sm2TimeMap` (v2.113+). The tool description and `resolve-advanced/README.md` now name the flat target explicitly and route to `drt.assemble_from_interchange` for authored retimes, dissolves, multi-track video, and audio.
366
+
367
+ Suites: Node 828 pass / 0 fail; Python 3101 passed + 799 subtests.
368
+
369
+ ## What's New in v2.116.1 — the flat-timemap divergence
370
+
371
+ Knowledge release. Freeze-frame probe: a **flat** keyed `Sm2TimeMap` (both keyframes at the same source Y) is the one timemap shape where readback and render *disagree in the trusting direction* — the imported item reads back frozen (source 96..96) but **renders moving** (48/48 unique frames). Freezes therefore stay in `flattenedRetimes` with the reason rather than being authored as flat maps. Recorded in `api_truth` (the readback-blind class now has a member that lies in both directions).
372
+
373
+ Suites: Node 828 pass / 0 fail; Python 3101 passed + 799 subtests.
374
+
375
+ ## What's New in v2.116.0 — audio cross-fades authored
376
+
377
+ ### The conform emulator learns audio cross-fades
378
+
379
+ An audio dissolve in interchange now becomes a **real, rendering cross-fade** in the assembled `.drt`.
380
+
381
+ **The harvest:** Resolve has no API for transitions, so we let it author one — an FCP7 `KGAudioTransCrossFade` imported via XMEML lands as an audio `Sm2TiTransition` (PrettyType "Final Cut Pro 7", which is what Resolve itself stores — and renders). That element is now a bundled template.
382
+
383
+ **Render proof:** the offline-authored crossfade's highpass-RMS **ramps** through the junction (−27.6 → −25.6 → −23.0 → −21.9 dB), identical in shape to a Resolve-authored control; a butt cut steps.
384
+
385
+ ### Changes
386
+ - `placeTransition` `trackType: 'audio'`; `drt.assemble` `transitions[].trackType`
387
+ - `eventsToAssembleSpec`: audio dissolves authored under the same abut/handle geometry; drops carry `trackType: 'audio'` and the reason
388
+ - XMEML gotcha recorded: an `<audio><channelcount>` block inside a *file definition* aborts the whole import silently
389
+ - 2 new bridge tests; template wrapper guard extended
390
+
391
+ Suites after last edit: Node 828 pass / 0 fail; Python 3101 passed + 799 subtests.
392
+
393
+ ## What's New in v2.115.1 — native DRT authoring guide
394
+
395
+ Documentation release: [docs/guides/native-drt-authoring.md](https://github.com/samuelgursky/davinci-resolve-mcp/blob/main/docs/guides/native-drt-authoring.md) consolidates the offline-authoring subsystem (v2.105–v2.115) — every capability with its spec surface, the four measured laws (Fusion comp byte-keyed cache, Fairlight strip, Sm2TimeMap generation split, timeline origin), the readback-is-blind verification doctrine, and a delivery checklist. AGENTS.md links it from the Conform/Interchange workflow row; per-IDE agent rules regenerated.
396
+
397
+ Suites: Node 826 pass / 0 fail; Python 3101 passed + 799 subtests.
398
+
399
+ ## What's New in v2.115.0 — audio authored: the Fairlight strip law
400
+
401
+ ### The conform emulator learns audio
402
+
403
+ A-track events in interchange now come out the other end as **real, playing audio clips** — the last big honesty-ledger item (`audioEventsSkipped`) falls.
404
+
405
+ **The law (measured by elimination):** audio tracks cannot be grown offline. The per-timeline Fairlight model (`FLStudioModelBA`, inside the media pool's `Sm2Sequence.FieldsBlob`) holds one mixer strip per audio track — a cloned track imports fine, reads back fine, and renders **silent**. We made the clip byte-identical to a live-authored one, the track byte-identical (`SubType` is the channel-format code — 1=mono — not an ordinal), shared the pool entry with a playing A1 clip: still silent. Only a template *captured* with the tracks plays. Audio aliveness is readback-blind — verify by rendered RMS.
406
+
407
+ **The fix is the capture-once pattern again:** the r19 media template was re-captured live with **8 mono audio tracks** (valid strips ride along). `audioOnly` cuts land on A1–A8 and render at native level; placements beyond the ceiling refuse with instructions.
408
+
409
+ **Full-route proof:** OTIO with V + two audio tracks → `.drt` → import → render: A1 tone −21.09 dB, A2 tone −24.08 dB (exactly the native control), video alive throughout.
410
+
411
+ ### Changes
412
+ - `drt.assemble`: `cuts[].audioOnly` + `track` (1–8); explicit audio suppresses the A1 convenience mirror; audio clones carry their own source identity (donor-identity clones were part of the silence)
413
+ - `eventsToAssembleSpec`: A-track events authored with per-track overlap checks; audio retimes skipped with reason; OTIO/EDL audio tracks numbered (`A`, `A2`, …)
414
+ - `api_truth`: Fairlight-strip entry (silent-failure class); 3 new tests
415
+
416
+ Suites after last edit: Node 826 pass / 0 fail; Python 3101 passed + 796 subtests.
417
+
418
+ ## What's New in v2.114.0 — reverse retimes authored
419
+
420
+ ### The last flattened retime falls
421
+
422
+ Reversed clips in interchange (OTIO negative `time_scalar`, XMEML/EDL reverse) are now **authored** into the assembled .drt — `flattenedRetimes` only holds zero-speed freezes.
423
+
424
+ **The shape:** reverse is the same r19 keyed `Sm2TimeMap` with the Y endpoints swapped — kf0=(0, YMax), kf1=(XMax, 0), a descending line. The encoder is **byte-exact** against Resolve 19.1.3.7's own −100% retime export.
425
+
426
+ **The In rule (measured):** for a reversed clip, `<In>` measures from the source **end**: `(sourceFrames − srcIn − dur×speed)/speed`. Offline proof: a reversed srcIn-24 dur-48 cut reads back source 71→23 — exactly the prediction — and renders 48 live frames.
427
+
428
+ ### Changes
429
+ - `drt.assemble`: `cuts[].reverse` (composable with `cuts[].speed`)
430
+ - `eventsToAssembleSpec`: reverse authored; ledger reasons updated
431
+ - `api_truth` timemap entry extended with the reverse shape + In-from-end rule
432
+
433
+ Suites after last edit: Node 823 pass / 0 fail; Python 3101 passed + 796 subtests.
434
+
435
+ ## What's New in v2.113.0 — retimes authored: the r19 Sm2TimeMap
436
+
437
+ ### The conform emulator learns speed
438
+
439
+ A 50% `LinearTimeWarp` in OTIO now comes out the other end as a **real retime** in the imported timeline — not a flattened 100% clip.
440
+
441
+ **The discovery:** `Sm2TimeMap` keyframes are generation-split. Resolve 21 stores protobuf points; Resolve 19 stores a keyed-dict of keyed-dict keyframes — and **19 silently ignores the protobuf form on import** (the clip reads back at 100%, no warning). The new `buildConstantSpeedTimemapKeyed` encoder emits the r19 form and is **byte-exact** against a timemap authored by Resolve 19.1.3.7 itself.
442
+
443
+ **Full-route proof:** OTIO `time_scalar: 0.5` → `assemble_from_interchange` → import → the item reads source 96..120 over 48 record frames (50% at source offset 96, the exact interchange intent) and renders live.
444
+
445
+ ### Semantics measured
446
+ - The timemap spans the **whole source** stretched by 1/speed; the clip's `<In>`/`<Duration>` window into it in **record-domain** frames (`srcIn` converts by `/speed`)
447
+ - Retimed cuts are video-only on A1 (audio would need its own timemap + pitch handling — stated in the ledger, not silent)
448
+ - Reverse still flattens, with the reason; `report.authoredRetimes` joins the ledger
449
+
450
+ ### Changes
451
+ - `drt.assemble`: `cuts[].speed` (forward constant, e.g. 0.5)
452
+ - `eventsToAssembleSpec`: forward speeds authored, reverse flattened with reason
453
+ - `api_truth`: generation-split entry (silent-failure class); harvest fixture + byte-exactness unit test
454
+
455
+ Suites after last edit: Node 822 pass / 0 fail; Python 3101 passed + 796 subtests.
456
+
457
+ ## What's New in v2.112.0 — multi-track video authoring
458
+
459
+ ### The conform emulator goes multi-track
460
+
461
+ Two-video-track interchange (OTIO/XMEML) now assembles into a .drt with real V2+ stacking — and it renders.
462
+
463
+ **Render proof (Studio 19.1.3.7):** two-track OTIO → `assemble_from_interchange` → import (3/3 linked) → render: V1 testsrc at 122.8/125.5 with the V2 white insert covering the middle at exactly **234**.
464
+
465
+ ### Changes
466
+ - `cutSourceIntoClips`: cuts gain `track` (1-based); missing video tracks grown as empty clones; track>1 cuts are **video-only** (their audio would overlap A1 — stated, not silent)
467
+ - OTIO/XMEML parsers number video tracks `V, V2, V3, …`; EDL stays single-V
468
+ - `eventsToAssembleSpec`: overlap judged **per video track** (V2 over V1 is legitimate geometry); dissolves match predecessors on their own track; ledger gains `upperTrackCutsVideoOnly`
469
+ - 2 new Node tests: V2 cut mapping, per-track overlap refusal naming the track
470
+
471
+ Suites after last edit: Node 820 pass / 0 fail; Python 3101 passed + 796 subtests.
472
+
473
+ ## What's New in v2.111.0 — dissolves authored coast-to-coast
474
+
475
+ ### The conform emulator learns dissolves
476
+
477
+ An EDL `D`-event now comes out the other end as a **real, rendering Cross Dissolve** — not a cut.
478
+
479
+ **Render proof (Studio 19.1.3.7):** an offline-authored `Sm2TiTransition` over transplanted cross-source media blends exactly through the cut — outgoing testsrc 123.9 → 130.8 → **181.6 at mid-dissolve** (predicted (124+234)/2 = 179) → 223.2 → incoming white 234. Transitions carry no Fusion comp, so the byte-keyed comp-cache law (v2.109.0/v2.110.0) does not apply: the harvested transition renders live on 19.
480
+
481
+ ### Changes
482
+ - `eventsToAssembleSpec` **authors** a cross-dissolve when the predecessor ends exactly at the cut and both sides have handle media for the centered span; every non-authorable dissolve stays in `droppedTransitions` **with the reason** (no abutting predecessor / insufficient handles, side named). The report gains `authoredTransitions`.
483
+ - Full route re-proven live: EDL `D 024` → `drt.assemble_from_interchange` → `.drt` → `timeline.import_timeline_checked` → render → 181.6 mid-blend.
484
+ - `drt` tool doc updated: transitions no longer "become cuts".
485
+ - 4 new Node tests cover the authored / no-incoming-handle / no-outgoing-tail / record-gap branches.
486
+
487
+ Suites after last edit: Node 818 pass / 0 fail; Python 3101 passed + 796 subtests.
488
+
489
+ ## What's New in v2.110.0 — offline generators render on 19; cache law scoped to titles
490
+
491
+ ### Element expedition, part two — generators are exempt
492
+
493
+ v2.109.0 mapped the law: imported Fusion comps on Resolve 19.x render only via the machine's byte-keyed Fusion disk cache. This release proves the carve-out: **built-in generators are plain `Sm2TiGenerator` clips with no Fusion comp, and they render live from a fully offline-authored .drt** — measured on Studio 19.1.3.7 over transplanted white media (YAVG 234):
494
+
495
+ | Element | YAVG | Verdict |
496
+ |---|---|---|
497
+ | Solid Color on V2 | 16.0 | alive — covers the white |
498
+ | half-coverage control | 16 / 234 in one render | discrimination clean |
499
+ | `PrettyType` → SMPTE Color Bar | 104.9 | bars render |
500
+ | `PrettyType` → Grey Scale | 125.1 | ramp renders |
501
+
502
+ So offline element authoring on pre-21 is real for generators (slates, leaders, bars, solids) — only Fusion **titles** remain cache-bound, with the live `timeline.set_title_text` post-import flow as the working alternative.
503
+
504
+ ### Changes
505
+ - `drt.assemble`: `elementsWarning` now fires **only for title elements** on pre-21 targets and documents verified generator kinds; spec doc lists `generatorName` options
506
+ - `api_truth`: generator exemption added to the byte-keyed cache-law entry; `api-limitations.md` regenerated
507
+ - **Version stamps unified**: v2.109.0's bump missed `install.py` and `src/granular/common.py` — the CI smoke test correctly **blocked** that npm publish (2.109.0 never reached npm). All four stamps now move together, enforced by `test_npm_package_metadata`
508
+ - New Node test: generator kind selection lands in the sequence XML; warning gate is title-only
509
+
510
+ Suites after last edit: Node 814 pass / 0 fail; Python 3101 passed + 796 subtests.
511
+
512
+ ## What's New in v2.109.0 — element render law: byte-keyed Fusion cache on 19.x
513
+
514
+ ### Element transplant expedition — verdict
515
+
516
+ **The law (measured on Studio 19.1.3.7):** a Fusion comp arriving via timeline import renders on Resolve 19.x **only** when the machine's Fusion disk cache holds frames keyed to the comp blob's *exact compressed bytes*. An identity recompression — byte-identical Lua, different zlib bytes, framing verified consistent — imported and read back perfectly but rendered black, while the untouched harvest rendered its cached frames. The live-render fallback for imported comps produces no frames on 19; Resolve 21-generation hosts render imported comps live (where the title/generator primitives were originally proven).
517
+
518
+ **Consequence:** offline text patching of Fusion comps for a 19.x host is impossible *by design* — no valid re-encoding can hit the byte-keyed cache.
519
+
520
+ **The working pre-21 flow:** `drt.assemble` media offline (native-descriptor transplant renders everywhere), then set title text **post-import** with `timeline.set_title_text` (its Fusion-comp write path is live-verified on 19.1.3).
521
+
522
+ ### Changes
523
+ - `composition-text`: wrong plaintext dual-mode branch reverted (both generations share identical nested framing); law documented at `rewriteInner`
524
+ - r19 title/generator snippets `<Element>`-wrapped (raw clips concatenated into Items made render jobs fail with no status); guard test added
525
+ - `snippetPathFor(templateVersion)` selects r19 snippets for pre-21 targets; `drt.assemble` `elementsWarning` now states the law and the working flow
526
+ - `api_truth`: new entry *Imported Fusion comps render via byte-keyed disk cache on 19.x*; `api-limitations.md` regenerated
527
+
528
+ Suites: Node 813 pass / 0 fail; Python 3101 passed + 796 subtests.
529
+
5
530
  ## What's New in v2.108.0
6
531
 
7
532
  **The conform emulator, coast to coast.** An interchange file goes in; an
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [简体中文](README.zh-CN.md)
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.108.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.137.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#server-modes)
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.108.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.137.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#服务器模式)
@@ -12,7 +12,7 @@
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
13
13
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
14
14
 
15
- > 本翻译对应 v2.108.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.137.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -13,6 +13,27 @@
13
13
  import { fileURLToPath, pathToFileURL } from 'node:url';
14
14
  import path from 'node:path';
15
15
 
16
+ // Node floor (package.json engines: >=20.9), enforced at STARTUP rather than
17
+ // discovered per-feature: under an old Node the pure-JS tools limp along
18
+ // while native-dep paths (better-sqlite3: project_read, fairlight DB actions,
19
+ // offline_ref live linking) die with a cryptic NODE_MODULE_VERSION mismatch —
20
+ // measured live under nvm's v18.20.8, which is exactly what a bare "node"
21
+ // command in an MCP registration resolves to on a machine whose shell
22
+ // default lags. Silent degradation is this repo's least favorite failure
23
+ // mode; refuse loudly with the fix instead.
24
+ const [major, minor] = process.versions.node.split('.').map(Number);
25
+ if (major < 20 || (major === 20 && minor < 9)) {
26
+ process.stderr.write(
27
+ `[davinci-resolve-advanced-mcp] Node ${process.versions.node} is below the ` +
28
+ `supported floor (>=20.9). This process was started by: ${process.execPath}\n` +
29
+ `Fix: point the MCP registration's command at a Node >=20.9 binary ` +
30
+ `(e.g. the absolute path from \`nvm which 20\`), or update the default ` +
31
+ `node on PATH. Re-running install.py also rewrites client configs with ` +
32
+ `an absolute, version-checked node path.\n`,
33
+ );
34
+ process.exit(1);
35
+ }
36
+
16
37
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
38
  const serverEntry = path.resolve(__dirname, '..', 'resolve-advanced', 'server', 'index.mjs');
18
39