pj_marketplace/docs/REQUIREMENTS.md
Version: 1.0.0 Last Updated: 2026-07-17 Purpose: Define WHAT the application should do, not HOW
PlotJuggler has grown significantly, evolving from an internal tool to a de facto standard for data visualization in robotics. With this growth comes a problem: how do we allow the community to contribute plugins without requiring a full PlotJuggler recompilation for each update?
| Category | Feature | Description |
|---|---|---|
| Discovery | Extension listing | Display all available extensions in VSCode-style cards |
| Search | Search by name, description, tags, and publisher | |
| Category filtering | Data Loader, Data Streamer, Parser, Toolbox | |
| Extension detail | Panel with complete information, changelog, and dependencies | |
| Startup snapshot | Host app may seed the marketplace with already-loaded plugin ids/versions before first render | |
| Installation | Secure download | ZIP artifact download with SHA256 verification |
| Automatic extraction | Decompression to extensions directory | |
| Platform detection | Automatic selection of correct artifact (Linux/Windows/macOS) | |
| Updates | Update detection | Local vs registry version comparison (semver); local-newer state is surfaced explicitly |
| Individual update | Update a specific extension | |
| Bulk update | "Update All" for multiple extensions | |
| Automatic backup | Backup of previous version before updating | |
| Uninstallation | Clean removal | Directory deletion + installed cache refresh |
| Confirmation | Confirmation dialog before uninstalling | |
| Core lock | Core (bundled) extensions cannot be uninstalled in default sessions (see §4.4) | |
| Management | Backup diagnostics | Report retained backup paths when an update install fails |
| Persistent state | Installed state derived from plugin DSOs; each embedded plugin manifest is the source of truth | |
| Core (bundled) extensions | Extensions shipped with the application are seeded into the extensions dir at startup, kept at least at the shipped version, locked against uninstall, and offer "Downgrade to bundled" when updated above the shipped version (see §4.4) | |
| Registry URL setting | Edited in the host's Preferences → Plugins page (validated http(s)/file scheme; empty restores the built-in default) and persisted in the host's QSettings (Marketplace/registryUrl). The host resolves the URL and passes it in each time the marketplace opens; the window never reads registry settings itself | |
| Unified diagnostics | All lifecycle events (install / staged-promotion / quarantine / uninstall failures, registry-fetch errors) are surfaced through ExtensionManager::diagnosticReported, the in-memory ring buffer, AND an optional PJ::DiagnosticSink so embedding hosts can fold marketplace events into their own diagnostic stream | |
| UI/UX | Download progress | Progress bar in status bar |
| Notifications | Status messages and available update alerts | |
| Context menu | Quick actions per installed extension |
| Category | Feature | Description |
|---|---|---|
| Build | Cross-platform compilation | Matrix build for Linux, Windows, and macOS |
| Static linking | All dependencies embedded in the artifact | |
| Dependency management | Support for Conan (current) and Pixi (future) | |
| Packaging | ZIP generation | Automatic packaging with plugin binaries and resources |
| Checksums | Automatic SHA256 generation per artifact | |
| Versioning | Version extraction from git tag | |
| Publishing | GitHub Release | Automatic release creation with attached artifacts |
| Registry update | Automatic PR to registry repo with new version | |
| Validation | Unit tests | Test execution on each platform |
| Lint/Format | Code style verification | |
| Schema validation | Registry JSON validation in PRs |
| Category | Feature | Description |
|---|---|---|
| Catalog | Complete listing | JSON with all available extensions |
| Metadata | Name, description, author, license, tags, category | |
| Versioning | Current version and minimum PlotJuggler versions | |
| Cross-platform | URLs and checksums per platform (Linux/Windows/macOS) | |
| Hosting | Static GitHub | JSON file accessible via raw.githubusercontent.com |
| Cache TTL | Support for local cache with expiration time | |
| Multiple registries | Configuration for alternative registries (enterprise) |
| Term | Definition |
|---|---|
| Extension | Marketplace distribution unit. Downloadable ZIP containing one or more plugins. |
| Plugin | C++ module dynamically loaded (.so/.dll/.dylib) implementing an SDK interface. |
| Registry | Static JSON file with the catalog of available extensions. |
| Plugin SDK | Abstract library (no Qt) that plugins use for UI and data access. |
| Artifact | Compiled binary of an extension for a specific platform. |
| Embedded manifest | JSON string exported by each plugin DSO describing the installed plugin. |
| Bundled (share) dir | <prefix>/lib/plotjuggler/plugins, shipped inside an installed build or AppImage. A seed source only — never scanned as a load path. |
| Core (bundled) extension | An extension whose id ships in the bundled dir. Seeded into the extensions dir, uninstall-locked in default sessions. |
| Seed | The startup sync of bundled plugins into the extensions dir (copy when absent, refresh when the bundled version is newer). |
These concerns are intentionally not part of pj_marketplace and belong
elsewhere in the host application:
pj_app that filters which discovered DSOs the loader instantiates),
not a marketplace concern. The marketplace's job ends at "installed on
disk"; whether the host chooses to load that DSO at startup is decided by
the host's plugin loader and config.| ID | Requirement | Acceptance Criteria |
|---|---|---|
| F-01 | Fetch and parse registry JSON from configurable URL | Given a valid URL, the system loads and parses extension metadata |
| F-02 | List extensions in sidebar with cards | User sees all available extensions with name, description, version |
| F-03 | Search by name, description, tags | Typing "ros" shows all ROS-related extensions |
| F-04 | Filter by category | User can filter by Data Loader, Streamer, Parser, Toolbox |
| F-05 | Show selected extension detail | Clicking an extension shows full information panel |
| F-06 | Download ZIP with SHA256 verification | Download fails if checksum doesn't match |
| F-07 | Extract ZIP to extensions directory | ZIP contents are extracted to correct location |
| F-08 | Register installed extension | Installed state is derived from disk by scanning extension DSOs and reading each embedded plugin manifest |
| F-09 | Detect updates (local vs registry version) | User sees "Update available" badge when registry is newer, and "Local newer" when the installed version is ahead |
| F-10 | Uninstall extension | User can remove installed extensions (except core extensions in default sessions — see F-28) |
| ID | Requirement | Acceptance Criteria |
|---|---|---|
| F-24 | Configurable, persisted registry URL | Preferences → Plugins offers a default/custom mode switch: default shows the built-in URL read-only, custom enables the field. On edit-finish the value is validated — http(s)/file scheme, plus a reachability probe when the system is online (skipped offline) — and the text renders red while the check fails (advisory, never blocking). On OK a valid custom URL persists in QSettings (Marketplace/registryUrl; default mode stores "no override") and the marketplace fetches from it on next open |
| F-11 | Local registry cache with TTL (deferred) | Not implemented today — RegistryManager always fetches fresh. Would be reintroduced only if a clear caching need emerges (see TODO.md). |
| F-12 | Backup previous version on updates | Old version saved before overwriting |
| F-13 | Automatic rollback if plugin fails | Deferred; backups may exist, but automatic restore is not implemented |
| F-14 | Deferred update: apply on restart | Updates are downloaded and staged, then applied only after restart; a fresh install activates immediately |
| F-16 | Cancel download in progress | User can abort a download |
| F-17 | Update All | Single action to update all extensions with available updates |
| F-18 | Confirmation dialogs | User confirms before install/uninstall/update actions |
| ID | Requirement | Acceptance Criteria |
|---|---|---|
| F-19 | Extension icons (download + cache) | Each extension displays its icon |
| F-20 | Changelog per extension | User can see version history |
| F-21 | Metrics (downloads, rating) | Extension cards show popularity metrics |
| F-22 | Notification: "N updates available" | User notified of available updates |
| F-23 | Multiple registry URLs | Support for private/enterprise registries |
The host application (via pj_runtime's ExtensionCatalogService) syncs bundled
plugins into the extensions dir and decides which folders are scanned; the
marketplace enforces the core-extension policy on top. Requirements on the
combined behavior:
| ID | Requirement | Acceptance Criteria |
|---|---|---|
| F-26 | Seed bundled plugins at startup, in every mode | Each bundled id absent from (or unreadable in) the extensions dir is copied there before the plugin scan — including in --plugin-dir sessions. The bundled dir itself is never scanned as a load path. |
| F-27 | Keep seeded extensions at least at the shipped version | When the bundled version is newer than the installed one, the installed copy is replaced (staged copy + swap: a failed refresh keeps the old working copy and retries next launch). An installed version equal to or newer than the bundled one is never touched — equal versions never re-copy (shipping a change requires a version bump), and a user install ahead of the bundled version is never downgraded automatically. |
| F-28 | Lock uninstall of core extensions (default sessions only) | In a default session the Uninstall action is unavailable for bundled ids. In a --plugin-dir session nothing is core: the managed folder is user-owned, so same-id copies there remain uninstallable. |
| F-29 | Offer "Downgrade to bundled" | When a core extension was updated above the bundled version, the user can revert to the shipped version (staged, applied on restart). |
| F-30 | Plugin folder precedence | Folders are scanned in this priority order, duplicates resolved by plugin id: the --plugin-dir override, then the custom folders from Preferences → Plugins, then the extensions dir. The first two tiers are user-explicit and win version-blind (an explicitly given folder wins even with an older build); within the extensions dir, compatibility then higher version decide. |
| ID | Requirement | Metric |
|---|---|---|
| NF-01 | C++20 | Module builds at C++20 (CMAKE_CXX_STANDARD 20), matching the rest of PJ4 and plotjuggler_sdk |
| NF-02 | Qt 6.x Widgets | 6.11 now; 6.12 LTS target |
| NF-03 | Cross-platform | Works on Linux, Windows, macOS |
| NF-04 | Build system: CMake | Standard CMake project |
| NF-05 | Dependencies: Conan (current), Pixi (future) | Builds with specified tools |
| NF-06 | No external dependencies beyond Qt | Single binary, no runtime deps |
| NF-07 | Standalone → integrable into PlotJuggler | Works as standalone, then embeds |
| NF-08 | Download in background thread | UI remains responsive during downloads |
| NF-09 | Performance: <100ms to load/filter registry | With ~50 extensions |
| NF-10 | Static linking in extensions | Plugins are self-contained |
Actor: PlotJuggler User Preconditions: PlotJuggler is running, internet available Flow:
Postconditions: Extension installed and registered
Actor: PlotJuggler User Preconditions: Extension installed, newer version available Flow:
Postconditions: New version installed, old version backed up
Actor: PlotJuggler User Preconditions: Extension installed; not a core (bundled) extension (those are uninstall-locked, see UC-08) Flow:
Postconditions: Extension removed, installed cache refreshed
Actor: System Preconditions: Extension recently updated, backup exists Flow:
Postconditions: User is notified; any backup remains available for manual recovery
Actor: Plugin Developer Preconditions: Developer has GitHub account, uses template Flow:
Postconditions: Extension available to all users
Actor: System Preconditions: Installed build or AppImage shipping plugins in the bundled dir; fresh user profile Flow:
Postconditions: Bundled plugins behave like normal installed extensions, managed from the extensions dir
Actor: System Preconditions: Extensions dir holds core extensions seeded by an older application version; the new application bundles newer versions Flow:
Postconditions: Core extensions are at least at the shipped version; user installs ahead of it survive
Actor: PlotJuggler User Preconditions: Core extension installed; registry offers a newer version Flow:
Postconditions: User controls the version within [bundled, latest]; the id can never disappear
Actor: Plugin Developer Preconditions: Freshly built plugin DSOs in a local folder Flow:
plotjuggler4 --plugin-dir <build-output>Postconditions: Developer builds shadow same-id plugins for the session; the user profile keeps its core extensions
| Category | Value | Description | Example |
|---|---|---|---|
| Data Loader | data_loader | Loads data from files (atomic operation) | CSV, MCAP, ROS bags |
| Data Streamer | data_streamer | Continuous streaming at 50Hz, thread-safe | ROS 2, MQTT, ZMQ |
| Parser | parser | Conversion from byte blob to individual fields | Protobuf, FlatBuffers |
| Toolbox | toolbox | Tools with GUI (FFT, CSV export, quaternion) | FFT analyzer |
| Bundle | bundle | ZIP with multiple plugins from different families | ROS 2 Complete |
| Scenario | Expected Behavior |
|---|---|
| No internet connection | Show cached registry (if available), disable install/update |
| Registry URL unreachable | Show error message, offer retry |
| Download interrupted | Partial file deleted, user can retry |
| Checksum mismatch | Download rejected, user notified |
| Scenario | Expected Behavior |
|---|---|
| Insufficient disk space | Error before extraction, suggest cleanup |
| No write permission to extensions dir | Clear error message, suggest running with permissions |
| Extension directory doesn't exist | Create it automatically |
| Corrupted ZIP file | Extraction fails gracefully, user notified |
| Scenario | Expected Behavior |
|---|---|
| Extension requires newer PlotJuggler | Show warning, prevent install |
| Downgrade requested | Reject with a diagnostic; keep the local install unchanged |
| Same version reinstall | Ask confirmation, then reinstall |
| Scenario | Expected Behavior |
|---|---|
| Update requested | Stage the new version and apply on restart — never hot-swap a plugin the running session has loaded |
| Invalid staged update | Remove staged files and leave the active install untouched |
| PlotJuggler crashes before applying update | Pending update remains for next start |
| Plugin DLL in use on uninstall (Windows) | Mark the directory for restart cleanup; removed on next start |
| Scenario | Expected Behavior |
|---|---|
| Plugin crashes on load | Report load failure; automatic rollback is deferred |
| Plugin incompatible with current SDK | Clear error message, don't load |
| Embedded manifest missing or invalid | Reject install or staged promotion with diagnostics |
| Marketplace opens in a host app | Seed the initial UI from the host's loaded-plugin snapshot, then reconcile with disk refreshes on demand |
| Scenario | Expected Behavior |
|---|---|
| Bundled version equals the installed one but the files differ (rebuilt without a version bump) | No refresh — the comparison is version-only. Shipping a change requires bumping the plugin version. |
| Extension folder exists but holds no loadable plugin (gutted or half-written copy) | Treated as absent: the seed re-copies the bundled payload. |
| Refresh fails mid-way (disk full, locked files) | The previous working copy is kept or restored; a warning diagnostic is emitted and the seed retries next launch. |
| Seed cannot write the extensions dir (read-only profile) | Bundled plugins are unavailable that session; warning diagnostic, retried next launch. |
| Staged marketplace update pending at startup | The staged install is promoted first; the seed compares against the promoted version, so it never clobbers a just-applied upgrade. |
| User wants to hold a core extension below the bundled version | Not possible through the marketplace (the seed lifts it back). Escape hatch: place the older build in a custom Preferences folder — user-explicit tiers win version-blind. |
| User adds the bundled dir itself as a custom folder | It is then scanned like any custom folder (user-explicit choice); plugins may appear loaded-but-not-installed, and for an AppImage the mount path changes every run. |
plotjuggler_sdk/pj_plugins and
expose only a small embedded manifest plus their vtable. This keeps
cross-platform CI simple and decouples plugin builds from Qt
version churn.The minimum viable product is successful if:
{
"registry_version": "1.0",
"last_updated": "ISO8601 timestamp",
"extensions": [
{
"id": "unique-extension-id",
"name": "Display Name",
"description": "Short description",
"author": "Author Name",
"publisher": "Publisher Name",
"website": "https://...",
"repository": "https://github.com/...",
"license": "SPDX identifier",
"icon_url": "https://... (optional)",
"category": "data_loader|data_streamer|parser|toolbox|bundle",
"tags": ["tag1", "tag2"],
"version": "semver",
"min_plotjuggler_version": "semver",
"plugins": [
{
"name": "PluginClassName",
"type": "plugin_type",
"library": "library_name_without_extension"
}
],
"platforms": {
"linux-x86_64": {
"url": "https://...",
"checksum": "sha256:..."
}
},
"changelog": {
"1.0.0": "Initial release"
}
}
]
}
There is no separate local state file. Installed extensions are discovered at runtime by
scanning extensions_dir, loading candidate plugin DSOs, and reading each DSO's embedded
plugin manifest. The marketplace never writes installed-state or plugin-manifest sidecars.
Staged updates use a transient .pj_pending_install intent file so restart-time
promotion can revalidate the staged DSO against the registry id/version that created it.
Fields read from the embedded plugin manifest:
| Field | Source |
|---|---|
id | Embedded plugin manifest key "id" |
version | Embedded plugin manifest key "version" |
install_date | Last-modified timestamp of the extension root directory |
path | The scanned subdirectory itself |
enabled | Always true by default (no persistence yet) |
{
"id": "extension-id",
"version": "semver",
"min_plotjuggler_version": "semver",
"plugins": [
{
"name": "PluginClassName",
"type": "plugin_type",
"library": "library_name",
"ui_file": "optional_ui_file.ui"
}
]
}
Remaining implementation follow-ups are tracked in TODO.md.
This file should be updated when:
Do NOT add: