From 1214a6c0a0a073f991e1615b6a71776c224c92b0 Mon Sep 17 00:00:00 2001 From: Miguel_LZPF Date: Tue, 19 May 2026 20:16:39 +0200 Subject: [PATCH] docs: extract plugin contracts into modular docs and slim CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.md had grown to 545 lines / 50 KB — most of it plugin-author content that other AI tools (Cursor, Copilot, Codex, Aider) and humans without AI never reach. Extract the plugin surface into 10 focused docs and a JSON Schema for plugin.json, then slim CLAUDE.md to a 156-line navigable index. New docs (~999 lines total, all self-contained): docs/PLUGIN_AUTHORING.md — entry point and quickstart docs/plugin-manifest.md — plugin.json field reference docs/plugin-visualization-contracts.md — setRenderer / overlay / note-state docs/plugin-audio-mixer.md — fader registration docs/plugin-logging.md — context["log"] + env vars docs/plugin-diagnostics.md — server_files / callable docs/plugin-keyboard-shortcuts.md — registerShortcut + scopes docs/plugin-sibling-imports.md — load_sibling pattern docs/websocket-protocol.md — /ws/highway message reference docs/testing-plugins.md — pytest fixtures + Playwright schema/plugin.schema.json — Draft 2020-12 schema for plugin.json; license enum mirrors CONTRIBUTING's curated allowlist. Backs CI validation and the plugin-validate skill. CLAUDE.md slim (581 lines changed, -485): - Removed ~300 lines of plugin-author prose (now in docs/). - Kept architecture quick reference, running the app, testing, git workflow, versioning, song formats, frontend/backend conventions, plugin authoring INDEX (table → docs/), first-hour pitfalls, "For AI agents" footer. - Anchor stubs preserved next to the new index entries so deep links from specs/001-slopsmith-platform/analyze.md still resolve. Verification: python -c "import json,glob,jsonschema; s=json.load(open('schema/plugin.schema.json')); [jsonschema.validate(json.load(open(p)), s) for p in sorted(glob.glob('plugins/*/plugin.json'))]" # ok — validates highway_3d, app_tour_library, app_tour_settings Signed-off-by: Miguel_LZPF --- CLAUDE.md | 639 +------------------------ docs/PLUGIN_AUTHORING.md | 73 +++ docs/plugin-audio-mixer.md | 45 ++ docs/plugin-diagnostics.md | 86 ++++ docs/plugin-keyboard-shortcuts.md | 64 +++ docs/plugin-logging.md | 67 +++ docs/plugin-manifest.md | 138 ++++++ docs/plugin-sibling-imports.md | 48 ++ docs/plugin-visualization-contracts.md | 198 ++++++++ docs/testing-plugins.md | 104 ++++ docs/websocket-protocol.md | 44 ++ schema/plugin.schema.json | 132 +++++ 12 files changed, 1013 insertions(+), 625 deletions(-) create mode 100644 docs/PLUGIN_AUTHORING.md create mode 100644 docs/plugin-audio-mixer.md create mode 100644 docs/plugin-diagnostics.md create mode 100644 docs/plugin-keyboard-shortcuts.md create mode 100644 docs/plugin-logging.md create mode 100644 docs/plugin-manifest.md create mode 100644 docs/plugin-sibling-imports.md create mode 100644 docs/plugin-visualization-contracts.md create mode 100644 docs/testing-plugins.md create mode 100644 docs/websocket-protocol.md create mode 100644 schema/plugin.schema.json diff --git a/CLAUDE.md b/CLAUDE.md index b92650f..ae2faaa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,633 +1,22 @@ -# Slopsmith — AI Agent Guide +# CLAUDE.md -Slopsmith is a self-hosted web app for browsing, playing, and practicing interactive music notation, built around its own open `.sloppak` chart format. Charts come from importing Guitar Pro (GP5/GP8) or MusicXML, or from authoring in the built-in editor. It runs as a Docker container with a FastAPI backend (`server.py`), vanilla JavaScript frontend (`static/`), shared Python libraries (`lib/`), and an extensive plugin system (`plugins/`). There are no frontend frameworks — everything is plain JS, HTML, and Tailwind CSS. +Claude Code memory file. This repo's canonical project orientation lives in [`AGENTS.md`](AGENTS.md) - the cross-tool standard read by Cursor, Copilot, Codex, Aider, Cline, and others. The line below uses Claude Code's `@`-import to inline AGENTS.md into this memory file, so there's a single source of truth and no drift between two near-duplicate files. -## Architecture Quick Reference +@AGENTS.md -``` -server.py FastAPI app — library API, WebSocket highway, plugin loading -static/ - app.js Main frontend — screens, library views, player, plugin loader - highway.js Canvas note highway renderer (createHighway factory) - index.html Single-page app shell - style.css Custom CSS loaded alongside Tailwind -lib/ - song.py Core data models (Note, Chord, Arrangement, Song) - sloppak.py Sloppak format support - loosefolder.py Loose-folder XML chart support - audio.py OGG/MP3 audio handling - retune.py Pitch-shifting logic - tunings.py Tuning name/offset utilities - gp2rs.py Guitar Pro to arrangement XML conversion - gp2midi.py Guitar Pro to MIDI -plugins/ - __init__.py Plugin discovery, loading, requirements install - / Each plugin is its own directory (often a git submodule) -tests/ - test_*.py pytest test suite -``` +## Claude-specific surfaces -## Plugin System +The rest of this file is content that *only* makes sense for Claude Code (other AI tools have their own incompatible automation mechanisms). Skills, subagent, rule, and settings live under [`.claude/`](.claude/): -Plugins are the primary extension point. Each plugin lives in `plugins//` with a `plugin.json` manifest. Plugins are typically their own git repositories — see [CONTRIBUTING.md](CONTRIBUTING.md) for the licensing policy (curated plugins should be AGPL-3.0 or AGPL-compatible: MIT, BSD, Apache-2.0). +- [`.claude/skills/plugin-scaffold/`](.claude/skills/plugin-scaffold/SKILL.md) - generates a new plugin skeleton (visualization / overlay / settings-only / routes-only). +- [`.claude/skills/plugin-validate/`](.claude/skills/plugin-validate/SKILL.md) - validates `plugin.json` against `schema/plugin.schema.json` locally before push. +- [`.claude/skills/speckit-*/`](.claude/skills/) - spec-kit skills (auto-generated from `.specify/`; don't edit manually). +- [`.claude/rules/plugin-author.md`](.claude/rules/plugin-author.md) - glob-scoped to `plugins/**`; encodes the contracts from `docs/PLUGIN_AUTHORING.md` so suggestions don't drift from them. +- [`.claude/agents/slopsmith-reviewer.md`](.claude/agents/slopsmith-reviewer.md) - plugin-aware code-review subagent. Invoke with `@slopsmith-reviewer`. +- [`.claude/settings.json`](.claude/settings.json) - repo defaults (no hooks enabled by default; commented opt-in example for `plugin.json` validation on save). -```json -{ - "id": "my_plugin", - "name": "My Plugin", - "version": "1.0.0", - "private": false, - "type": "visualization", - "nav": { "label": "My Plugin", "screen": "plugin-my_plugin" }, - "screen": "screen.html", - "script": "screen.js", - "styles": "assets/plugin.css", - "routes": "routes.py", - "settings": { - "html": "settings.html", - "server_files": ["my_plugin.db", "my_plugin_models/"] - }, - "diagnostics": { - "server_files": ["my_plugin.diag.json"], - "callable": "diagnostics:collect" - } -} -``` +See [`.claude/README.md`](.claude/README.md) for conventions when adding more. -All fields except `id` and `name` are optional. Plugins can have any combination of frontend (screen/script), backend (routes), and settings. +## Why this file is short -`version` and `private` are advisory metadata — the plugin loader does not currently consume them, but plugins commonly include them for publishing/tooling purposes. - -`description`, `category`, and `icon` are **optional, additive v3 Pedalboard metadata** (surfaced in `/api/plugins`, consumed by the v3 Plugins page `static/v3/plugins-page.js`). `description` is a short one-sentence summary shown under the pedal name. `category` (`audio | creation | practice | game | tools`, free-form; unknown/absent → curated default → `"other"`) picks which pedalboard the plugin sits on. `icon` is an assets-relative thumbnail path (e.g. `"assets/thumb.png"`, ~square ~256×256, same containment rule as `styles`, served via `/api/plugins//assets/...`); if omitted the loader auto-detects `assets/thumb.png`, and plugins with no thumbnail get a default pedal graphic. All three are backward-compatible — omit them and the plugin still loads. See [docs/plugin-v3-ui.md](docs/plugin-v3-ui.md). - -`styles` is the **opt-in** for self-hosted CSS (Principle II — prebuilt Tailwind, no Play CDN). Core's `static/tailwind.min.css` only contains classes scanned from core source at build time, so a plugin installed at runtime (community / NAS) that uses classes core didn't scan — especially arbitrary values like `text-[11px]` — renders unstyled. Declaring `styles` makes the frontend inject one versioned `` into `` (covering the plugin's screen *and* its settings panel) pointing at the plugin's own compiled stylesheet. The value is a **plugin-root-relative path that must live under `assets/`** (e.g. `"assets/plugin.css"`) so it serves through the sandboxed `/api/plugins//assets/...` route. Build it with `corePlugins: { preflight: false }` (utilities only — core ships the single base reset; don't duplicate it) and **never** the Tailwind Play CDN. Plugins that use only core-guaranteed utilities, or ship no Tailwind, omit `styles` and are byte-for-byte unaffected. Full authoring guide + scaffold: [docs/plugin-styles.md](docs/plugin-styles.md). - -`settings.server_files` is the **opt-in** for the unified Settings export/import flow (slopsmith#113). It's a list of relpaths under `context["config_dir"]` that the plugin wants included in user-triggered backups. A trailing `/` denotes a directory (recurse). Plugins that omit this field have no server-side files exported; their state lives entirely in browser `localStorage`, which is bundled wholesale on every export. Rules: -- Relpaths only. Absolute paths, drive letters, `..` segments, and backslashes are rejected at load time with a `[Plugin]` warning. -- The same allowlist is consulted at both export and import: a bundle that references a file the *importing host*'s manifest no longer declares is skipped with a warning (handles plugin updates between export and import). A bundle that references a file your host's manifest never declared is also skipped — no surprise writes. -- Files are encoded as `{"encoding": "json", "data": }` for `.json` files that parse cleanly (diff-friendly), `{"encoding": "base64", "data": "..."}` otherwise (sqlite, model blobs, IRs). -- Plugins own their internal data migration. Importing a bundle whose data schema predates your current code restores bytes verbatim — your plugin must cope at next load. -- Symlinks are skipped on export and never followed on import. - -`diagnostics` is the **opt-in** for the troubleshooting bundle (slopsmith#166 — Settings → Export Diagnostics). Two independent fields: -- `diagnostics.server_files` — same allowlist semantics as `settings.server_files`: relpaths under `context["config_dir"]`, no `..`, no abs paths, no backslashes, no leading dots. Files listed here are copied verbatim into `plugins//` inside the bundle. Use this for snapshot-style state (small DB excerpts, model lists, last-error files). -- `diagnostics.callable` — `":"` (e.g. `"diagnostics:collect"`). Resolved lazily via `load_sibling` when the user clicks Export, then called as `func({"plugin_id": "...", "config_dir": Path(...)})`. Return `dict`/`list` → written to `plugins//callable.json`; `bytes` → `callable.bin`; `str` → `callable.txt`. Exceptions are caught and appended to the bundle's `manifest.notes` — a buggy plugin never crashes the export. - -Plugins that omit the field contribute nothing to the bundle from the backend side. Frontend plugins can independently push state via `window.slopsmith.diagnostics.contribute(plugin_id, payload)` from their `screen.js` before the user hits Export. Bundle layout + per-file schemas: [docs/diagnostics-bundle-spec.md](docs/diagnostics-bundle-spec.md). - -Best practices: -- Embed your own `schema` field (e.g. `"my_plugin.diag.v1"`) in JSON returned by `callable` so future tooling can dispatch by version. -- Keep payloads small (< 100 KB). Diagnostics are not a backup channel — that's `settings.server_files`. -- Don't include user secrets, API keys, or session tokens. The bundle is shared with maintainers / posted to GitHub issues. - -`type` is an optional role hint (slopsmith#36). Supported values: -- `"visualization"` — plugin provides a highway renderer. Declaring this makes the plugin eligible for the main-player viz picker AND splitscreen's per-panel picker. Must pair with a `window.slopsmithViz_` factory exporting the setRenderer contract below. -- Absent → no declared role; plugin is loaded and its script runs, but it doesn't appear in role-specific UIs. - -**Backend routes** — `routes.py` must export a `setup(app, context)` function. The `context` dict provides: -- `config_dir` — persistent config path -- `get_dlc_dir()` — returns the DLC folder Path -- `extract_meta()` — metadata extraction callable -- `meta_db` — shared MetadataDB instance -- `library_providers` — shared library provider registry for source-aware browsing -- `register_library_provider(provider)` — register a plugin-provided library source. Providers expose `id`, `label`, optional `kind`/`capabilities`, and callable `query_page`, `query_artists`, `query_stats`, and `tuning_names` methods. Providers with `art.read` may also expose `get_art(song_id)` returning one of: a `Response` object (any media type, served as-is); raw `bytes` or `bytearray` (**assumed PNG** — use a `Response` or a `dict` with `content`+`media_type` keys for JPEG/WebP or other formats); a URL string (http/https → 302 redirect; other schemes are rejected with 400); a filesystem path string or `Path` (served as a file with auto-detected media type); or a `dict` with a `url`, `path`, or `content` key. Providers with `song.sync` may expose `sync_song(song_id)` returning `None` (success with no local file) or a `dict` — the dict is passed through as the JSON response and should include `filename`/`local_filename` if a local playable file was produced. -- `unregister_library_provider(provider_id)` — remove a plugin-provided library source by id. The built-in `local` provider cannot be removed. -- `get_sloppak_cache_dir()` — sloppak cache path -- `load_sibling(name)` — loads a sibling module from this plugin's directory under a unique, namespaced module name. See "Sibling imports" below. -- `log` — stdlib `logging.Logger` namespaced to `slopsmith.plugin.`. Pre-configured with the app-wide level, format (including JSON mode), and correlation IDs. Use this for all backend plugin output instead of `print()`. See "Backend plugin logging" below. - -**Sibling imports — use `load_sibling`, not bare imports** (slopsmith#33). The plugin loader inserts each plugin's directory onto `sys.path` so `from extractor import X` works, but Python caches imports by **module name** in `sys.modules`. Two plugins that each ship a top-level `extractor.py` (or any other generic name — `util.py`, `client.py`, `parser.py`, `config.py`, …) collide: whichever loads first wins, and the other plugin's `from extractor import X` either gets the wrong module or fails with `cannot import name 'X' from 'extractor'`. - -The fix is `context["load_sibling"](name)`, which loads the sibling under a namespaced module name (`plugin_.`, where plugin_id is bijectively encoded so reverse-DNS-style ids like `com.example.foo` work without colliding: `_` -> `_5f_`, `.` -> `_2e_`) so each plugin gets its own copy: - -```python -def setup(app, context): - helper = context["load_sibling"]("helper") - HelperClass = helper.HelperClass - # … -``` - -Notes: -- `name` is a bare module name — no `.py` suffix, no slashes, no `.`. The helper raises `ValueError` for path traversal / format issues and `ImportError` for missing files. -- Both single-file siblings (`extractor.py`) and package-form siblings (`extractor/__init__.py`) work. Package form wins when both exist (matches CPython's import-resolution precedence). -- Relative imports between siblings work — `from .shared import X` in a top-level helper, `from ..shared import X` from inside a sibling package. The synthetic parent package `plugin_` carries the plugin directory in its `__path__`. -- `from . import sibling` (attribute-style) also resolves: loaded children are exposed as attributes on the parent package. -- Repeat calls return the cached module. Concurrent first-time calls are serialized via per-module locks so no caller observes a half-initialized module. -- Bare `import sibling` from `routes.py` still works during the transition period, but the loader prints a startup warning when it detects two plugins shipping a same-named top-level module — covering both `.py` files and package directories. Migrate to `load_sibling` to silence the warning and immunize your plugin from future ecosystem collisions. (Don't mix bare imports and `load_sibling` for the same module — they'd execute the file twice and split module-level state.) - -**Frontend scripts** — `screen.js` runs in the global scope via a `