diff --git a/.claude/README.md b/.claude/README.md index 4775173..0100d58 100644 --- a/.claude/README.md +++ b/.claude/README.md @@ -4,7 +4,7 @@ This directory holds [Claude Code](https://claude.ai/code) artifacts: skills, ru ## Layout -``` +```text .claude/ ├── agents/ Subagents (invoked via @) │ └── slopsmith-reviewer.md Plugin-aware code review diff --git a/.claude/agents/slopsmith-reviewer.md b/.claude/agents/slopsmith-reviewer.md index 4234ac0..10f0077 100644 --- a/.claude/agents/slopsmith-reviewer.md +++ b/.claude/agents/slopsmith-reviewer.md @@ -46,7 +46,7 @@ Run each item; structure the output as `PASS` / `FAIL` / `N/A` with file:line ci ## Output format -``` +```text plugin-review: ========================= 1. manifest validates PASS diff --git a/.claude/rules/plugin-author.md b/.claude/rules/plugin-author.md index 41a9c2e..679e833 100644 --- a/.claude/rules/plugin-author.md +++ b/.claude/rules/plugin-author.md @@ -32,7 +32,7 @@ These rules apply only when editing files under `plugins/**`. They encode the co ## State and config - **`localStorage` keys must be prefixed with the plugin id** to avoid collisions. -- **`settings.server_files` declares config-dir paths the plugin wants included in the Settings export/import flow.** Relpaths only — no `..`, no abs paths, no backslashes. See [`docs/plugin-manifest.md`](../../docs/plugin-manifest.md). +- **`settings.server_files` declares config-dir paths the plugin wants to include in the Settings export/import flow.** Relpaths only — no `..`, no abs paths, no backslashes. See [`docs/plugin-manifest.md`](../../docs/plugin-manifest.md). - **`diagnostics.server_files` / `diagnostics.callable`** declares what enters the Export Diagnostics bundle. Keep payloads under 100 KB and don't include secrets. See [`docs/plugin-diagnostics.md`](../../docs/plugin-diagnostics.md). ## Visualization specifics diff --git a/.claude/skills/plugin-validate/SKILL.md b/.claude/skills/plugin-validate/SKILL.md index d357230..821a636 100644 --- a/.claude/skills/plugin-validate/SKILL.md +++ b/.claude/skills/plugin-validate/SKILL.md @@ -31,6 +31,7 @@ for path in sorted(glob.glob('plugins/*/plugin.json')): plugin_dir = pathlib.Path(path).parent plugin_id = plugin_dir.name m = json.load(open(path)) + plugin_ok = True # per-iteration flag so we don't print OK after a later FAIL # 1. Schema try: jsonschema.validate(m, schema) @@ -42,16 +43,19 @@ for path in sorted(glob.glob('plugins/*/plugin.json')): if m['id'] != plugin_id: print(f"FAIL {path}: id={m['id']!r} but directory is {plugin_id!r}") ok = False + plugin_ok = False # 3. Declared files exist for field in ('script', 'routes', 'tour'): if field in m and not (plugin_dir / m[field]).exists(): print(f"FAIL {path}: {field}={m[field]!r} but file missing") ok = False + plugin_ok = False if 'settings' in m and 'html' in m['settings']: h = m['settings']['html'] if not (plugin_dir / h).exists(): print(f"FAIL {path}: settings.html={h!r} but file missing") ok = False + plugin_ok = False for field in ('settings', 'diagnostics'): if field in m and 'server_files' in m[field]: for relpath in m[field]['server_files']: @@ -61,7 +65,9 @@ for path in sorted(glob.glob('plugins/*/plugin.json')): if '..' in relpath or relpath.startswith('/') or '\\' in relpath: print(f"FAIL {path}: {field}.server_files contains unsafe path {relpath!r}") ok = False - print(f"OK {path}") + plugin_ok = False + if plugin_ok: + print(f"OK {path}") sys.exit(0 if ok else 1) PY ``` @@ -84,13 +90,13 @@ pytest tests/test_plugin_schema.py::test_schema_license_enum_subset_of_contribut Use the format from the script: `OK ` per validated manifest, `FAIL : ` per failure. Add a one-line summary: -``` +```text Result: 3/3 plugins valid (OK app_tour_library, app_tour_settings, highway_3d) ``` or -``` +```text Result: 2/3 plugins valid; 1 FAIL (see above) ``` diff --git a/.github/workflows/validate-plugins.yml b/.github/workflows/validate-plugins.yml index 665115c..101cac3 100644 --- a/.github/workflows/validate-plugins.yml +++ b/.github/workflows/validate-plugins.yml @@ -67,4 +67,8 @@ jobs: PY - name: Run schema sanity tests - run: pytest tests/test_plugin_schema.py -v + # --noconftest skips tests/conftest.py, which imports structlog + # (not in requirements-test.txt). The schema tests don't use + # shared fixtures, so this is safe and avoids dragging the full + # runtime requirements into a 2 KB schema-validation job. + run: pytest tests/test_plugin_schema.py -v --noconftest diff --git a/AGENTS.md b/AGENTS.md index 2d539aa..c9314c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ This file is the canonical orientation. Tool-specific automation (Claude skills/ ## Architecture quick reference -``` +```text server.py FastAPI app — library API, WebSocket highway, plugin loading main.py Programmatic uvicorn entrypoint — installs structlog before boot logging_setup.py Structured logging + correlation IDs (LOG_LEVEL/LOG_FORMAT/LOG_FILE) diff --git a/docs/PLUGIN_AUTHORING.md b/docs/PLUGIN_AUTHORING.md index bbb8f3e..b1017ce 100644 --- a/docs/PLUGIN_AUTHORING.md +++ b/docs/PLUGIN_AUTHORING.md @@ -6,7 +6,7 @@ This guide is the entry point. Each topic below has a dedicated doc — read wha ## Quickstart -``` +```text plugins/my_plugin/ ├── plugin.json Manifest (required) — see docs/plugin-manifest.md ├── screen.html Optional — markup mounted at #plugin-my_plugin diff --git a/docs/plugin-logging.md b/docs/plugin-logging.md index abad16a..9c3ea0f 100644 --- a/docs/plugin-logging.md +++ b/docs/plugin-logging.md @@ -44,7 +44,7 @@ That logger inherits from the root `slopsmith` logger, which is configured by `l Look for your plugin's logger name in the console output: -``` +```text INFO slopsmith.plugin.my_plugin: plugin ready ``` diff --git a/docs/testing-plugins.md b/docs/testing-plugins.md index aba5a23..655c7d8 100644 --- a/docs/testing-plugins.md +++ b/docs/testing-plugins.md @@ -4,7 +4,7 @@ Slopsmith has three test surfaces — Python unit/integration tests (pytest), JS ## Test layout -``` +```text tests/ ├── conftest.py Shared pytest fixtures (isolate_logging) ├── test_plugins.py Plugin loader + load_sibling + collision tests (includes reset_plugin_state) diff --git a/docs/websocket-protocol.md b/docs/websocket-protocol.md index 7178c3d..135f757 100644 --- a/docs/websocket-protocol.md +++ b/docs/websocket-protocol.md @@ -9,7 +9,7 @@ Each connection receives the following JSON frames, roughly in this order: | Message | Shape | Description | |---------|-------|-------------| | `loading` | `{ type: 'loading', stage }` | Status/progress message during extraction or conversion. | -| `song_info` | `{ type, title, artist, arrangement, arrangement_index, arrangements, duration, tuning, capo, format, audio_url, audio_error, stems }` | Song metadata. `arrangements` is the full list for the switcher. `audio_url` is `null` when audio is unavailable, in which case `audio_error` is non-null; otherwise `audio_error` is `null`. `stems` is always present — an empty array for non-sloppak songs or sloppak songs with no split stems. `tuning` is an array (6 for guitar, 4 for bass). | +| `song_info` | `{ type, title, artist, arrangement, arrangement_index, arrangements, duration, tuning, capo, format, audio_url, audio_error, stems }` | Song metadata. `arrangements` is the full list for the switcher. `audio_url` is `null` when audio is unavailable, in which case `audio_error` is non-null; otherwise `audio_error` is `null`. `stems` is always present — an empty array for non-sloppak songs or sloppak songs with no split stems. `tuning` is an array whose length depends on the source arrangement (typically 6 for guitar, 4 for bass, but extended-range GP imports can be 7/8 for guitar or 5/6 for bass — use `highway.getStringCount()` for the authoritative count). | | `beats` | `{ type, data: [{ time, measure }] }` | Beat timestamps with measure numbers. | | `sections` | `{ type, data: [{ time, name }] }` | Named sections (Intro, Verse, Chorus, etc.). | | `anchors` | `{ type, data: [{ time, fret, width }] }` | Fret zoom anchors. | diff --git a/schema/plugin.schema.json b/schema/plugin.schema.json index fa83452..eefd8b5 100644 --- a/schema/plugin.schema.json +++ b/schema/plugin.schema.json @@ -92,8 +92,8 @@ "type": "array", "items": { "type": "string", - "not": { "pattern": "^/|^[a-zA-Z]:|\\\\|(^|/)\\.\\.(/|$)" }, - "description": "Relpath under context['config_dir']. No abs paths, no '..', no backslashes. Trailing '/' denotes a directory (recurse)." + "not": { "pattern": "^/|^[a-zA-Z]:|\\\\|(^|/)\\.\\.(/|$)|//|(^|/)\\.(/|$)|^\\." }, + "description": "Relpath under context['config_dir']. No abs paths, no '..', no '//', no './', no leading dotfiles, no backslashes. Mirrors the runtime _validate_relpath rules in plugins/__init__.py so a schema-valid manifest is also load-time-valid. Trailing '/' denotes a directory (recurse)." }, "uniqueItems": true, "description": "Opt-in for Settings export/import (slopsmith#113). Files included in user-triggered backups. See docs/plugin-manifest.md." @@ -108,10 +108,10 @@ "type": "array", "items": { "type": "string", - "not": { "pattern": "^/|^[a-zA-Z]:|\\\\|(^|/)\\.\\.(/|$)" } + "not": { "pattern": "^/|^[a-zA-Z]:|\\\\|(^|/)\\.\\.(/|$)|//|(^|/)\\.(/|$)|^\\." } }, "uniqueItems": true, - "description": "Files copied verbatim into plugins// inside the diagnostics bundle." + "description": "Files copied verbatim into plugins// inside the diagnostics bundle. Path rules match the settings.server_files pattern (no abs paths, no '..', no '//', no './', no leading dotfiles, no backslashes)." }, "callable": { "type": "string",