# Phase 1 acceptance — 2026-09-16

## Verdict

**Phase 1 complete for the tested Fedora 44 / PipeWire 1.6.8 environment.**
All required foundation checks have fresh evidence below. No Phase 2 work was
implemented. This is functional acceptance, not a hard-real-time certification
or a claim of long-duration reliability on all JACK servers.

The starting tree was clean at `e5a47e3`. `AGENTS.md`, the master prompt,
`DEVELOPMENT.md`, `ARCHITECTURE.md`, `BUILDING.md`, `CONTRIBUTING.md` and
`VERSIONING.md` were read. Earlier notes were treated as historical only.
`VERSION` remains `0.1.0`; no tags, publication or author identity were changed.

## Acceptance checklist

| Requirement | Implementation references | Fresh verification / outcome |
| --- | --- | --- |
| Reproducible system C++20 / Qt 6 build | `CMakeLists.txt`, `VERSION`, `src/app/main.cpp` | Fresh Debug and Release with `RIVET_REQUIRE_JACK=ON`; 4/4 CTest each; both `RIVET 0.1.0`; system headers/library, no `/tmp` cache paths. PASS |
| Functional native window and separated responsibilities | `src/app`, `src/ui`, `src/audio`, `src/dsp`, `src/midi`, `src/utilities`; Qt `QSettings` owned by UI | Source audit; native Qt lifecycle/settings tests at 1× and 2× (15 checks each); native images inspected, scrolling and 480×320 logical viewport verified. PASS |
| Real stereo output and device selection | `AudioEngine::start`, `availablePorts`, `status`; `MainWindow` | Real isolated and desktop capture, exact graph links, L/R and MIDI disconnect reporting, missing/wrong-port errors. PASS |
| Truthful rate and buffer configuration | JACK rate/buffer callbacks, `AudioConfig`, UI policy note | 44.1/48/96 kHz private servers; 32/512/2048/8192 frame requests matched connected capture callbacks. Desktop followed its 48 kHz / 2048-frame policy. PASS |
| Validated MIDI, timing, bounded delivery and overflow | `parseMidi`, `SpscQueue<1024>`, `Impl::process` | Four supported message types/channel/velocity/offsets through JACK; last-frame events; exact 256-event callback cap, 1023-slot queue, 301 total drops after bursts, resumed delivery; parser boundaries and 100,000 ordered concurrent events. PASS |
| Persistent settings and safe defaults | `MainWindow::saveSettings`, constructor, `DiagnosticProcessor::reset` | Round trip; invalid saved buffer falls back to Follow server; activation never restored; captured startup silence and stop reset. PASS |
| Logging and actionable errors | `Logging.cpp`, engine error strings, UI error label | Launch-time log rotation and real write; inaccessible settings/state paths warn on stderr and fail smoke check as expected; missing server/ports fail visibly. PASS |
| Real-time communication and safe ownership | `Impl` callbacks, DSP/parser/queue callees | Source audit below; compile-time lock-free assertions; concurrent controls/queue; active destruction, repeated stop, server loss during DSP; UBSan trap CTest and integration pass. PASS within stated backend scope |
| Startup, shutdown, repeated start/stop and server loss | `AudioEngine::start/stop`, GUI status polling | Ten real restart/double-stop cycles per integration; callbacks cease after stop; missing left/right/MIDI and incompatible port; disconnects; isolated daemon termination during DSP and meter reset; absent-server error. PASS |
| Real signal and development-only disposition | `DiagnosticProcessor`, `jack_integration.cpp`, `--development-audio` | Actual capture: silence → finite stereo 440 Hz / 0.025 peak → exact silence; actual meters; diagnostic UI requires launch option, starts unchecked, never persists enabled state. PASS |

No required Phase 1 implementation item is deferred to Phase 2. Untested
platforms, optional sanitizer runtimes and longer-term limitations appear below.

## Measurements and commands

Exact configure/build/CTest, native UI, capture and sanitizer commands are in
[`BUILDING.md`](BUILDING.md). They were run from the repository root, with
fresh `build-accept-debug`, `build-accept-release` and `build-accept-ubsan`
directories, then rebuilt after affected fixes. Final results:

