v0.21.0 — the doors that were only open from the inside
Four API gaps, all found by building kadr-persistence against the published API rather than from inside the package, where the internal initialisers are in reach. Each is a case where kadr produces a value it gives you no way to reconstruct — which is exactly what a save/reopen cycle has to do.
Control flow in Video.audio { }
AudioBuilder had only buildBlock, so the closure took a literal list of tracks and nothing else. A caller holding [AudioTrack] had no way in: audioTracks is internal(set), and the builder was the only public door.
video.audio {
for bed in savedTracks { bed }
if includeVoiceover { AudioTrack(url: voiceoverURL) }
}VideoClip.filter(_:id:animation:)
Every other filter modifier calls FilterID.generate(), and the only initialiser accepting explicit filterIDs is internal — so a filter identity could be read but never written back. Restoring a saved clip produced fresh ids, orphaning any animation bound with filterAnimation(for:) and dropping any UI selection keyed to them.
Video { } compiles
An empty block was ambiguous between VideoBuilder's two variadic buildBlock overloads, so the empty composition — a new project, or one whose last clip was just deleted — could not be written as a literal at all.
Preset is Equatable
An enum with associated values and no conformance, while ExportQuality has been Equatable since it shipped.
How all four were missed. None is reachable from the existing tests: the audio tests build their tracks as literals, and the filter-id tests construct clips through the internal memberwise initialiser — the one door a client does not have. The new test files use a plain import Kadr, not @testable, so they see what a client sees.
631 tests pass. All changes additive and source-compatible.