All files / src/packlets/project-format schema.ts

100% Statements 2/2
100% Branches 0/0
100% Functions 0/0
100% Lines 2/2

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179                                                                                                                                                                                            1x                                                                                                                       1x                                                
/**
 * @packageDocumentation
 *
 * TypeBox schemas for the Beat Muser project file format.
 *
 * ## File Format Overview
 *
 * A Beat Muser project is stored as `beat-muser-project.json` in a project
 * directory. The format uses an **ECS-lite** entity model at the project level.
 *
 * ## Design Principles
 *
 * - **ECS-lite**: Entities are just `{id, version, components}`. An entity's
 *   kind is determined by the components it carries, not by a type hierarchy.
 * - **Flat**: All project data lives in a single top-level `entities` array.
 *   Charts, notes, BPM changes, sound channel definitions — everything is an
 *   entity. Relationships are expressed through reference components.
 * - **CRDT-ready**: Every entity has a UUIDv7 `id` and `version`.
 *   Merge is simple: union by `id`, higher `version` wins. Deletion strips
 *   all components and bumps the version.
 * - **Open schema**: Component objects allow `additionalProperties: true` so
 *   plugins and game modes can attach arbitrary attributes.
 * - **Versioned**: `schemaVersion` is a number for migration support.
 *   `version` is a UUIDv7 timestamp for collaborative editing.
 *
 * ## Core Components
 *
 * The base format defines a small set of core components. Plugins may add
 *   their own component names without restriction.
 *
 * | Component      | Presence implies...                                     |
 * | -------------- | ------------------------------------------------------- |
 * | `chart`        | This entity is a chart (difficulty).                    |
 * | `event`        | This entity is timed on the timeline (`y` in pulses).   |
 * | `chartRef`     | This entity belongs to a specific chart.                |
 * | `note`         | This entity is a playable note.                         |
 * | `bpmChange`    | This entity changes the BPM.                            |
 * | `sound`        | This entity is a keysound event or sound definition.    |
 * | `soundRef`     | This entity references a sound channel by UUID.         |
 *
 * ## Example Entities
 *
 * A chart:
 * ```json
 * {
 *   "id": "01H...",
 *   "version": "01H...",
 *   "components": {
 *     "chart": { "name": "Hard" }
 *   }
 * }
 * ```
 *
 * A note on that chart:
 * ```json
 * {
 *   "id": "01H...",
 *   "version": "01H...",
 *   "components": {
 *     "event": { "y": 240 },
 *     "chartRef": { "chartId": "01H..." },
 *     "note": { "lane": 0 }
 *   }
 * }
 * ```
 *
 * A sound channel definition (untimed):
 * ```json
 * {
 *   "id": "01H...",
 *   "version": "01H...",
 *   "components": {
 *     "soundChannel": { "name": "Kick", "path": "audio/kick.wav" }
 *   }
 * }
 * ```
 *
 * ## Merge Rule
 *
 * Entities are merged by `id`. If the same `id` exists in two versions,
 * the entity with the lexicographically higher `version` wins.
 * Deletion strips all components and bumps the `version`.
 */
 
import { Type } from "typebox";
import { EntitySchema } from "../entity-manager";
 
// ---------------------------------------------------------------------------
// Project Metadata & Root Schema
// ---------------------------------------------------------------------------
 
/**
 * Project-level metadata describing the song.
 */
export const ProjectMetadataSchema = Type.Object(
  {
    title: Type.String({
      description: "Song title.",
    }),
    artist: Type.String({
      description: "Artist or creator name.",
    }),
    genre: Type.String({
      description: "Music genre classification.",
    }),
  },
  {
    additionalProperties: true,
    description: "Additional project-level properties.",
  },
);
 
/**
 * The root structure of a Beat Muser project file (`beat-muser-project.json`).
 *
 * Structure:
 * ```json
 * {
 *   "$schema": "https://beat-muser.pages.dev/schemas/beat-muser-project.schema.json",
 *   "schemaVersion": 2,
 *   "version": "01H...",
 *   "metadata": {
 *     "title": "Song Name",
 *     "artist": "Artist",
 *     "genre": "Genre"
 *   },
 *   "entities": [
 *     {
 *       "id": "01H...",
 *       "version": "01H...",
 *       "components": {
 *         "chart": { "name": "Hard" }
 *       }
 *     }
 *   ]
 * }
 * ```
 *
 * Key details:
 * - `schemaVersion`: Number for schema migration support. Current version: 2.
 * - `version`: UUIDv7 project-level revision timestamp.
 * - `metadata`: Song-level metadata (title, artist, genre).
 * - `entities`: Flat array of all project entities. Deleted entities remain
 *   in this array with empty `components` and a bumped `version`.
 * - All objects allow additional properties for plugin extensibility.
 * - Asset paths use `/` as separator and are relative to the project directory.
 * - Default BPM is 60. PPQN is 240 (bmson standard).
 *
 * ## Merge Rule
 * Merge two project versions by unioning `id`s:
 * - Entities: higher `version` wins.
 * - An entity with empty `components` represents a deletion.
 * - No special cases — deletion is just another write.
 */
export const ProjectFileSchema = Type.Object(
  {
    $schema: Type.Optional(
      Type.String({
        description:
          "URL to the JSON Schema for this format. Enables IDE validation and autocomplete.",
      }),
    ),
    schemaVersion: Type.Number({
      description: "Schema version number for migration support. Current version: 2.",
    }),
    version: Type.String({
      description: "UUIDv7 project-level revision timestamp.",
    }),
    metadata: ProjectMetadataSchema,
    entities: Type.Array(EntitySchema, {
      description: "Flat array of all project entities.",
    }),
  },
  {
    additionalProperties: true,
    description: "Additional top-level properties.",
  },
);