davinci-resolve-mcp 4.5.2 → 4.6.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,107 @@
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 v4.6.0 — a default project archive no longer crashes Resolve 21.1
6
+
7
+ The archive coverage measured by @legionsound in [#233](https://github.com/samuelgursky/davinci-resolve-mcp/pull/233), plus the offline node-graph relayout work that was sitting unreleased on `main`.
8
+
9
+ ### Fixed
10
+
11
+ - **`project_manager` `archive` defaulted source media and render cache ON, and on
12
+ Resolve 21.1.0.14 a default call crashes Resolve.** ([#233](https://github.com/samuelgursky/davinci-resolve-mcp/pull/233), @legionsound)
13
+ Measured on Studio 21.1.0.14, one isolated call per case on a disposable project:
14
+ with source media and proxies off, `ProjectManager.ArchiveProject` returns `False`
15
+ instantly and writes nothing, open or closed, render cache on or off. With either
16
+ `isArchiveSrcMedia` or `isArchiveProxyMedia` on it creates an empty directory at the
17
+ target and Resolve SIGSEGVs in the same second (4 of 4; flags-off 0 of 5), the call
18
+ comes back through the bridge as `None`, and unsaved work in the open project is
19
+ lost. A file or folder already at the target survived every case byte for byte, so
20
+ the destination is never the casualty. 19.1.3.7 agrees where it was measured: this
21
+ repo's 2026-08-02 mode matrix recorded `False` for `.dra` and folder paths with
22
+ every flag off. **No scriptable call on either build has produced an archive.**
23
+ - New `src/utils/archive_guard.py`, shared by the compound `archive`,
24
+ `safe_project_archive`, and the granular `archive_project`: every flag defaults
25
+ off; only real booleans are accepted (`bool("false")` is `True`, and two of these
26
+ flags crash); `src_media` and `proxy_media` are refused unless
27
+ `acknowledge_trap=true`; the result reports the native return as observed —
28
+ `True`, `False` ("wrote nothing"), or `None` ("likely crashed; check Resolve is
29
+ running") — instead of a bare bool.
30
+ - `safe_project_archive` keeps `allow_media_archive` as the size guard and now also
31
+ needs `acknowledge_trap` for the crashing flags. One guards size, the other the
32
+ crash.
33
+ - Granular `archive_project` now carries the `DESTRUCTIVE_TOOL` annotation and
34
+ `@granular_destructive_op()`. It previously fell through to plain `WRITE_TOOL`
35
+ (`archive_` is in no verb table) with no gate.
36
+ - Both compound actions are in the `destructive_hook` registry and rated MEDIUM in
37
+ the risk sets, off the ratchet backlog; `safe_project_archive` honours `dry_run`
38
+ natively and is in `NATIVE_DRY_RUN_ACTIONS`. The refusal is at parameter level
39
+ rather than a symbol-level `destroys_prior_work`, because a flags-off call is a
40
+ harmless no-op that should not need acknowledgement.
41
+ - `api_truth`: new `ProjectManager.ArchiveProject` entry (`verified_on:
42
+ 21.1.0.14`, tagged `reported` — the contributor's measurement) and
43
+ `ACTION_SYMBOLS` for both actions, so the fact rides on every result as a
44
+ `known_limitation`. `docs/reference/api-coverage.md` row corrected from "API
45
+ accepts; archiving is slow"; evidence in `docs/reference/project-archive.md` and
46
+ `docs/reference/evidence/project-archive-21.1.json`.
47
+ - Not measured: `RestoreProject` round-trip (nothing produced an archive to
48
+ restore), render cache on a project that has one, headless 21.1, builds after
49
+ 21.1.0.14, Windows/Linux, and whether the media flags crash 19.1.3.7 — this
50
+ machine's Resolve was left alone rather than risked on it.
51
+
52
+ ### Added
53
+
54
+ - **`drp` `relayout_node_graphs` — whole-project Cleanup Node Graph on an exported `.drp`.**
55
+ The UI command has no scripting API and the two existing paths each had a gap: `drx`
56
+ `relayout` is one clip at a time, and `project_db` `relayout_node_graphs` needs the
57
+ project closed plus a full Resolve quit. This one runs on `ExportProject` output and
58
+ round-trips through `ImportProject` as a sibling, so the open project is never touched.
59
+ - Indexes **every** node graph the export carries: every LOCAL version of every timeline
60
+ clip (a clip with three versions is three graphs — the earlier `extract_node_graphs` /
61
+ `inject_grades` only ever saw the first `<Body>`), remote versions on media-pool clips,
62
+ group pre/post graphs (`project.xml`), timeline-level graphs.
63
+ - Scope by anything the `.drp` can name: timeline globs, video track, clip id, clip name /
64
+ media globs, absolute frame range (overlap), clip position range, color group (a group
65
+ = its clips + its own graphs), graph kind, active-only / version-name globs, node count,
66
+ node label. Several scopes union. Unknown selectors are refused (zod `.strict()`), never
67
+ ignored — an ignored selector would silently widen the sweep to the whole project.
68
+ - Byte-preserving: only the position varints move (node-layout.js); `HasCorrection` is
69
+ left exactly as found (this is not a grade change). Resolve's 0x80 **STORED** body
70
+ container (how it serialises small/default graphs in exports) is now decoded and
71
+ re-wrapped in kind — before this every ungraded clip read as "not a grade Body".
72
+ - The default body carries no node message at all (Resolve creates the node lazily in
73
+ the UI): reported as `empty`, not skipped, not an error. Measured on a 737-clip
74
+ export: 746 empty, 3 real graphs, 0 skipped.
75
+ - Write path re-indexes the written file from scratch: same graph count, every rewritten
76
+ body at the target positions with its node count intact, every untouched body
77
+ byte-identical, every flag unchanged — a failed read-back deletes the output and throws.
78
+ - Tests: `test/drp-node-graph-relayout.test.mjs` — fixture in the measured Resolve 19.1.3
79
+ shapes with a two-version clip, a remote version, group graphs, an empty default, an
80
+ undecodable body; the full scope matrix incl. a null control; write + idempotence +
81
+ layout tuning.
82
+ - **Topology-aware relayout (node-layout.js, all three relayout paths).** Positions now
83
+ come from the graph's own wiring: F8 connection messages (from/to node id, ports —
84
+ 64 = RGB path, 16 = key link — input slot, creation order) and the F9/F10 markers
85
+ naming the chain's first and last node. Nodes are ranked by longest RGB path (x), a
86
+ fan-out stacks its branches into lanes ordered by the mixer input slot they feed (y),
87
+ a merge returns to its lowest input lane, key links never move a node, and an
88
+ unrankable graph (cycle) falls back to a row in index order and says so in `meta`.
89
+ - Fixes a latent bug in the row layout: it placed nodes in LIST order, and Resolve
90
+ lists nodes by id, so any chain with a node inserted mid-way (every traced KICK
91
+ graph: index order 4,3,6,5,2,7,8,9,1) was laid out scrambled. A chain now lands on
92
+ the measured row in chain order; generator chains are byte-identical to before.
93
+ - New options everywhere (`drx` `relayout`, `drp` `relayout_node_graphs`,
94
+ `project_db` `relayout_node_graphs`): `spacingY` (lane pitch, default 178 —
95
+ Resolve's own vertical placement grid seen in every stacked export) and
96
+ `layout`/`mode` = `topology` (default) | `row` (the old behaviour). Every result
97
+ carries `layout: {kind: chain|dag|row, ranks, lanes, keyLinks}`; the sweep report
98
+ counts `stackedGraphs`.
99
+ - NOT yet measured: native Cleanup Node Graph on a graph with a parallel/layer
100
+ mixer (the lane pitch and how Resolve orders branches). The x row is measured;
101
+ the lane pitch is a documented default until a mixer graph is cleaned natively
102
+ and read back. `test/node-layout-topology.test.mjs` pins the planner on rewired
103
+ real bodies (fan-out + merge, slot order, key link, cycle, row mode, single node).
104
+
105
+
5
106
  ## What's New in v4.5.2 — granular safety stops guessing, and the audit log stops lying
6
107
 
7
108
  Two findings from a review of the v4.5.0 enforcement hook, both measured before and
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-4.5.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.6.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-37%20(387%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-4.5.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.6.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-37%20(387%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
- > 本翻译对应 v4.5.2 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v4.6.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
package/docs/SKILL.md CHANGED
@@ -403,7 +403,17 @@ Operating rules an agent must know:
403
403
  has no API). Single clip, live: `gallery_stills.grab_and_export` → advanced
404
404
  `drx(action="relayout")` → `graph.reset_all_grades` → `safe_apply_drx` with
405
405
  EXPLICIT item indices (the reset is required — a same-structure apply keeps
406
- the old layout). Whole project, offline: `project_db(action="relayout_node_graphs")`.
406
+ the old layout). Whole project, offline: `project_db(action="relayout_node_graphs")`
407
+ (closed project + quit/relaunch). Whole project or ANY SUBSET **without closing it**:
408
+ `project_manager.export_project` → advanced `drp(action="relayout_node_graphs")`
409
+ (scope by timeline/track/clip id/name/media/frames/clip position/group/version/node
410
+ count/node label; covers every LOCAL version of every clip, remote versions, group
411
+ pre/post and timeline graphs; dry-run first, read-back verified on write) →
412
+ `project_manager.import_project` as a sibling `<name>_CLEANED`, then re-export THAT
413
+ and dry-run again to prove the effect. BPA's "Node Graph Cleanup" job is this loop.
414
+ Layout is topology-aware (rank by RGB wiring, branches stacked into lanes at
415
+ `spacingY`, key links untouched); the lane pitch is NOT measured against native
416
+ Cleanup on a mixer graph yet — the x row is.
407
417
  - **project_db patches** require the project CLOSED in Resolve plus
408
418
  `iConfirmProjectClosed:true`; every write auto-backs-up and read-back
409
419
  verifies. Resolve caches open projects in memory: after patching, fully QUIT
@@ -820,7 +830,11 @@ lifecycle, settings, database, preset, and archive boundary helpers:
820
830
  - `safe_project_create(name, media_location_path?, dry_run?)`
821
831
  - `safe_project_export(name, path, with_stills_and_luts?, dry_run?)`
822
832
  - `safe_project_import(path, name, dry_run?)`
823
- - `safe_project_archive(name, path, src_media=false, render_cache=false, proxy_media=false, dry_run?)`
833
+ - `safe_project_archive(name, path, src_media=false, render_cache=false, proxy_media=false, allow_media_archive?, acknowledge_trap?, dry_run?)`
834
+ — `src_media` and `proxy_media` crash Resolve 21.1.0.14 (reported, #233); they are refused
835
+ unless `acknowledge_trap=true`, and every flag defaults off on `archive` too. No
836
+ scriptable call has produced an archive on 19.1.3.7 or 21.1.0.14; see
837
+ `docs/reference/project-archive.md`.
824
838
  - `safe_project_restore(path, name, dry_run?)`
825
839
  - `safe_project_delete(name, close_current?, dry_run?)`
826
840
  - `safe_set_project_settings(settings, restore?, dry_run?)`
@@ -155,6 +155,9 @@ Cross-server rules an agent must know:
155
155
  `drx(action="relayout")` → `graph.reset_all_grades` → `safe_apply_drx` with
156
156
  explicit indices (a same-structure apply keeps the OLD layout — the reset is
157
157
  required). Whole project, offline: `project_db(action="relayout_node_graphs")`.
158
+ Any scope, open project: export → `drp(action="relayout_node_graphs")` (every
159
+ version of every clip + group/timeline graphs; dry-run, read-back verify) →
160
+ import as a sibling → re-export the sibling and dry-run again.
158
161
  - **Deps.** The grading catalog needs `sharp`; call the advanced `capabilities`
159
162
  tool for live status and install hints.
160
163
 
@@ -251,7 +251,7 @@ Every method in the DaVinci Resolve Scripting API and its test status. Methods a
251
251
 
252
252
  | # | Method | Status | Test Result / Notes |
253
253
  |---|--------|--------|---------------------|
254
- | 1 | `ArchiveProject(projectName, filePath, ...)` | ⚠️ | API accepts; archiving is slow |
254
+ | 1 | `ArchiveProject(projectName, filePath, ...)` | ⚠️ | Never produces an archive by script: returns `False` and writes nothing with source media and proxies off; either on crashes Resolve 21.1.0.14. Wrappers default every flag off and refuse the crashing two unless `acknowledge_trap`. See [project archive](project-archive.md). |
255
255
  | 2 | `CreateProject(projectName, mediaLocationPath)` | ✅ | Creates new project; optional media location added in Resolve 20.2.2 |
256
256
  | 3 | `DeleteProject(projectName)` | ⚠️ | Returns `False` if project is open |
257
257
  | 4 | `LoadProject(projectName)` | ✅ | Returns Project object |
@@ -12,7 +12,7 @@ that none exists).
12
12
 
13
13
  **Verified on:** DaVinci Resolve Studio 21.0.2
14
14
 
15
- **Totals:** 41 missing capabilities, 53 bugs / unreliable behaviors.
15
+ **Totals:** 41 missing capabilities, 54 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
@@ -776,3 +776,11 @@ values, or automation-hostile modal prompts.
776
776
  - **Behavior:** Gated on the Color page. Measured on all six pages: returns False from media, edit, fusion, fairlight and deliver, and True only from color. The refusal is a bare False with no reason. It does at least fail cleanly - no file is written on the failing pages, so there is no stale-file trap here.
777
777
  - **Workaround / current handling:** resolve.OpenPage('color') before the call and restore the prior page afterwards. Treat a False as 'you were on the wrong page' before suspecting the path.
778
778
  - **Tags:** page-gated, silent-failure, lut
779
+
780
+ ### ProjectManager.ArchiveProject
781
+
782
+ - **Object:** `ProjectManager`
783
+ - **Signature:** `(projectName, filePath, isArchiveSrcMedia=True, isArchiveRenderCache=True, isArchiveProxyMedia=False) -> bool`
784
+ - **Behavior:** No scriptable call produces an archive. With source media and proxies off it returns False instantly and writes nothing, for an open or a closed project and with render cache on or off. With isArchiveSrcMedia or isArchiveProxyMedia on it creates an empty directory at the target and Resolve crashes (SIGSEGV) in the same second; the call comes back through the bridge as None and every later handle is dead. Four crashes, one of them in a Blackmagic Cloud library, share identical top stack frames in Fusion script-symbol teardown on the UI thread; a separate crash in the same session during DeleteProject/LoadProject had a different stack, so the signature belongs to the archive calls. Media-flag calls crashed 4 of 4, flags-off calls 0 of 5. A file already at the target survived every case byte for byte, including a crash, so the destination is never overwritten; unsaved work in the open project is what is lost. The native defaults turn source media on, so a default call crashes Resolve. Resolve logs nothing about the False returns.
785
+ - **Workaround / current handling:** Keep isArchiveSrcMedia and isArchiveProxyMedia off unless you have verified the build, and save every open project first. Treat False as 'nothing archived', not as a path problem: a .dra and a folder-style path fail identically. Archive from the Project Manager UI when you need a real archive.
786
+ - **Tags:** crash, unreliable-return, silent-failure, project, reported
@@ -0,0 +1,81 @@
1
+ # Project archive on Resolve 21.1
2
+
3
+ `ProjectManager.ArchiveProject` has no `api_truth` entry until now, and both of
4
+ this server's archive actions sat in the ratchet's unrated backlog. The native
5
+ defaults, `isArchiveSrcMedia=True` and `isArchiveRenderCache=True`, were
6
+ inherited by both the compound `project_manager archive` action and the granular
7
+ `archive_project` tool. On Studio 21.1.0.14 a default call crashes Resolve.
8
+
9
+ ## Measured
10
+
11
+ Studio 21.1.0.14, local disk database, one disposable project with one synthetic
12
+ 2-second clip on one timeline, archived by name. One isolated call per row, with
13
+ breadcrumbs written to disk before and after so a crash cannot hide which call
14
+ caused it. Presence was checked with `dir()` membership against a known-present
15
+ control, not `hasattr`, which Resolve answers True for any name.
16
+
17
+ | Case | Returns | Effect |
18
+ | --- | --- | --- |
19
+ | Open project, all flags off | `False` | nothing written, instantly |
20
+ | Closed project, all flags off | `False` | nothing written, instantly |
21
+ | Closed project, render cache only | `False` | nothing written, instantly |
22
+ | Closed project, source media | `None` | empty directory at the target, then Resolve crashes |
23
+ | Closed project, proxy media | `None` | empty directory at the target, then Resolve crashes |
24
+ | Unrelated file at target, flags off | `False` | file byte-identical |
25
+ | Populated directory at target, flags off | `False` | untouched |
26
+ | Source media onto an existing file | `None` | Resolve crashes; file byte-identical |
27
+
28
+ The crash is SIGSEGV, stamped the same second as the call. A crashing call comes
29
+ back through the bridge as `None`, not a bool, and every handle after it is dead.
30
+ Resolve logs nothing about the `False` returns.
31
+
32
+ **Media-flag calls crashed 4 of 4; flags-off calls crashed 0 of 5.** The four
33
+ crashes, one of them in a Blackmagic Cloud library on an earlier attempt, share
34
+ identical top stack frames in Fusion script-symbol teardown on the UI thread. A
35
+ fifth crash in the same session, during `DeleteProject` then `LoadProject` 44
36
+ seconds after a relaunch, has a different stack past the generic crash-handler
37
+ frames, so the archive signature is not a general instability. Crash uptimes were
38
+ 1:45, 14:16, 1:31 and 0:45.
39
+
40
+ **The destination is never the casualty.** A file or folder already at the target
41
+ survived every case byte for byte, including the crash. Resolve cannot create its
42
+ bundle folder over an existing file and crashes anyway, so the crash is in starting
43
+ the archive job, not in writing the destination. What is lost is unsaved work in
44
+ the open project.
45
+
46
+ 19.1.3.7 agrees on the part it measured: this repo's mode matrix (2026-08-02, GUI
47
+ and headless) and the project-lifecycle kernel recorded `False` for both a `.dra`
48
+ and a folder-style path with every flag off. No scriptable call on either build has
49
+ produced an archive.
50
+
51
+ ## What changed
52
+
53
+ - `src/utils/archive_guard.py`: every flag defaults off; only real booleans are
54
+ accepted, since `bool("false")` is `True` and two flags crash; source media and
55
+ proxy media are refused unless `acknowledge_trap=true`; the result reports the
56
+ native return as observed (`True`, `False` with "wrote nothing", or `None` with
57
+ "likely crashed") rather than a bare bool.
58
+ - Compound `project_manager archive` and `safe_project_archive` use it.
59
+ `safe_project_archive` keeps its existing `allow_media_archive` size guard and
60
+ now also needs `acknowledge_trap` for the crashing flags: one guards size, the
61
+ other the crash.
62
+ - Granular `archive_project`: defaults off, `acknowledge_trap` for the crashing
63
+ flags, `DESTRUCTIVE_TOOL` annotation and `@granular_destructive_op()`.
64
+ - Both compound actions are registered, rated MEDIUM, and removed from the ratchet
65
+ backlog. `safe_project_archive` honours `dry_run` natively and is listed as such.
66
+ - `api_truth` gains a `ProjectManager.ArchiveProject` entry and `ACTION_SYMBOLS`
67
+ maps both compound actions to it, so the measured fact rides on every result.
68
+
69
+ ## A design question for the maintainer
70
+
71
+ The entry deliberately does **not** set `destroys_prior_work`. That flag is
72
+ symbol-level, so every mapped action would refuse until acknowledged, including
73
+ the flags-off call, which is a harmless no-op. The refusal here is parameter-level
74
+ instead, in the wrappers. If the symbol-level trap is preferred, it is a one-line
75
+ change plus the granular confirmation tier.
76
+
77
+ ## Not tested
78
+
79
+ No scriptable call produced an archive, so a `RestoreProject` round-trip could not
80
+ be measured. Also untested: render cache on a project that has one, headless mode
81
+ on 21.1, builds after 21.1.0.14, and Windows or Linux.
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "4.5.2"
40
+ VERSION = "4.6.0"
41
41
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
42
42
  # Resolve's scripting bridge loads into newer interpreters on recent builds
43
43
  # (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": "4.5.2",
3
+ "version": "4.6.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -79,6 +79,16 @@ Each dispatches on an `action`. Highlights:
79
79
  - **`drx`** — per-clip grade (`.drx`) codec: `parse`, `generate`, `generate_from_request`,
80
80
  `export_cdl`, `merge`, plus the **grading/QC catalog** below.
81
81
  - **`drp` / `drt`** — project / timeline file authoring + editing + grade injection + structural diff.
82
+ `drp` `relayout_node_graphs` is the whole-project **Cleanup Node Graph** (the UI command has
83
+ no API): every node graph an exported `.drp` carries — every LOCAL version of every clip,
84
+ remote versions, group pre/post, timeline-level — relaid out to Resolve's clean row with the
85
+ grade bytes untouched and `HasCorrection` as found; scoped by timeline / track / clip id /
86
+ name / media / frame range / clip position / color group / version / node count / node
87
+ label; dry-run report, read-back verify. Round trip: `export_project` → `relayout_node_graphs`
88
+ → `import_project` as a sibling. Single `.drx`: `drx` `relayout`; closed `Project.db`:
89
+ `project_db` `relayout_node_graphs`. All three are topology-aware: nodes rank by their
90
+ RGB wiring (chain order, not list order), branches stack into lanes, key links stay put;
91
+ `layout: row` is the old single-row mode.
82
92
  - **`conform`** — offline conform/relink QC engine (frame-oracle math, not filename matching),
83
93
  reverse-clip DB repair, sequence lineage store + diff, per-cut frame QC (sampled clear of
84
94
  transition windows; ingests Resolve's own FCP7 export — `-1` edges → junctions, no ticks needed).