| Command | Result |
| --- | --- |
| `ctest --test-dir build-accept-debug --output-on-failure` | 4/4; 0.91 s |
| `ctest --test-dir build-accept-release --output-on-failure` | 4/4; 0.84 s |
| `ctest --test-dir build-accept-ubsan --output-on-failure` | 4/4; 0.85 s; no undefined-behavior trap |
| `QT_QPA_PLATFORM=offscreen ./build-accept-debug/rivet --version` | `RIVET 0.1.0`, equal to `VERSION` |
| `QT_QPA_PLATFORM=offscreen ./build-accept-release/rivet --version` | `RIVET 0.1.0`, equal to `VERSION` |
| `python3 tests/isolated_audio.py build-accept-debug/rivet_jack_check` | 62 PASS, exit 0; 48 kHz / 256 frames; 243 callbacks; 439.586 Hz |
| `python3 tests/isolated_audio.py build-accept-release/rivet_jack_check` | 62 PASS, exit 0; 48 kHz / 256 frames; 242 callbacks; 439.586 Hz |
| `./build-accept-release/rivet_jack_check` | 50 PASS, exit 0; desktop 48 kHz / 2048 frames; 31 callbacks; 440.885 Hz |
| `python3 tests/isolated_audio.py build-accept-release/rivet_jack_check --rate 44100` | 62 PASS, exit 0; 44.1 kHz / 256 frames; 223 callbacks; 440.258 Hz |
| `python3 tests/isolated_audio.py build-accept-release/rivet_jack_check --rate 96000` | 62 PASS, exit 0; 96 kHz / 256 frames; 489 callbacks; 440.413 Hz |
| `python3 tests/isolated_audio.py build-accept-ubsan/rivet_jack_check` | 62 PASS, exit 0; 48 kHz / 256 frames; 243 callbacks; 439.586 Hz; no trap |
| `RIVET_UI_AUDIO_TEST=1 ./build-accept-debug/rivet_ui_tests` | Native 1×: 15 PASS, exit 0 |
| `QT_SCALE_FACTOR=2 RIVET_UI_AUDIO_TEST=1 ./build-accept-debug/rivet_ui_tests` | Native 2×: 15 PASS, exit 0 |
| `git diff --check` | Clean |

Every captured signal above had peak 0.025 (approximately −32.04 dBFS), finite
identical stereo samples, and zero reported xruns during its measured signal
window. Frequency is a finite-window crossing estimate, hence the small error.
The four private buffer requests all measured the requested quantum, including
8192 despite the daemon's ordinary default maximum of 2048: a force request can
exceed normal scheduling policy. This is why the UI describes effects on other
clients and displays actual values rather than promising a local buffer size.

Native captures were inspected at `/tmp/rivet-accept-normal.png`,
`/tmp/rivet-accept-2x.png` and `/tmp/rivet-accept-2x-scrolled.png`. At 2× on this
desktop, the diagnostic panel requires vertical scrolling; at the deliberately
small 480×320 viewport, both scrollbars are needed. Controls remain reachable.
The normal launch has no diagnostic panel. Generated images/logs are temporary
local evidence, not Git artifacts; old `docs/phase1.png` remains historical.
Final text logs are in the ignored build directories (`isolated.log`,
`desktop.log`, alternate-rate logs, and native UI logs).

## Callback and ownership audit

- `Impl::process`: JACK supplies buffers; no application buffer allocation.
  Work is O(frames) plus at most 256 decoded MIDI events. Excess raw events are
  counted with one atomic addition, not traversed by RIVET.
- `DiagnosticProcessor::process` and `safeSample`: fixed local scalar state,
  bounded math (`sin`, finite checks, clamp/max), no heap, lock, I/O, GUI or
  logging. Phase/gain belong exclusively to audio while active. Invalid rates
  produce silence, parameter setters validate bounds/non-finite values, output
  is bounded, and peak/clip publication uses lock-free scalar atomics.
- `parseMidi`: examines only a three-byte supported message and returns a
  stack `optional<MidiEvent>`. No allocation or variable-length message copy.
- `SpscQueue`: fixed array, one producer/consumer, release/acquire indices;
  full means drop, never wait. Float, bool, unsigned and size_t atomics have
  compile-time `is_always_lock_free` assertions on the supported target.
- Rate/buffer/xrun/shutdown callbacks only publish atomic values. Shutdown does
  not close a client or call Qt/logging. `status()` and port APIs are strictly
  control-thread operations. `MainWindow` drains at most 1024 monitor attempts
  per timer tick and owns all settings/widget work.
- `stop()` deactivates/closes the client before clearing port pointers or
  resetting DSP. Callback argument storage outlives closure, including partial
  startup and server loss. The shutdown flag uses release/acquire publication;
  status masks running after loss even if start/shutdown publication overlaps.
  Real tests verify stopped callback counts and destruction while active.
- Newly added retry sleeps, string construction and port validation run only
  in `start()` on the control thread. The total added sleep budget is bounded
  to 200 ms per selected connection; backend API calls have their own timing.
  No application callback allocation, blocking, logging or GUI work was added.

