docs: extract plugin contracts into modular docs and slim CLAUDE.md

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 <mgcdreamer@gmail.com>
This commit is contained in:
Miguel_LZPF
2026-06-18 00:38:50 -07:00
committed by Bret Mogilefsky
parent cad78857fb
commit 1214a6c0a0
12 changed files with 1013 additions and 625 deletions
+67
View File
@@ -0,0 +1,67 @@
# Plugin logging
Use `context["log"]` for all backend plugin output. It is a stdlib `logging.Logger` namespaced to `slopsmith.plugin.<id>`, pre-configured with the app-wide level, format (including JSON mode), and correlation IDs. **Never use `print()`** — it bypasses correlation context and log rotation.
## Backend usage
```python
def setup(app, context):
log = context["log"]
log.info("plugin ready")
log.warning("optional dependency %r not found, feature disabled", dep)
try:
risky_init()
except Exception:
log.exception("unhandled error during setup") # auto-captures traceback
```
## CLI entry-point fallback
For helper scripts that also run as `__main__`, add a stdlib fallback so the logger works without the server pipeline:
```python
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
```
## How it's wired
The plugin loader assigns each plugin a logger before calling `setup()`:
```python
context["log"] = logging.getLogger(f"slopsmith.plugin.{plugin_id}")
```
That logger inherits from the root `slopsmith` logger, which is configured by `logging_setup.configure_logging()` (called from `main.py` before uvicorn boots). It respects:
- **`LOG_LEVEL`** — `DEBUG | INFO | WARNING | ERROR` (default `INFO`)
- **`LOG_FORMAT`** — `json | text` (default `text` — coloured console)
- **`LOG_FILE`** — optional path for a persistent log file (e.g. `/config/slopsmith.log`)
- **Correlation IDs** — each HTTP request carries `X-Request-ID`; structlog injects it into every log line during that request's lifetime. WebSocket sessions get their own `ws_conn_id` contextvar bound at accept time.
## Verifying
Look for your plugin's logger name in the console output:
```
INFO slopsmith.plugin.my_plugin: plugin ready
```
Switch `LOG_FORMAT=json` to see structured output:
```json
{"timestamp": "...", "level": "info", "logger": "slopsmith.plugin.my_plugin", "event": "plugin ready", "request_id": "..."}
```
## Common mistakes
- **Using `print()` inside `setup()` or request handlers.** `print()` goes to stdout, bypasses level filtering, log rotation, JSON formatting, and correlation IDs. Audit your plugin with `grep -nR 'print(' plugins/<id>/`.
- **Creating a new logger with `logging.getLogger("my_plugin")`.** This creates an unparented logger that doesn't inherit slopsmith's configuration. Always use the one passed via `context["log"]`.
- **Logging at `INFO` in hot paths.** Default `LOG_LEVEL` is `INFO`, so `log.info(...)` inside a per-frame or per-message loop floods output. Use `log.debug(...)` for hot-path tracing.
## Related
- [PLUGIN_AUTHORING.md](PLUGIN_AUTHORING.md) — guide index
- [plugin-manifest.md](plugin-manifest.md) — the `routes.py` / `setup(app, context)` contract
- [plugin-diagnostics.md](plugin-diagnostics.md) — capturing logs into the diagnostics bundle