docs: amend Constitution Principle II for tsc --noEmit

- Constitution II: permit `tsc --noEmit` with allowJs/checkJs as a
  CI/dev-time check and encourage JSDoc + `// @ts-check`; emit-mode
  TypeScript, .ts/.tsx source, JSX, frameworks, and bundlers stay
  forbidden in core. Bump version 1.0.0 -> 1.1.0.
- Constitution IV: add static/slopsmith.d.ts to the stable-contract
  surface -- breaking changes need a "Migration notes" CHANGELOG entry.
- CHANGELOG: note the typecheck step + ambient .d.ts under [Unreleased].
- CLAUDE.md: point the Frontend Conventions section at slopsmith.d.ts
  as the typed plugin-contract source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
byrongamatos
2026-06-18 00:33:22 -07:00
committed by Bret Mogilefsky
co-authored by Claude Opus 4.7
parent 6f433d9cea
commit de7ced6429
3 changed files with 24 additions and 6 deletions
+22 -6
View File
@@ -41,12 +41,17 @@ is Tailwind CSS, served as a prebuilt static stylesheet
(`static/tailwind.min.css`, regenerated by `scripts/build-tailwind.sh`) (`static/tailwind.min.css`, regenerated by `scripts/build-tailwind.sh`)
— never the runtime Play CDN, whose on-the-fly JIT rescans the DOM on — never the runtime Play CDN, whose on-the-fly JIT rescans the DOM on
the main thread and caused sustained frame drops with the 3D highway the main thread and caused sustained frame drops with the 3D highway
(slopsmith-desktop#110). No React, Vue, Svelte, bundler, transpiler, or (slopsmith-desktop#110). No React, Vue, Svelte, bundler, or transpiler
TypeScript appears in the core static tree, and no build step runs on appears in the core static tree, and no build step runs on the serve
the serve path: the Tailwind build is a maintainer-only one-shot whose path: the Tailwind build is a maintainer-only one-shot whose output is
output is committed, so Docker / desktop / end users never build. New committed, so Docker / desktop / end users never build. `tsc --noEmit`
features extend `app.js` and the existing globals (`window.playSong`, with `allowJs` / `checkJs` is permitted as a CI / dev-time type check
`window.showScreen`, `window.createHighway`, `window.slopsmith`). — JSDoc type annotations and `// @ts-check` directives in core `.js`
files are encouraged. TypeScript stays a dev-only devDependency; emit
-mode TypeScript, `.ts` / `.tsx` source, JSX, frameworks, and bundlers
remain forbidden in core. New features extend `app.js` and the existing
globals (`window.playSong`, `window.showScreen`,
`window.createHighway`, `window.slopsmith`).
**Non-negotiable rules** **Non-negotiable rules**
@@ -63,6 +68,11 @@ features extend `app.js` and the existing globals (`window.playSong`,
and does not re-apply the base reset core already provides. Plugins and does not re-apply the base reset core already provides. Plugins
MUST NOT load the Tailwind Play CDN (or any runtime CSS JIT) — the MUST NOT load the Tailwind Play CDN (or any runtime CSS JIT) — the
same no-CDN, build-free-at-serve rule that binds core binds plugins. same no-CDN, build-free-at-serve rule that binds core binds plugins.
- `tsc --noEmit` with `allowJs` / `checkJs` is permitted as a CI /
dev-time type check. JSDoc type annotations and `// @ts-check`
directives in core `.js` files are encouraged. TypeScript stays a
dev-only devDependency — emit-mode TypeScript, `.ts` / `.tsx` source,
JSX, frameworks, and bundlers remain forbidden in core.
- New UI state lives in `localStorage` (or a backend endpoint), not in a - New UI state lives in `localStorage` (or a backend endpoint), not in a
framework store. framework store.
- Naming: camelCase JS, kebab-case CSS, snake_case plugin IDs. Player - Naming: camelCase JS, kebab-case CSS, snake_case plugin IDs. Player
@@ -117,6 +127,12 @@ format. Both must keep playing across releases.
- Existing arrangement IDs, sloppak manifests, and the highway - Existing arrangement IDs, sloppak manifests, and the highway
WebSocket message shape are stable contracts. Breaking changes WebSocket message shape are stable contracts. Breaking changes
require a CHANGELOG entry under "Migration notes". require a CHANGELOG entry under "Migration notes".
- The plugin-facing JS contracts — `window.slopsmith`, `window.highway`,
the highway WebSocket message shape, and the `setRenderer`
visualization contract — are declared in `static/slopsmith.d.ts` and
form part of this stable-contract surface. The `.d.ts` is the typed
source of truth; a breaking change to it requires the same CHANGELOG
entry under "Migration notes".
### V. Pure-Function Core Libraries, Tested ### V. Pure-Function Core Libraries, Tested
+1
View File
@@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Perf**: reduce per-frame allocations in the 2D highway chord + lyric render paths. `_ensureChordRenderCache` now also caches `sortedNotes` / `nonZeroNotes` / `nonZeroFrets` / `allMuted` / `hasMultipleNotes` (computed once per chord, invalidated on `src` / `_inverted` / `chordTemplates` change — the third key catches a stale `isOpen`-derived classification when the WS `chord_templates` message lands after the final `chords` chunk), so `drawChords` no longer re-sorts / re-filters / spreads min-max per visible chord per frame. The in-chord unison bend classification is folded inline (no `chordPositions.filter` × 2 per frame). `drawLyrics` memoizes `ctx.measureText` results in a two-level `Map<fontSize, Map<text, width>>` so cache hits don't allocate a composite string key. Lit-sustain shimmer in `drawSustains` swaps the 4 per-note-per-frame `Math.random()` calls for a 64-entry precomputed jitter LUT (xorshift32-seeded — the LUT contents are reload-stable and test-reproducible; rendered shimmer is deterministic per `createHighway()` instance, since the seed includes that instance's `_frameIdx`) indexed by `(frameIdx + n.s + ⌊n.t·60⌋)`, visually indistinguishable and allocation-free. - **Perf**: reduce per-frame allocations in the 2D highway chord + lyric render paths. `_ensureChordRenderCache` now also caches `sortedNotes` / `nonZeroNotes` / `nonZeroFrets` / `allMuted` / `hasMultipleNotes` (computed once per chord, invalidated on `src` / `_inverted` / `chordTemplates` change — the third key catches a stale `isOpen`-derived classification when the WS `chord_templates` message lands after the final `chords` chunk), so `drawChords` no longer re-sorts / re-filters / spreads min-max per visible chord per frame. The in-chord unison bend classification is folded inline (no `chordPositions.filter` × 2 per frame). `drawLyrics` memoizes `ctx.measureText` results in a two-level `Map<fontSize, Map<text, width>>` so cache hits don't allocate a composite string key. Lit-sustain shimmer in `drawSustains` swaps the 4 per-note-per-frame `Math.random()` calls for a 64-entry precomputed jitter LUT (xorshift32-seeded — the LUT contents are reload-stable and test-reproducible; rendered shimmer is deterministic per `createHighway()` instance, since the seed includes that instance's `_frameIdx`) indexed by `(frameIdx + n.s + ⌊n.t·60⌋)`, visually indistinguishable and allocation-free.
### Added ### Added
- Optional JS type checking via `tsc --noEmit`. Added a `static/slopsmith.d.ts` ambient declaration file — the typed source of truth for the plugin-facing contract (`window.slopsmith`, `window.highway`, the highway WebSocket message shape, the `setRenderer` visualization contract, the keyboard-shortcut API) — plus a `tsconfig.json`, a `typecheck` npm script, and a CI step. TypeScript is a dev-only devDependency: no build/emit step, no runtime dependency, every shipped `.js` file stays byte-identical. Core `.js` files opt in individually with a `// @ts-check` directive (currently `static/app.js` and `static/highway.js`); files without it are unaffected.
- **Player progression: Mastery Rank, instrument-path challenges, daily/weekly quests, Decibels currency, cosmetics shop (spec 010).** Onboarding gains two steps: pick one or more **instrument paths** (Guitar / Bass / Drums — data-driven, more can ship as content) and a **calibration challenge** offer (play the bundled Slopsmith Diagnostic with note detection at 100% accuracy — or skip; either way you reach **Mastery Rank 1**, and a skipped calibration can still be completed later from the Progress screen). Each path levels by completing a content-defined number of **challenges** (any order) from that level's set; Mastery Rank = onboarding rank + the sum of path levels, starting at 0 on a fresh install. The existing unified XP backend is untouched but the frontend renames it to **Decibels (dB)** — a spendable currency earned ONLY by playing (songs, FeedBarcade rounds, quest rewards; no real-money path exists or may be added) — with spend tracked in a separate wallet so lifetime earnings stay monotonic. Rotating **daily/weekly quests** (deterministic per period, lazy instantiation, local-midnight / Monday resets) award dB and feed `quest_completed` challenges. A new **Progress** screen (rank hero, per-path checklists, quest countdowns, add-a-path) and **Shop** screen (themes via CSS-variable swaps under `html[data-fb-theme]`, avatar frames; atomic balance-checked purchases — 402 on insufficient dB, 409 on re-buy) join the v3 nav, and the topbar badge now shows Rank + challenge-set progress + dB balance. All definitions live in `data/progression/` JSON (paths/levels/challenges, quest pools, shop catalog) — adding a rank, challenge, quest, or cosmetic is a content edit + restart, never code; invalid content degrades to logged warnings. New tables (additive + idempotent): `progression_state`, `player_paths`, `challenge_progress`, `quest_state`, `wallet`, `shop_owned`, `shop_equipped`. New endpoints: `GET /api/progression`, `POST /api/progression/paths|onboarding|events` (events whitelists `minigame_run`; `song_completed` stays server-derived inside `POST /api/stats`, which now resolves the instrument server-side and reports an additive `progression` outcome key), `GET /api/shop`, `POST /api/shop/buy|equip`; equipped cosmetics ride along on `GET /api/profile`. A new **`progression` capability domain** (core-owned, kind: command, safety: safe — `inspect`, `record-event`, `list-shop`, `buy-item`/`equip-item` gated on user action) emits `challenge-completed`/`quest-completed`/`path-level-up`/`rank-changed`/`db-changed`/`calibration-completed`/`cosmetic-equipped`, mirrored as `progression:*` window events, with a redaction-safe diagnostics contributor; backend plugins get the symmetric `record_progression_event` context hook (the bundled minigames hub reports runs through it, guarded for standalone). Spec: `specs/010-progression-domain/`. Tests: `tests/test_progression.py`, `tests/test_progression_api.py`. **Migration notes:** existing XP totals carry over as lifetime dB (balance = lifetime spent); resetting a per-source XP contribution (e.g. a minigames profile reset) after spending can clamp the spendable balance to 0 until new dB is earned; drums-path v1 content uses currently-satisfiable goals (arcade rounds, quests, any-instrument plays) until drums scoring lands. - **Player progression: Mastery Rank, instrument-path challenges, daily/weekly quests, Decibels currency, cosmetics shop (spec 010).** Onboarding gains two steps: pick one or more **instrument paths** (Guitar / Bass / Drums — data-driven, more can ship as content) and a **calibration challenge** offer (play the bundled Slopsmith Diagnostic with note detection at 100% accuracy — or skip; either way you reach **Mastery Rank 1**, and a skipped calibration can still be completed later from the Progress screen). Each path levels by completing a content-defined number of **challenges** (any order) from that level's set; Mastery Rank = onboarding rank + the sum of path levels, starting at 0 on a fresh install. The existing unified XP backend is untouched but the frontend renames it to **Decibels (dB)** — a spendable currency earned ONLY by playing (songs, FeedBarcade rounds, quest rewards; no real-money path exists or may be added) — with spend tracked in a separate wallet so lifetime earnings stay monotonic. Rotating **daily/weekly quests** (deterministic per period, lazy instantiation, local-midnight / Monday resets) award dB and feed `quest_completed` challenges. A new **Progress** screen (rank hero, per-path checklists, quest countdowns, add-a-path) and **Shop** screen (themes via CSS-variable swaps under `html[data-fb-theme]`, avatar frames; atomic balance-checked purchases — 402 on insufficient dB, 409 on re-buy) join the v3 nav, and the topbar badge now shows Rank + challenge-set progress + dB balance. All definitions live in `data/progression/` JSON (paths/levels/challenges, quest pools, shop catalog) — adding a rank, challenge, quest, or cosmetic is a content edit + restart, never code; invalid content degrades to logged warnings. New tables (additive + idempotent): `progression_state`, `player_paths`, `challenge_progress`, `quest_state`, `wallet`, `shop_owned`, `shop_equipped`. New endpoints: `GET /api/progression`, `POST /api/progression/paths|onboarding|events` (events whitelists `minigame_run`; `song_completed` stays server-derived inside `POST /api/stats`, which now resolves the instrument server-side and reports an additive `progression` outcome key), `GET /api/shop`, `POST /api/shop/buy|equip`; equipped cosmetics ride along on `GET /api/profile`. A new **`progression` capability domain** (core-owned, kind: command, safety: safe — `inspect`, `record-event`, `list-shop`, `buy-item`/`equip-item` gated on user action) emits `challenge-completed`/`quest-completed`/`path-level-up`/`rank-changed`/`db-changed`/`calibration-completed`/`cosmetic-equipped`, mirrored as `progression:*` window events, with a redaction-safe diagnostics contributor; backend plugins get the symmetric `record_progression_event` context hook (the bundled minigames hub reports runs through it, guarded for standalone). Spec: `specs/010-progression-domain/`. Tests: `tests/test_progression.py`, `tests/test_progression_api.py`. **Migration notes:** existing XP totals carry over as lifetime dB (balance = lifetime spent); resetting a per-source XP contribution (e.g. a minigames profile reset) after spending can clamp the spendable balance to 0 until new dB is earned; drums-path v1 content uses currently-satisfiable goals (arcade rounds, quests, any-instrument plays) until drums scoring lands.
- **3D highway score FX (notedetect game-scoring layer).** The bundled `highway_3d` renderer now visualizes the scoring layer shipped in slopsmith-plugin-notedetect ≥1.13: floating **"+N" score pops** above each judged gem (sourced from the note-state provider's new `{ points, mult, popKey }` verdict fields — chord members share the chord-level `popKey`, so a chord pops once, not once per string), plus session-level FX from the new `notedetect:fx` event — a particle burst at the strike line on streak milestones (25/50/every 100), an expanding ring pulse on multiplier tier-ups (×2/×3/×4), and a brief red wash when a ≥10 streak breaks. Colors and the pop font follow the user's notedetect scoring-UI skin (`slopsmith_notedetect_skin`: neon/esports/metal, refreshed live on the `notedetect:skin` bus event). Everything renders on the existing 2D overlay canvas from fixed-size slot pools — no Three.js geometry, no text-sprite cache traffic, near-zero cost when idle — and degrades to a silent no-op with older notedetect builds (the new fields/events simply never arrive). Splitscreen panels scope FX to their own detector instance via the bubbling per-panel `notedetect:fx` dispatch. `plugins/highway_3d` v3.24.1. - **3D highway score FX (notedetect game-scoring layer).** The bundled `highway_3d` renderer now visualizes the scoring layer shipped in slopsmith-plugin-notedetect ≥1.13: floating **"+N" score pops** above each judged gem (sourced from the note-state provider's new `{ points, mult, popKey }` verdict fields — chord members share the chord-level `popKey`, so a chord pops once, not once per string), plus session-level FX from the new `notedetect:fx` event — a particle burst at the strike line on streak milestones (25/50/every 100), an expanding ring pulse on multiplier tier-ups (×2/×3/×4), and a brief red wash when a ≥10 streak breaks. Colors and the pop font follow the user's notedetect scoring-UI skin (`slopsmith_notedetect_skin`: neon/esports/metal, refreshed live on the `notedetect:skin` bus event). Everything renders on the existing 2D overlay canvas from fixed-size slot pools — no Three.js geometry, no text-sprite cache traffic, near-zero cost when idle — and degrades to a silent no-op with older notedetect builds (the new fields/events simply never arrive). Splitscreen panels scope FX to their own detector instance via the bubbling per-panel `notedetect:fx` dispatch. `plugins/highway_3d` v3.24.1.
- **3D highway: slide direction arrows + gem-follow animation.** Slide notes now show a / arrow indicating which way the slide goes — on the note/gem itself, as an early preview on the neck before the note arrives, and (optionally) chained further ahead for multi-leg slides — each independently toggleable in Settings → 3D Highway (`slideArrowApproachVisible`, `slideArrowNeckVisible`, `slideArrowChainPreviewVisible`). The note gem also now visually glides from its starting fret to the slide's destination over the note's sustain and holds there through the brief post-sustain linger, instead of snapping back to the starting fret — most noticeable on unpitched "slide to nothing" notes. `plugins/highway_3d` v3.25.2. - **3D highway: slide direction arrows + gem-follow animation.** Slide notes now show a / arrow indicating which way the slide goes — on the note/gem itself, as an early preview on the neck before the note arrives, and (optionally) chained further ahead for multi-leg slides — each independently toggleable in Settings → 3D Highway (`slideArrowApproachVisible`, `slideArrowNeckVisible`, `slideArrowChainPreviewVisible`). The note gem also now visually glides from its starting fret to the slide's destination over the note's sustain and holds there through the brief post-sustain linger, instead of snapping back to the starting fret — most noticeable on unpitched "slide to nothing" notes. `plugins/highway_3d` v3.25.2.
+1
View File
@@ -547,6 +547,7 @@ Sloppak is the preferred format for new features. The [Stems plugin](https://git
- **Styling** — Tailwind CSS utility classes, dark theme (`bg-dark-600`, `text-gray-300`, accent `#4080e0`, gold `#e8c040`). Tailwind is served as a **prebuilt** stylesheet (`static/tailwind.min.css`, regenerated by `bash scripts/build-tailwind.sh`), **never** the runtime Play CDN — the CDN's on-the-fly JIT rescanned the DOM on the main thread and dropped ~26% of frames with the 3D highway (slopsmith-desktop#110). The committed CSS only contains classes the build scanner saw, so CI (`tailwind-fresh`) rebuilds and diffs it; run the build script and commit when you add new classes. A plugin that uses classes not guaranteed in core (notably arbitrary values like `w-[37px]`) MUST ship its own compiled stylesheet via the `styles` manifest key, built with `corePlugins.preflight = false` (utilities only — core ships the one base reset). Plugins MUST NOT load the Tailwind Play CDN or any runtime CSS JIT. See constitution Principle II. - **Styling** — Tailwind CSS utility classes, dark theme (`bg-dark-600`, `text-gray-300`, accent `#4080e0`, gold `#e8c040`). Tailwind is served as a **prebuilt** stylesheet (`static/tailwind.min.css`, regenerated by `bash scripts/build-tailwind.sh`), **never** the runtime Play CDN — the CDN's on-the-fly JIT rescanned the DOM on the main thread and dropped ~26% of frames with the 3D highway (slopsmith-desktop#110). The committed CSS only contains classes the build scanner saw, so CI (`tailwind-fresh`) rebuilds and diffs it; run the build script and commit when you add new classes. A plugin that uses classes not guaranteed in core (notably arbitrary values like `w-[37px]`) MUST ship its own compiled stylesheet via the `styles` manifest key, built with `corePlugins.preflight = false` (utilities only — core ships the one base reset). Plugins MUST NOT load the Tailwind Play CDN or any runtime CSS JIT. See constitution Principle II.
- **Naming** — camelCase for JS functions, kebab-case for CSS classes, snake_case for plugin IDs - **Naming** — camelCase for JS functions, kebab-case for CSS classes, snake_case for plugin IDs
- **Player layout** — `#player` is `display:flex; flex-direction:column; position:fixed; inset:0`. `#highway` is `flex:1`. `#player-controls` sits at the bottom. Hiding the highway collapses the layout — use `margin-top: auto` on controls if you need to hide it. - **Player layout** — `#player` is `display:flex; flex-direction:column; position:fixed; inset:0`. `#highway` is `flex:1`. `#player-controls` sits at the bottom. Hiding the highway collapses the layout — use `margin-top: auto` on controls if you need to hide it.
- **Plugin contract types** — the plugin-facing JS surface (`window.slopsmith`, `window.highway`, the highway WebSocket message shape, the `setRenderer` contract, the keyboard-shortcut API) is declared in `static/slopsmith.d.ts`. That file is the typed source of truth — consult it for exact signatures, and update it when you change a contract. Core `.js` files opt into checking with a `// @ts-check` directive on line 1; `npm run typecheck` (`tsc --noEmit`, dev/CI-only) verifies them. No build or emit step — the shipped files are plain JS.
## Backend Conventions ## Backend Conventions