feat(editor): add source-aware DAW track session - #285
ChrisBeWithYou wants to merge 21 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis change adds persisted source-aware track sessions with audio-source selection, transcription pairing, folder organization, and tempo-guide controls. It also introduces cached reference switching, multitrack playback, master mixer routing, a bottom mixer drawer, silent edit previews, XML encoding normalization, and related tests and documentation. ChangesTrack session and mixer integration
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Editor
participant TrackSession
participant Host
participant Audio
participant Mixer
Editor->>TrackSession: Initialize and render session
TrackSession->>Host: Select target or source
Host->>Audio: Activate cached audio source
TrackSession->>Mixer: Update strip state
Mixer->>Host: Notify mix change
Host->>Audio: Apply track mix and reference gain
Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@assets/v3-theme.css`:
- Around line 538-543: Update the `#plugin-editor` .editor-mixer-drawer styles so
the hidden class remains effective: remove the explicit display: flex
declaration or add a more-specific .editor-mixer-drawer.hidden rule with
display: none, preserving the drawer’s collapsed state when closed.
In `@routes.py`:
- Line 4005: Extend the create/conversion flows to parse track_session like the
/save flow and pass it through to _write_sloppak_pak in both save_as_sloppak and
/build. Ensure newly created or converted sessions preserve folders, pairings,
and tempo-guide settings.
- Around line 3804-3817: Update the stem cache filename construction that
populates _stem_urls so each cached path includes a unique per-source index or
collision suffix, preventing sanitized IDs from overwriting one another. Keep
the source ID generation and audio_sources mapping in the shown loop consistent
with the resulting cached URLs.
In `@src/audio.js`:
- Around line 73-76: Update the cached-source branches in the audio handling
flow, including the branch around cached and _installDecodedAudio, to call
cancelAudioLoad() before installing either cached master or stem audio. Ensure
both cache-hit paths invalidate pending loads and advance the generation before
_installDecodedAudio runs, preventing an older decode from replacing the newly
selected source.
In `@src/file-ops.js`:
- Around line 177-180: Update the flow before installTrackSession to populate
the synthesized Master Mix source with data.audio_url when normalization creates
it with an empty URL. Ensure activateTrackAudioSource can validate the Master
Mix against the cached audio, while preserving existing audio_sources handling
for servers that provide a populated master source.
In `@src/track-session.js`:
- Around line 288-293: Update the select action in the track-session control
handler so an unpaired transcription falls back to the Master Mix source rather
than S.trackSession.tempoGuideSourceId. Preserve the pairedSourceId selection
when an explicit pairing exists, and continue passing the resolved
S.focusedSourceId to host.selectTrackSessionSource.
- Around line 61-87: Update the track normalization logic around audioTrackId
and transcriptionTrackId so persisted folders or unrelated leaves cannot cause
canonical source/target tracks to be omitted via seen.has(id). Reserve canonical
IDs only for their matching leaf types and source/target identifiers, or assign
a collision-free ID to conflicting persisted items, while preserving
deduplication for valid matching leaves.
In `@tests/test_gp_xml_encoding.py`:
- Around line 22-30: Update test_generated_utf8_xml_is_left_byte_identical to
read the file bytes after _normalize_generated_xml_encoding([path]) and assert
they exactly match the original payload, preserving the valid UTF-8 byte
identity guarantee.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 0a696eba-09b5-4420-8d99-63de005d913b
📒 Files selected for processing (18)
CHANGELOG.mdassets/v3-theme.cssdocs/USER-GUIDE.mdroutes.pyscreen.htmlsrc/audio.jssrc/file-ops.jssrc/host.jssrc/main.jssrc/mixer-panel.jssrc/state.jssrc/track-session.jstests/audio_mixer.test.jstests/loop_ab.test.jstests/mixer_panel.test.mjstests/test_gp_xml_encoding.pytests/test_track_session_persist.pytests/track_session.test.mjs
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/mixer_panel.test.mjs (1)
163-169: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for live meter routing and lifecycle.
These assertions cover pure ballistics and rendered markup, but not
_meterInputForKey,host.mixerMeterLevels(), or meter start/stop behavior. A routing or lifecycle regression could therefore pass the suite; add a focused fake-RAF integration test for active audio, master, guide, and panel hide behavior.Also applies to: 216-217
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/mixer_panel.test.mjs` around lines 163 - 169, Add a focused fake-RAF integration test alongside the existing meter tests that exercises _meterInputForKey and host.mixerMeterLevels() for active audio, master, and guide routing, then verifies meter updates stop when the panel is hidden and resume or initialize correctly when shown. Keep the existing pure ballistics assertions unchanged and use the test’s lifecycle controls to cover meter start/stop behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/mixer_panel.test.mjs`:
- Around line 163-169: Add a focused fake-RAF integration test alongside the
existing meter tests that exercises _meterInputForKey and
host.mixerMeterLevels() for active audio, master, and guide routing, then
verifies meter updates stop when the panel is hidden and resume or initialize
correctly when shown. Keep the existing pure ballistics assertions unchanged and
use the test’s lifecycle controls to cover meter start/stop behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 0e622ad9-4cd9-4dc9-a982-6442cfb39a5a
📒 Files selected for processing (8)
assets/v3-theme.cssscreen.htmlsrc/audio.jssrc/host.jssrc/main.jssrc/mixer-panel.jstests/audio_mixer.test.jstests/mixer_panel.test.mjs
🚧 Files skipped from review as they are similar to previous changes (5)
- tests/audio_mixer.test.js
- screen.html
- src/host.js
- assets/v3-theme.css
- src/audio.js
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/audio.js (1)
1275-1297: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider caching per-analyser sample buffers instead of allocating on every meter poll.
audioMixerMeterLevels()allocates a newFloat32Arrayper bus/track analyser on every call. If this is polled per animation frame for live meters (per PR objective), that's repeated allocation on a hot path. Reusing aMap<analyser, Float32Array>keyed by analyser (sized once) would avoid the churn.♻️ Proposed direction
+const _meterSampleBufs = new WeakMap(); +function _samplesFor(analyser) { + let buf = _meterSampleBufs.get(analyser); + if (!buf || buf.length !== (analyser.fftSize || 256)) { + buf = new Float32Array(analyser.fftSize || 256); + _meterSampleBufs.set(analyser, buf); + } + return buf; +} export function audioMixerMeterLevels() { ... - const samples = new Float32Array(analyser.fftSize || 256); + const samples = _samplesFor(analyser); analyser.getFloatTimeDomainData(samples);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/audio.js` around lines 1275 - 1297, Update audioMixerMeterLevels() to reuse cached Float32Array sample buffers per analyser instead of allocating a new buffer on each poll. Add or use a Map keyed by analyser, create or resize each buffer only when needed to match analyser.fftSize (defaulting to 256), and use the cached buffer for both bus and track analyser reads.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/multitrack_audio.test.mjs`:
- Line 69: Update the final assertion-count log in the multitrack audio test to
report 8 passed instead of 7, matching the eight assert calls in the file.
---
Nitpick comments:
In `@src/audio.js`:
- Around line 1275-1297: Update audioMixerMeterLevels() to reuse cached
Float32Array sample buffers per analyser instead of allocating a new buffer on
each poll. Add or use a Map keyed by analyser, create or resize each buffer only
when needed to match analyser.fftSize (defaulting to 256), and use the cached
buffer for both bus and track analyser reads.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: fdbd5fb6-8a8a-4b14-957d-0359809bad4b
📒 Files selected for processing (13)
CHANGELOG.mdassets/v3-theme.cssdocs/USER-GUIDE.mdscreen.htmlsrc/audio.jssrc/host.jssrc/main.jssrc/mixer-panel.jssrc/track-session.jstests/audio_mixer.test.jstests/mixer_panel.test.mjstests/multitrack_audio.test.mjstests/track_session.test.mjs
🚧 Files skipped from review as they are similar to previous changes (8)
- src/host.js
- tests/track_session.test.mjs
- assets/v3-theme.css
- screen.html
- docs/USER-GUIDE.md
- CHANGELOG.md
- tests/audio_mixer.test.js
- src/mixer-panel.js
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/mouse.js`:
- Around line 651-654: Update the S.partsViewMode gesture handling around
host.scrollTrackArea so horizontal-dominant input is routed through e.deltaX
rather than e.deltaY. Preserve the existing vertical-dominant condition and
ensure the fall-through pan receives horizontal trackpad swipes so the timeline
moves.
In `@src/parts-view.js`:
- Around line 223-249: Update _partsViewOnMouseDown’s transcription-row
selection branch to resolve the row’s pairedSourceId, falling back to the Master
Mix source when no pairing exists, before selecting the transcription. Switch
the audible source through the existing source-selection mechanism, while
preserving the arrangement selection and status updates.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 320d10a1-a9b8-45c8-997e-9eca60a0030c
📒 Files selected for processing (15)
CHANGELOG.mdassets/v3-theme.cssdocs/USER-GUIDE.mdscreen.htmlsrc/audio.jssrc/host.jssrc/main.jssrc/mixer-panel.jssrc/mouse.jssrc/parts-view.jssrc/state.jssrc/track-session.jstests/mixer_panel.test.mjstests/parts_view.test.jstests/track_session.test.mjs
🚧 Files skipped from review as they are similar to previous changes (9)
- src/state.js
- docs/USER-GUIDE.md
- src/host.js
- assets/v3-theme.css
- screen.html
- CHANGELOG.md
- tests/mixer_panel.test.mjs
- src/mixer-panel.js
- src/audio.js
resetTrackAudioCache disconnected and cleared the per-track gain nodes but left their meter analysers in _meterAnalysers. _attachMeterTap dedupes on that key, so the freshly-created gain for each audio source never got a tap on the next song — every non-focused audio-track VU meter read the dead old node (the focused one only survived via the ref-bus fallback). Drop the track: meter taps in the reset so _ensureTrackGain re-taps fresh gains. Bus taps (ref/guide/click/master) persist with their long-lived nodes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- routes.py: give each cached stem a per-source index in its filename so distinct source ids that sanitize alike (drums/kit vs drums:kit) no longer overwrite one another and share a URL. - audio.js: cancelAudioLoad() before both cached-source installs so a slower in-flight stem decode can't finish and replace the just-installed source. - file-ops.js: seed the synthesized Master Mix with data.audio_url when an older server omits audio_sources, so clicking Master Mix resolves against the cached audio instead of reporting "unavailable". - mouse.js: pan the timeline by the dominant wheel axis so a horizontal trackpad swipe moves it instead of being consumed with no effect. - tests: assert valid UTF-8 XML is byte-identical after normalize; add regression tests for the four code fixes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Status: this PR is being re-cut on top of the merged main rather than merged as-is. It branched off The decision (Christian): tracks DO become persistent first-class objects — this PR's core model wins — but rebuilt to speak the landed vocabulary. The salvage lands as a PR sequence:
Leaving this PR open as the reference implementation until the salvage sequence exists; it closes when the last slice is up. (Its Ctrl/Cmd+A work already landed via #287, and parts of the fit behavior via #279.) |
…s, +6 dB Replaces the docked mixer side-panel with #285's bottom mixer DRAWER: a horizontal row of vertical channel strips (one per stem + transcription part), each a live level meter beside a rotated fader, followed by the SOURCE/GUIDE/CLICK utility buses and a dedicated MASTER output strip. Rises on open, falls on close (reduced-motion aware); opens closed each session (a per-screen view toggle, not a saved pref). Metering: an AnalyserNode taps each bus + per-stem gain (a zero-gain sink keeps the browser processing them without a second audible copy); audioMixerMeterLevels reads RMS→−60..0 dBFS levels + peak dB each frame; the panel's rAF tick fills the bars with instant-attack/gravity-decay ballistics and a peak-hold clip readout. A master gain sits post-limiter (limiter → masterGain → destination); the reference joins there too, so the recording is metered and master-trimmed but STILL never through the limiter (a hot recording is never colored). Per-stem taps drop on song switch; bus taps persist. Headroom: the fader range is 0..106 — 0..100 linear to unity, 100..106 a log map to +6 dB. partStripState/clapState resolve vol through _mixerGainForFaderPure, so stems and synth parts alike can be pushed to +6 dB; every fader reads dB. The selected Tracks-column row lights its matching strip. Removed the vestigial edit-blip toggle. Faithful port of #285's mixer design onto the current main. Tests: mixer_meters.test.mjs (level/peak/ballistics/dB pures); mixer_panel reconciled to the +6 dB clamp, dB labels, master bus, animated close, and always-closed init; audio_mixer/loop_ab sliced envs inject the meter-tap stub and assert the new post-limiter ref routing. 184 JS suites, 291 pytest, lint 0 errors. Stacked on fix/editor-beat-lock-guard (#294) → the meters/+6dB correction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
Clicking an audio track in the Tracks column now focuses it as the active reference: its decoded buffer becomes S.audioBuffer, so the main waveform shows that source and onset tools (Suggest, snap) analyze it. Playback is NOT rerouted — the newly-active source plays via the reference path and every OTHER live source keeps playing through the scheduler, so what you hear is unchanged; only what you see and analyze follows the click. This generalizes the additive engine to #285's model without adopting its full unified scheduler: _liveAudioSources() now returns master + stems, _startStemSources schedules every live source EXCEPT the active one (exposed as the pure _scheduledSourceIdsPure), and the active one rides the existing S.audioSource → _refGain path. activateTrackAudioSource decodes on demand (reusing the source cache), installs the buffer, recomputes the waveform, and restarts playback split if playing. The master's URL is held in S.masterAudioUrl so it survives while a stem is active (S.audioUrl then points at the stem). Onsets self-invalidate on the buffer swap; syncStemAudio adopts the already-decoded active buffer for free rather than re-fetching. Wired: host.selectTrackSessionSource → activateTrackAudioSource; the Tracks-column audio-row click focuses the source. resetStemAudioCache and the load/create paths reset activeAudioSourceId to 'master' and seed S.masterAudioUrl. The last parity gap from #285 — completes the re-cut. Tests: stem_engine.test.mjs pins the active-vs-scheduled split (_scheduledSourceIdsPure). 184 JS suites, 291 pytest, lint 0 errors. Stacked on feat/editor-mixer-meters (#295). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
…s, +6 dB Replaces the docked mixer side-panel with #285's bottom mixer DRAWER: a horizontal row of vertical channel strips (one per stem + transcription part), each a live level meter beside a rotated fader, followed by the SOURCE/GUIDE/CLICK utility buses and a dedicated MASTER output strip. Rises on open, falls on close (reduced-motion aware); opens closed each session (a per-screen view toggle, not a saved pref). Metering: an AnalyserNode taps each bus + per-stem gain (a zero-gain sink keeps the browser processing them without a second audible copy); audioMixerMeterLevels reads RMS→−60..0 dBFS levels + peak dB each frame; the panel's rAF tick fills the bars with instant-attack/gravity-decay ballistics and a peak-hold clip readout. A master gain sits post-limiter (limiter → masterGain → destination); the reference joins there too, so the recording is metered and master-trimmed but STILL never through the limiter (a hot recording is never colored). Per-stem taps drop on song switch; bus taps persist. Headroom: the fader range is 0..106 — 0..100 linear to unity, 100..106 a log map to +6 dB. partStripState/clapState resolve vol through _mixerGainForFaderPure, so stems and synth parts alike can be pushed to +6 dB; every fader reads dB. The selected Tracks-column row lights its matching strip. Removed the vestigial edit-blip toggle. Faithful port of #285's mixer design onto the current main. Tests: mixer_meters.test.mjs (level/peak/ballistics/dB pures); mixer_panel reconciled to the +6 dB clamp, dB labels, master bus, animated close, and always-closed init; audio_mixer/loop_ab sliced envs inject the meter-tap stub and assert the new post-limiter ref routing. 184 JS suites, 291 pytest, lint 0 errors. Stacked on fix/editor-beat-lock-guard (#294) → the meters/+6dB correction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
Clicking an audio track in the Tracks column now focuses it as the active reference: its decoded buffer becomes S.audioBuffer, so the main waveform shows that source and onset tools (Suggest, snap) analyze it. Playback is NOT rerouted — the newly-active source plays via the reference path and every OTHER live source keeps playing through the scheduler, so what you hear is unchanged; only what you see and analyze follows the click. This generalizes the additive engine to #285's model without adopting its full unified scheduler: _liveAudioSources() now returns master + stems, _startStemSources schedules every live source EXCEPT the active one (exposed as the pure _scheduledSourceIdsPure), and the active one rides the existing S.audioSource → _refGain path. activateTrackAudioSource decodes on demand (reusing the source cache), installs the buffer, recomputes the waveform, and restarts playback split if playing. The master's URL is held in S.masterAudioUrl so it survives while a stem is active (S.audioUrl then points at the stem). Onsets self-invalidate on the buffer swap; syncStemAudio adopts the already-decoded active buffer for free rather than re-fetching. Wired: host.selectTrackSessionSource → activateTrackAudioSource; the Tracks-column audio-row click focuses the source. resetStemAudioCache and the load/create paths reset activeAudioSourceId to 'master' and seed S.masterAudioUrl. The last parity gap from #285 — completes the re-cut. Tests: stem_engine.test.mjs pins the active-vs-scheduled split (_scheduledSourceIdsPure). 184 JS suites, 291 pytest, lint 0 errors. Stacked on feat/editor-mixer-meters (#295). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
…s, +6 dB Replaces the docked mixer side-panel with #285's bottom mixer DRAWER: a horizontal row of vertical channel strips (one per stem + transcription part), each a live level meter beside a rotated fader, followed by the SOURCE/GUIDE/CLICK utility buses and a dedicated MASTER output strip. Rises on open, falls on close (reduced-motion aware); opens closed each session (a per-screen view toggle, not a saved pref). Metering: an AnalyserNode taps each bus + per-stem gain (a zero-gain sink keeps the browser processing them without a second audible copy); audioMixerMeterLevels reads RMS→−60..0 dBFS levels + peak dB each frame; the panel's rAF tick fills the bars with instant-attack/gravity-decay ballistics and a peak-hold clip readout. A master gain sits post-limiter (limiter → masterGain → destination); the reference joins there too, so the recording is metered and master-trimmed but STILL never through the limiter (a hot recording is never colored). Per-stem taps drop on song switch; bus taps persist. Headroom: the fader range is 0..106 — 0..100 linear to unity, 100..106 a log map to +6 dB. partStripState/clapState resolve vol through _mixerGainForFaderPure, so stems and synth parts alike can be pushed to +6 dB; every fader reads dB. The selected Tracks-column row lights its matching strip. Removed the vestigial edit-blip toggle. Faithful port of #285's mixer design onto the current main. Tests: mixer_meters.test.mjs (level/peak/ballistics/dB pures); mixer_panel reconciled to the +6 dB clamp, dB labels, master bus, animated close, and always-closed init; audio_mixer/loop_ab sliced envs inject the meter-tap stub and assert the new post-limiter ref routing. 184 JS suites, 291 pytest, lint 0 errors. Stacked on fix/editor-beat-lock-guard (#294) → the meters/+6dB correction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
Clicking an audio track in the Tracks column now focuses it as the active reference: its decoded buffer becomes S.audioBuffer, so the main waveform shows that source and onset tools (Suggest, snap) analyze it. Playback is NOT rerouted — the newly-active source plays via the reference path and every OTHER live source keeps playing through the scheduler, so what you hear is unchanged; only what you see and analyze follows the click. This generalizes the additive engine to #285's model without adopting its full unified scheduler: _liveAudioSources() now returns master + stems, _startStemSources schedules every live source EXCEPT the active one (exposed as the pure _scheduledSourceIdsPure), and the active one rides the existing S.audioSource → _refGain path. activateTrackAudioSource decodes on demand (reusing the source cache), installs the buffer, recomputes the waveform, and restarts playback split if playing. The master's URL is held in S.masterAudioUrl so it survives while a stem is active (S.audioUrl then points at the stem). Onsets self-invalidate on the buffer swap; syncStemAudio adopts the already-decoded active buffer for free rather than re-fetching. Wired: host.selectTrackSessionSource → activateTrackAudioSource; the Tracks-column audio-row click focuses the source. resetStemAudioCache and the load/create paths reset activeAudioSourceId to 'master' and seed S.masterAudioUrl. The last parity gap from #285 — completes the re-cut. Tests: stem_engine.test.mjs pins the active-vs-scheduled split (_scheduledSourceIdsPure). 184 JS suites, 291 pytest, lint 0 errors. Stacked on feat/editor-mixer-meters (#295). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
Summary
Compatibility and scope
Validation
UI verification
Summary by CodeRabbit