The audit used the [JACK callback contract](https://jackaudio.org/api/group__ClientCallbacks.html),
installed headers, and matching [PipeWire 1.6.8 JACK source](https://github.com/PipeWire/pipewire/blob/1.6.8/pipewire-jack/src/pipewire-jack.c).
The latter's close path stops processing and notification threads before freeing
client storage. Buffer/MIDI access uses prepared buffers and backend conversion.
Qualification: backend MIDI conversion has overload warning paths and optional
trace logging; RIVET cannot certify the system library or scheduler as entirely
free of such behavior. RIVET's own callback/callees contain none. Acceptance
runs use ordinary backend logging; debug tracing was used separately to diagnose
the startup race. Sanitizers cover RIVET/test code, not system library internals.

## Fixes and development diagnostic decision

1. PipeWire graph changes could temporarily hide a valid selected output after
   a buffer request, producing JACK EINVAL. Added type/direction preflight
   before the request, then bounded transient connection retries. Repeated
   buffer changes now pass in Debug, Release and UBSan, with real capture.
2. A single connected output was presented as generally connected audio. UI
   status now reports left/right separately and exposes MIDI connectivity.
3. The old 640-pixel minimum height exceeded some 2× logical desktops. The
   window now respects available screen size and supports 480×320 with scrolling.
4. Normal launches no longer expose diagnostic tone controls. Manual access
   requires `--development-audio`; the checkbox additionally requires a running
   engine, remains off on every start and is never saved. Retaining this explicit
   development option preserves useful diagnosis while moving ordinary audio
   proof into tests. The sine is not synthesis, a rack device or a mixer.
5. The private-server helper now requires and displays the binary path instead
   of defaulting to possibly stale `build/rivet_jack_check`; optional server
   rates and more complete integration/UI/boundary checks were added.
6. Failed log-directory setup previously left Qt's platform handler installed,
   so warnings were not reliably observable on stderr. RIVET now installs its
   stderr handler first; `gui_io_failure` verifies the real failure path.

Changed implementation files: `CMakeLists.txt`, `src/app/main.cpp`,
`src/audio/AudioEngine.{h,cpp}`, `src/ui/MainWindow.{h,cpp}`,
`src/utilities/Logging.cpp`.
Tests: `tests/core_tests.cpp`, `tests/jack_integration.cpp`,
`tests/isolated_audio.py`, new `tests/ui_tests.cpp` and `tests/io_failure.cmake`.
Documentation: `README.md`, `BUILDING.md`, `ARCHITECTURE.md`, `CONTRIBUTING.md`,
`CHANGELOG.md`, `DEVELOPMENT.md`, and this acceptance record.

## Failed attempts, skips and limitations

- An initial desktop invocation returned 77 because of sandbox socket access;
  a private daemon also initially failed to bind under sandbox restrictions.
  Both were rerun with the necessary access and passed. Neither was counted
  as a pass or silently skipped.
- An early test compared against an inactive capture client's stale quantum.
  It now uses live connected capture callbacks and an immutable published
  sample prefix. The resulting test then exposed the startup race fixed above.
- One Debug absent-server subprocess exceeded the unchanged five-second helper
  watchdog during concurrent validation. Its cause was not reproduced or
  established. The full sequential Debug suite passed afterward, and 20 more
  absent-server attempts all returned actionable errors within 0.008 s each.
  This observation is retained; long-duration/concurrent-load reliability is
  not claimed. The helper still fails on timeout rather than converting it to
  a skip. No required final audio check was skipped.
- ASan+UBSan configuration failed because `libasan.so.8.0.0` is absent. RPM also
  confirms absent `libubsan` and `libtsan`. ASan and TSan were not run; UBSan
  trap-mode CTest and real isolated integration passed without those runtimes.
- The uninstalled development application emits a portal app-ID registration
  warning on native launch. It does not prevent UI operation; no packaging or
  installed desktop entry is claimed.
- Native JACK/jackd, physical MIDI controller unplug, suspend/resume, listening
  on hardware, long-duration stress and backend sanitizer instrumentation are
  unverified. All test audio remained away from physical playback ports; no
  desktop daemon was terminated. Virtual port and real server loss were tested.
- Reconnection is manual. MIDI remains a lossy diagnostic monitor, not a voice
  engine or recorder. Unsupported MIDI types/SysEx are ignored; production
  note-state recovery is required before instruments exist. Meter polling can
  miss short transients; JACK load is graph-wide. The diagnostic safety clamp
  is not a production lookahead limiter. Settings and small log writes remain
  on the GUI thread; future large file/database I/O needs background workers.

## Phase 2 recommendations (not implemented)

Define device metadata and typed ports; implement/test connection validation,
cycle rejection and execution order; prepare graph buffers/plans off audio and
retire them safely; prove routing using test signal/gain/sink devices; then add
rack operations and rear cables backed by that tested graph. Preserve the
silence, timing, overflow and lifetime contracts established here.
