# Server-First Item Architecture Refactor Plan ## Goal Make the **server the only source of truth** for item definitions, schema, defaults, options, validation rules, and editable behavior. The client should consume server definitions and provide UX/rendering/audio only. This plan removes client fallback definitions and introduces a repeatable, consistent item authoring structure so adding new item types is low-risk and uniform. ## Target End State ### 1) Source of truth - Server owns, for each item type: - Type id, label, tooltip - Full property schema (value type, required/optional, min/max/step, maxLength, enum options) - Defaults (global + per-item initial params) - Editability and read-only behavior - Validation, normalization, migration policy (if any) - Runtime actions (`use`, optional custom actions) - Capability list - Server sends this as canonical `uiDefinitions` + schema metadata on `welcome` (or equivalent bootstrap). ### 2) Client model - Client has no static fallback item definitions. - If schema payload is missing/invalid, item features are unavailable (explicit error/status), not silently guessed. - Client property editor and item menus are metadata-driven. - Client runtime behavior modules remain for UX/audio only (e.g., piano local mode), keyed by server type ids. ### 3) Repeatable item authoring Adding an item type uses one standard server folder/template and a short checklist, with **auto-discovery** on server boot (no manual registry edits). --- ## Proposed Architecture ## A) Server: Item Type Package Contract (Auto-Discovered) Create/standardize per-item server packages under something like: - `server/app/items/types//` Each item type package exports the same contract: - `definition.py` - `type_id`, `label`, `tooltip` - `schema` (properties + metadata) - `defaults` - `editable_properties` - `capabilities` - `validator.py` - `validate_create(params) -> normalized_params` - `validate_update(existing, patch) -> normalized_params` - Must drop unknown keys by default. - `actions.py` - `use(context, item, client, payload?) -> result` - optional `custom_actions` handlers - `ui.py` (optional if definition is enough) - transforms schema -> `uiDefinitions` payload fragments A central loader in server scans `server/app/items/types/*` at boot and imports one plugin entrypoint per folder (for example `plugin.py` with `ITEM_TYPE_PLUGIN` export). The discovered plugins are then assembled into an in-memory registry object exposing: - validation hooks - defaults - ui definitions - capabilities - action dispatch This means: - adding a new item folder + plugin file is sufficient for server registration - no hand-edited master list is required ## B) Server: Strict Params Hygiene In update flow: - Build next params by applying patch into current params. - Run through type validator that: - strips unknown keys - normalizes known keys - enforces types/ranges/options - Persist only validated output. No raw client params should persist. ## C) Server: Save Strategy Replace synchronous `save_state()` every mutation with coalesced writes: - mark dirty on mutation - debounce write (e.g., 100-300ms) - cap max delay (e.g., 1-2s) - flush on shutdown/signal This preserves durability while reducing event-loop blocking. ## D) Client: Schema-Driven UI Runtime Refactor client item registry/editor to consume server schema only. Client keeps: - Presentation helpers - Generic item behavior path driven only by schema/metadata - Optional per-item UX runtime modules only where needed (example: piano key mode) Client removes: - static defaults/options/editability lists as authority - fallback-driven assumptions - requirement for per-item client modules when behavior is generic Property editor logic becomes generic: - `valueType: boolean` -> toggle - `valueType: list` + `options` -> list select - `valueType: number` + `range` -> numeric editor/stepper - `valueType: text/sound` + `maxLength` -> text editor - `readonly` -> blocked edit with status Special-case handlers only for UX extras (e.g., live preview for certain fields). ### D.1) Dependent Property Rules Add dependency metadata to server schema so client can hide dependent fields generically. Recommended metadata fields per property: - `visibleWhen`: simple predicate (for example `{ directional: true }`) Example: - `facing` has `visibleWhen: { directional: true }` - when `directional` is `false`, `facing` is hidden - when a controlling property changes, the property menu is recomputed immediately so visibility updates live ## E) Protocol Shape (Recommended) Ensure `welcome.uiDefinitions` includes enough to be complete: - `itemTypes[]` - `type`, `label`, `tooltip` - `editableProperties[]` - `propertyMetadata{ key -> { valueType, tooltip, range, maxLength, options?, readonly?, visibleWhen? } }` - `globalProperties` - `capabilities` - `itemTypeOrder[]` Optional future: - `schemaVersion` for compatibility checks. --- ## Phased Implementation Plan ## Phase 0: Preconditions and guardrails 1. Document canonical schema contract in `docs/item-schema.md`. 2. Add tests asserting unknown keys are rejected/stripped per type. 3. Add tests asserting `uiDefinitions` completeness for all registered types. Deliverable: - Locked schema contract and tests before heavy refactor. ## Phase 1: Server type package standardization + auto-discovery 1. Standardize all existing item types to same package contract. 2. Move any remaining type-specific logic out of generic server paths into per-type packages. 3. Add auto-discovery loader APIs: - `get_type_definition(type_id)` - `validate_update(type_id, existing, patch)` - `build_ui_definitions()` 4. Loader scans item folders at startup and registers plugins automatically. Deliverable: - Uniform server-side item modules for all current item types. ## Phase 2: Strict validation and unknown-key stripping 1. Enforce strict allowed-key filtering in per-type validators. 2. Fail/strip behavior decision: - recommended: strip unknown keys on load/update, optionally log at debug level. 3. Backfill tests for each type. Deliverable: - No unsupported params can persist. ## Phase 3: Client removes authority/fallback definitions 1. Remove client hardcoded item defaults/options as authoritative data. 2. Keep only bootstrap guards: - if schema missing/invalid, fail item UX with explicit status (no fallback behavior). 3. Refactor `itemRegistry` to be a runtime cache of server definitions. Deliverable: - Client item UI driven entirely by server payload. ## Phase 4: Metadata-driven property editor + visibility dependencies 1. Replace key-specific submit/toggle/list branches with generic metadata-based handlers. 2. Keep a small optional hook map: - `onPropertyPreviewChange(type,key,value)` for UX preview. 3. Implement `visibleWhen` semantics with live menu recompute when controlling values change. 4. Verify all current item properties work with generic editor. Deliverable: - Adding a new field usually requires server changes only. ## Phase 5: Behavior registry completion (optional modules) 1. Keep one generic behavior path that works for items with no special runtime. 2. Keep per-item behavior modules only when UX/audio runtime is truly custom. 3. Ensure all runtime hooks are accessed via registry interfaces. 4. Remove any remaining type checks in `main.ts` and shared handlers. Deliverable: - `main.ts` stays orchestration-only. ## Phase 6: Coalesced persistence 1. Implement debounced save queue in server item service. 2. Add durability tests (flush on shutdown). 3. Add config knobs (debounce ms, max delay). Deliverable: - lower save overhead under bursty updates. --- ## Repeatable New Item Template When adding a new item type: 1. Server - Create `server/app/items/types//` - Implement: - `definition.py` - `validator.py` - `actions.py` - `plugin.py` (entrypoint export for auto-discovery) - Add tests: - create defaults - update validation (valid + invalid + unknown keys) - `use` behavior - `uiDefinitions` fields present 2. Client - Add `client/src/items/types//behavior.ts` only if custom UX runtime exists. - Prefer zero client type-specific code for generic items. - If behavior module is needed, register via behavior loader pattern. - No hardcoded property logic in editor. 3. Docs - Update `docs/item-types.md` - Update `docs/item-schema.md` - Update controls docs if keybindings changed. --- ## Risks and Mitigations 1. Risk: temporary client breakage when fallback removed. - Mitigation: explicit schema-required startup check and clear status error. 2. Risk: inconsistent schema during deploy rollover. - Mitigation: include `schemaVersion` and reject incompatible client/server combinations with clear reconnect message. 3. Risk: over-generalized editor misses edge-case UX. - Mitigation: keep small per-item preview hooks while generic editor handles core commit logic. 4. Risk: debounced persistence data loss on crash. - Mitigation: short debounce + max-delay + flush on shutdown. --- ## Suggested Execution Order for Your Repo (Practical) 1. Implement strict unknown-key stripping on server (highest impact, lowest UX risk). 2. Implement server plugin auto-discovery for item type folders. 3. Convert client item registry to require server schema payload (remove fallback authority). 4. Make item property editor fully metadata-driven with dependency rules. 5. Finalize optional client behavior modules (only for custom UX items like piano). 6. Add coalesced persistence. --- ## Definition of Done - Server item validators fully define accepted params and drop unknowns. - Server item types are boot-loaded from folder plugins (no manual master registry edits). - `uiDefinitions` is complete and authoritative for all item UI config. - Client contains no authoritative item defaults/options/editability outside server payload. - Client has no fallback schema path. - New item addition follows one template with predictable files/tests. - `main.ts` has no item-type-specific runtime branches. --- ## Implementation Update (2026-02-24) ### Completed - Phase 0: - Added server-side contract coverage for `uiDefinitions` completeness. - Added/kept tests for unknown-key stripping and validation behavior. - Phase 1: - Server item plugins are auto-discovered from `server/app/items/types/*/plugin.py`. - Registry now builds type order/modules from discovered plugins. - Phase 2: - Unknown params are stripped by validators and use-path updates are revalidated before persist. - Phase 3: - Client item registry now requires server `uiDefinitions`; no fallback item-definition authority. - Missing/invalid schema now disables item menus with explicit status. - Phase 4: - Property editor behavior is metadata-driven by `valueType/range/options/maxLength`. - `visibleWhen` is supported and item property rows recompute live after updates. - Phase 5: - Client runtime behavior remains modular per item via behavior registry; `main.ts` orchestration no longer carries item-specific business branches. - Phase 6: - Coalesced/debounced state saving implemented. - Flush-on-shutdown implemented. - Save timing now configurable via: - `storage.state_save_debounce_ms` - `storage.state_save_max_delay_ms` ### Notes - Client item-specific runtime is now reduced to only `piano`; simple items (`dice`, `wheel`, `clock`, `radio_station`, `widget`) run through generic client flows with no custom behavior module. - Server item implementations now live inside per-type folders (`server/app/items/types/*/module.py`) and plugins point directly to those modules. - Server type packages are now split into `definition.py` / `validator.py` / `actions.py` plus a thin `module.py` export surface. - Added docs sample folder at `docs/examples/item-type-sample/` and updated template docs to reflect the package layout.