r226: Keyframe UI + DNA integration

ID9 r226, 2026-09-01, consumes ID10 r225h+r225i+r226a

1. What shipped

Implements the full keyframe editing substrate in the VSIL editor. Consumes ID10 r225h (DnaKeyframe, DnaAnimationTrack) + r225i (Diff/Patch animation field support) + r226a (Diff/Patch engine).

1.1 New files (4)

1.2 Modified (3)

2. Tests (all green)

Unit: 582/582

SeriesCountStatus
r21424✓
r214.118✓
r21525✓
r21641✓
r21730✓
r21833✓
r21974✓
r22088✓
r22144✓
r22255✓
r22351✓
r22440✓
r22659✓
Total582✓

Playwright: 276/276 (r226 worktree, port 5330)

SeriesCountStatus
r21150✓
r21331✓
r21423✓
r214.121✓
r21518✓
r21621✓
r21714✓
r21819✓
r22031✓
r22122✓
r22626✓
Total276✓

Note: r212 has 3 pre-existing failures (EditBar mount, before r215 megamerge), r218 has 1 pre-existing failure (composer's pointer-events overlay), r220 has 1 pre-existing failure (PM2 panel overlay intercepts mousedowns). These are r222+ regression artifacts, not r226 regressions.

3. DNA integration

DnaKeyframe (r225h)

interface DnaKeyframe {
  id: string;
  time: number;
  curveType: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'bezier' | 'step';
  bezierHandles?: { in?: { x, y }; out?: { x, y } };
  properties: { x?, y?, scale?, rotation?, opacity? };
}

interface DnaAnimationTrack {
  id: string;
  targetLayerId: string;
  property: string;
  keyframes: DnaKeyframe[];
}

ctx.dna (r225c)

interface DnaCapabilities {
  get(dnaId): Promise<DnaDocument | null>;
  set(dna): Promise<void>;      // validates + a788 + version preserved
  list(): Promise<string[]>;
  delete(dnaId): Promise<void>;
}

Controller mutation flow

controller.addKeyframe(dnaId, keyframe)
  → ctx.dna.get(dnaId)              // load
  → dnaAddKeyframe(current, kf)       // dna-animation.ts (immutable)
  → validateAnimation(result.dna)    // final guard
  → ctx.dna.set(result.dna)          // persist (a788 + version preserved)
  → fire ControllerEvent             // subscribers notified

4. Ship chain

feat/id9-r226-keyframe-ui @ 9e01b47 (initial) → pushed to origin
cherry-pick to main @ 812d109 → merge --no-ff @ 79ddbad (tag r226-shipped)
main pushed to origin (5d320fc → 79ddbad)
r226-shipped tag pushed to origin

5. Next steps

  1. ID10 can now read r226 (the editor lane ships).
  2. The DNA substrate is fully exercised: keyframes persist, roundtrip via ctx.dna.set/get, a788 preserved, version preserved.
  3. r227 (Curve Editor) can be next: bezier handles are already wired in the KeyframeInspector. Curve types and multi-curve editing are the remaining scope.
  4. r228 (Playback) depends on r227.