Functional requirements — Rymflux v1
Derived from the charter vision, scope, and platform philosophy. Each FR traces to a test case and user story.
Functional requirements — Rymflux v1
Plugin system
FR-PLUGIN-1: Plugin discovery and registration
| Related story | Register a content-source plugin |
| Priority | Must |
The platform shall scan a well-known plugins directory at startup and register every valid plugin whose manifest parses successfully. Invalid manifests shall be logged and skipped without crashing the platform.
Acceptance criteria:
Scenario: Valid plugin discovered at startup
Given a directory "~/.rymflux/plugins/" exists
And a subdirectory "spotify" contains a valid plugin.toml and an executable entry point
When the platform starts
Then the plugin is registered with id="spotify" and its declared capabilities are available
Scenario: Invalid manifest skipped
Given a subdirectory "broken" contains a malformed plugin.toml
When the platform starts
Then the broken plugin is not registered
And an error is logged with the path and parse failure reason
And the platform continues startup without crashing
Scenario: Plugin directory does not exist
Given no plugins directory exists
When the platform starts
Then the platform starts with zero plugins registered
And no error is surfaced to the user
FR-PLUGIN-2: Plugin manifest format
| Related story | Define a plugin’s identity and capabilities |
| Priority | Must |
The platform shall accept a plugin.toml manifest with the following required fields
and reject manifests that omit any of them.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Unique plugin identifier (reverse domain or slug) |
name |
string | yes | Human-readable name |
version |
semver | yes | Plugin version |
entry |
string | yes | Path to executable (relative to manifest directory) |
capabilities |
array | yes | List of supported operations |
Supported capability values: search, get_track, get_album, get_artist,
stream. (get_lyrics is reserved for v2.)
Scenario: Valid manifest accepted
Given a plugin.toml contains id, name, version, entry, and at least one capability
When the platform parses the manifest
Then registration succeeds
Scenario: Missing required field
Given a plugin.toml is missing the "id" field
When the platform parses the manifest
Then the manifest is rejected
And the error message includes the missing field name
Scenario: Unknown capability declared
Given a plugin.toml declares capability "teleport"
When the platform parses the manifest
Then the unknown capability is ignored
And a warning is logged
FR-PLUGIN-3: Plugin lifecycle
| Related story | Run a plugin when needed |
| Priority | Must |
The platform shall manage plugin processes through a defined lifecycle: launch on first request, keep alive for a configurable idle timeout, terminate on request, and recover from crashes.
Scenario: Plugin launched on first request
Given a registered plugin with entry "./spotify"
And the plugin process is not running
When the platform sends a "search" request
Then the plugin executable is launched
And the request is delivered within 5 seconds of launch
Scenario: Plugin crash during request
Given a plugin process is running
When the plugin process exits unexpectedly before responding
Then the platform returns a PLUGIN_CRASHED error to the caller
And the plugin is marked for restart on the next request
And the core platform continues without crashing
Scenario: Plugin timeout
Given a plugin does not respond within the configured timeout (default 30s)
When the timeout expires
Then the platform kills the plugin process
And returns a PLUGIN_TIMEOUT error to the caller
Scenario: Idle plugin terminated
Given a plugin has no requests for the configured idle timeout (default 5 min)
When the idle timeout expires
Then the plugin process is terminated gracefully
And the plugin remains registered
FR-PLUGIN-4: Plugin search capability
| Related story | Search content from a plugin |
| Priority | Must |
A plugin that declares the search capability shall accept a search query string
and return a list of matching tracks, albums, and artists in a standardized JSON
format.
Scenario: Plugin returns search results
Given a plugin with capability "search"
When the platform sends a search request with query "Imagine Dragons"
Then the plugin returns a JSON response containing
| Field | Type | Description |
|-----------|--------|-----------------------|
| id | string | Plugin-scoped content ID |
| title | string | Track/album/artist name |
| type | string | "track" | "album" | "artist" |
| artist | string | Artist name |
| duration | int | Duration in seconds (tracks only) |
And the response is returned within the configured timeout
Scenario: Plugin returns empty results
Given a plugin with capability "search"
When the platform sends a search request with query "zzzznotexist"
Then the plugin returns an empty results array
And no error is raised
FR-PLUGIN-5: Plugin streaming capability
| Related story | Stream audio from a plugin |
| Priority | Must |
A plugin that declares the stream capability shall accept a content ID and return
audio data either as a stream URL or raw bytes over the IPC channel, as declared in
its manifest.
Scenario: Plugin returns stream URL
Given a plugin with capability "stream" and streaming_mode = "url"
When the platform sends a stream request with content_id "123"
Then the plugin returns a JSON object containing
| Field | Type | Description |
|-------------|--------|------------------------|
| url | string | Direct audio stream URL |
| content_type| string | MIME type of the audio |
And the URL is accessible by the playback engine
Scenario: Plugin returns audio bytes (**v2** — pipe mode deferred, see ADR-006)
Given a plugin with capability "stream" and streaming_mode = "pipe"
When the platform sends a stream request with content_id "123"
Then the plugin writes audio bytes to the IPC channel
And the stream includes content_type metadata
And the stream ends when the audio is complete or on an explicit stop signal
FR-PLUGIN-6: Plugin metadata capability
| Related story | Get track, album, or artist details |
| Priority | Must |
A plugin that declares metadata capabilities shall return detailed information for a given content ID.
Scenario: Plugin returns track details
Given a plugin with capability "get_track"
When the platform sends a get_track request with id "123"
Then the plugin returns a JSON object containing
| Field | Type |
|-------------|--------|
| id | string |
| title | string |
| artist | string |
| album | string |
| duration | int |
| cover_art | string (URL, optional) |
| streamable | bool |
Scenario: Plugin returns 404 for unknown content
Given a plugin with capability "get_track"
When the platform sends a get_track request with id "nonexistent"
Then the plugin returns error code CONTENT_NOT_FOUND
Room framework
FR-ROOM-1: Room registration and lifecycle
| Related story | Register a room experience |
| Priority | Must |
The room framework shall accept room registrations and manage their lifecycle: activate (room becomes visible and interactive), deactivate (room hidden, state preserved), suspend (resources reclaimed, state preserved).
Scenario: Room registers successfully
Given a room declares itself with a unique id and display name
When the room framework processes the registration
Then the room appears in the shell's room list
And the room is not active until the user enters it
Scenario: User enters a room
Given a registered room with id "music"
When the user navigates to the music room
Then the room receives an "activate" event
And the room's home screen is displayed
And the previous room receives a "deactivate" event (state preserved)
Scenario: Duplicate room ID rejected
Given a room with id "music" is already registered
When a second room attempts to register with id "music"
Then the second registration is rejected
And an error is logged
FR-ROOM-2: Room search integration
| Related story | Search content within a room context |
| Priority | Should |
A room may declare search filters that narrow the set of plugins queried and the result fields displayed. The search manager respects these filters when processing queries from that room.
Scenario: Music Room search filters by type
Given the Music Room is active
And the Music Room declares filters: types=["track", "album", "artist"]
When the user searches for "Imagine Dragons"
Then the search manager queries all plugins with capability "search"
And results are filtered to types track, album, and artist only
Scenario: Room with no search filters
Given a room that does not declare search filters
When the user searches
Then all searchable plugins are queried
And all result types are returned
FR-ROOM-3: Room player expansion
| Related story | Customize the player for a room |
| Priority | Should |
A room may register a player expansion view that is displayed when the user expands the mini-player. The expansion view receives the currently playing content metadata.
Scenario: Music Room player expansion
Given the Music Room is active
And a track is playing
When the user expands the mini-player
Then the Music Room's expansion view is displayed
And the view receives the current track's metadata (title, artist, album, duration, cover_art)
And the mini-player controls (play/pause, next, previous, seek, volume) remain visible
Scenario: Room without expansion
Given a room that does not register a player expansion view
When the user expands the mini-player
Then a default view is shown with basic metadata and controls
Core platform services
FR-CORE-1: Source-agnostic playback engine
| Related story | Play audio from any source |
| Priority | Must |
The playback engine shall accept an audio source as a URL or file path and play it. The engine shall have no awareness of plugins, rooms, or content types.
Scenario: Play audio from URL
Given the playback engine receives a stream URL "https://example.com/audio.mp3"
When the user presses play
Then audio playback starts within 2 seconds
And the engine emits a "playing" event with the source URL
Scenario: Play audio from file path
Given the playback engine receives a file path "/home/user/music/track.flac"
When the user presses play
Then audio playback starts within 2 seconds
Scenario: Engine ignores source provenance
Given the playback engine receives a source
When queried for the source's origin
Then the engine returns no plugin or room metadata
And returns only the source URL or file path
Scenario: Unsupported format rejected
Given the playback engine receives a source with unsupported format
When playback is attempted
Then the engine returns error UNSUPPORTED_FORMAT
And does not crash
FR-CORE-2: Queue management
| Related story | Build and manage a playback queue |
| Priority | Must |
The queue shall maintain an ordered list of content items that persists across room switches. Items are referenced by their stable content ID.
Scenario: Add item to empty queue
Given the queue is empty
When a content item is added to the queue
Then the queue contains one item
And the item is set as the next to play
Scenario: Reorder items in queue
Given the queue contains items [A, B, C]
When item C is moved to position 1
Then the queue order is [C, A, B]
Scenario: Queue persists across room switches
Given the queue contains [A, B, C]
And the user is in the Music Room
When the user switches to another room
Then the queue still contains [A, B, C]
And playback continues uninterrupted
Scenario: Remove item from queue
Given the queue contains [A, B, C]
When item B is removed
Then the queue contains [A, C]
And playback of the current item (if B is playing) advances to the next item
FR-CORE-3: Unified library
| Related story | Save content to library |
| Priority | Must |
The library shall store content items using opaque, source-independent UUIDs. A separate resolution table shall map each content UUID to the source plugin and foreign ID for playback. The library shall support favorites, playlists, and playback history.
Scenario: Save content to favorites
Given a content item with UUID "abc-123" exists in search results
When the user saves it to favorites
Then the library stores the content UUID as the track's identity
And the library stores plugin_id as a resolution hint, not as content identity
And two saves of the same track via different plugins share one UUID
And the item appears in the user's favorites list
Scenario: Play back saved content
Given a content item with UUID "abc-123" is in the library
When the user plays it
Then the library resolves the content UUID to (plugin_id, foreign_id) via the mapping table
And the resolved plugin's stream capability is called
Scenario: Add content to playlist
Given a library item with UUID "abc-123" exists
When the user adds it to a playlist named "Favorites 2026"
Then the playlist contains the content UUID
And the original library item is unchanged
Scenario: View playback history
Given the user has played content items [A, B, C]
When the user views history
Then history shows [C, B, A] (most recent first)
With timestamps for each play
FR-CORE-4: Parallel search manager
| Related story | Search across all plugins |
| Priority | Must |
The search manager shall dispatch a search query to all registered plugins that
declare the search capability, collect results with a configurable per-plugin
timeout, merge them, and return a unified result set.
Scenario: Parallel search across two plugins
Given two registered plugins with capability "search": PluginA and PluginB
When the user searches for "Imagine Dragons"
Then both plugins receive the search query
And results from both are returned
Scenario: Slow plugin does not block results
Given PluginA responds to search in 100ms
And PluginB takes longer than the configured timeout (30s)
When the user searches
Then PluginA's results are displayed after 100ms
And PluginB's results are either displayed when they arrive or omitted on timeout
And a notification "PluginB did not respond" is shown
Scenario: Search results deduplicated in memory
Given PluginA returns track with title "Believer" and foreign_id "plA-1"
And PluginB returns track with title "Believer" and foreign_id "plB-1"
When the search manager merges results
Then the results list contains one entry for "Believer"
And the entry carries both (pluginA, plA-1) and (pluginB, plB-1) as candidate sources
And no database writes occur
Scenario: Library save creates resolution records
Given a deduplicated search result for "Believer" with two candidate sources
When the user saves "Believer" to their library
Then library_items contains one row for "Believer" with a single UUID
And content_resolution contains two rows: (UUID, pluginA, plA-1) and (UUID, pluginB, plB-1)
Scenario: No plugins registered
Given zero plugins with capability "search" are registered
When the user searches
Then the search manager returns an empty result
And a message "No search plugins installed" is shown
Identity system
FR-ID-1: Stable content UUIDs
| Related story | Identify content independently of source |
| Priority | Must |
Every content item stored in the library, queue, playlist, or history shall be identified by a UUID that does not encode the source plugin. The platform shall maintain a content resolution table that maps content UUIDs to one or more (plugin_id, foreign_id) tuples.
Scenario: Content UUID generated on first save
Given a content item returned by a plugin
When the item is added to the library
Then a UUID is assigned as the content ID
And the mapping (plugin_id, foreign_id, content_uuid) is stored in the resolution table
Scenario: Same content from two plugins
Given PluginA returns a track identified as "plA-1"
And PluginB returns the same track identified as "plB-1"
When both are added to the library
Then they share the same content UUID (deduplication)
And the resolution table has two entries: (plA-1 → uuid) and (plB-1 → uuid)
Initial rooms
FR-MUSIC-1: Music Room browsing
| Related story | Browse music content |
| Priority | Must |
The Music Room shall provide a browse view organized by album, artist, and genre. Content is sourced from all registered plugins that support music-type content.
Scenario: Browse albums
Given at least one plugin with capability "search" is registered
When the user opens the Music Room and selects "Albums"
Then the view displays album covers and titles
And selecting an album shows its track list
Scenario: Browse by genre
Given content with genre metadata exists across plugins
When the user browses by a specific genre
Then only content matching that genre is shown
And results are drawn from all plugins
Scenario: No content available
Given zero plugins are registered
When the user opens the Music Room
Then the view shows "No music sources installed"
And a link to the plugin setup guide
FR-MUSIC-2: Music Room player expansion
| Related story | Expanded player in Music Room |
| Priority | Must |
When the mini-player is expanded in the Music Room, the expansion view shall show album artwork, now-playing metadata, and the playback queue.
Scenario: Player expansion shows artwork and metadata
Given a track is playing in the Music Room
When the user expands the mini-player
Then the expanded view shows album artwork, track title, artist name
And the queue is accessible below the player controls
FR-HOME-1: Home (Lobby)
| Related story | Platform home screen |
| Priority | Must |
The Home view shall display: continue listening (most recent unfinished content), recently played, pinned rooms, and recent searches.
Scenario: Home shows continue listening
Given the user has partially played a track
When the user returns to Home
Then "Continue Listening" shows the partially played track with progress
Scenario: Home shows pinned rooms
Given the user has pinned the Music Room
When the user views Home
Then the Music Room appears in pinned rooms
And clicking it activates the Music Room
Scenario: Empty state on first launch
Given the user has never played any content
When Home is displayed
Then "Continue Listening" section is hidden
And "Getting Started" guide is shown with plugin installation instructions
Reference plugin
FR-REF-1: Local-files reference plugin
| Related story | Use Rymflux with local audio files |
| Priority | Must |
A local-files plugin shall ship with Rymflux. It shall scan configured directories
for audio files, index them, and make them available through search, get_track,
get_album, get_artist, and stream capabilities.
Scenario: Plugin indexes music directory
Given a configured music directory containing .mp3, .flac, and .ogg files with basic ID3 tags
When the local-files plugin starts
Then the plugin indexes all supported audio files
And search returns tracks organized by album and artist extracted from metadata
Scenario: Stream local file
Given the local-files plugin is registered
When the playback engine requests stream for a known local track
Then the plugin returns a file:// URL pointing to the audio file on disk
Scenario: Plugin handles directory with no audio files
Given an empty configured music directory
When the local-files plugin starts
Then the plugin registers successfully
And search returns empty results with no error
Traceability
| FR | User story file | Priority | Depends on |
|---|---|---|---|
| FR-PLUGIN-1 | register-plugin.md | Must | — |
| FR-PLUGIN-2 | register-plugin.md | Must | — |
| FR-PLUGIN-3 | register-plugin.md | Must | FR-PLUGIN-1, FR-PLUGIN-2 |
| FR-PLUGIN-4 | search-across-sources.md | Must | FR-PLUGIN-3 |
| FR-PLUGIN-5 | stream-from-plugin.md | Must | FR-PLUGIN-3 |
| FR-PLUGIN-6 | stream-from-plugin.md | Must | FR-PLUGIN-3 |
| FR-ROOM-1 | register-room.md | Must | — |
| FR-ROOM-2 | register-room.md | Should | FR-ROOM-1, FR-CORE-4 |
| FR-ROOM-3 | register-room.md | Should | FR-ROOM-1, FR-CORE-1 |
| FR-CORE-1 | play-music-from-any-source.md | Must | FR-PLUGIN-5 |
| FR-CORE-2 | play-music-from-any-source.md | Must | FR-CORE-1 |
| FR-CORE-3 | save-to-library.md | Must | FR-PLUGIN-4, FR-ID-1 |
| FR-CORE-4 | search-across-sources.md | Must | FR-PLUGIN-4 |
| FR-ID-1 | save-to-library.md | Must | — |
| FR-MUSIC-1 | play-music-from-any-source.md | Must | FR-ROOM-1, FR-CORE-4 |
| FR-MUSIC-2 | play-music-from-any-source.md | Must | FR-ROOM-3, FR-CORE-1 |
| FR-HOME-1 | play-music-from-any-source.md | Must | FR-ROOM-1 |
| FR-REF-1 | register-plugin.md | Must | FR-PLUGIN-1 through FR-PLUGIN-5 |