@memizy/oqse 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2444 @@
1
+ # OQSE v0.1 Specification (Open Quiz & Study Exchange)
2
+
3
+ **OQSE** (Open Quiz & Study Exchange) is an open, JSON-based format designed for easy creation, sharing, and importing of study sets. It is designed with emphasis on flexibility, extensibility, semantic interoperability, legal clarity, and backward compatibility.
4
+
5
+ ### Keywords per RFC 2119
6
+
7
+ In this specification, the keywords **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** are interpreted according to [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119):
8
+
9
+ - **MUST** / **REQUIRED**: An absolute requirement of the specification.
10
+ - **MUST NOT**: An absolute prohibition.
11
+ - **SHOULD** / **RECOMMENDED**: There may be reasons to ignore, but consequences must be understood.
12
+ - **SHOULD NOT**: There may be reasons to accept, but consequences must be understood.
13
+ - **MAY** / **OPTIONAL**: Truly optional.
14
+
15
+ **Note on Tables:** In columns labeled "Required," "Yes" = MUST be present (per RFC 2119), "No" = MAY be omitted.
16
+
17
+ **Implementation Terminology:**
18
+ - **Application:** Any software entity that interacts with OQSE data. Every application declares its specific **Capabilities** via a Manifest.
19
+ - **Capability:** A specifically declared competence of an application. This encompasses the actions it can perform (e.g., `render`, `edit`, `export`), the item types and asset formats it supports (e.g., `mcq-single`, `image/png`), and the advanced features (e.g., `explanations`, `sourceMaterials`) or metadata it can process (e.g., `markdown`, `math`).
20
+ - **Rule Application:** When the specification states "Application MUST...", the rule applies contextually based on the application's declared capabilities. For example, rules regarding UI display and tolerant parsing apply to applications declaring the `render` action, whereas strict data normalization rules during saving apply to those declaring `edit` or `export`.
21
+
22
+
23
+ ## Table of Contents
24
+
25
+ * [1. Global Rules and Root Structure](#1-global-rules-and-root-structure)
26
+ * [2. Capability-Based Interoperability, Application Manifest & Progress Data](#2-capability-based-interoperability-application-manifest--progress-data)
27
+ * [3. The `meta` Object](#3-the-meta-object)
28
+ * [4. Common Item Properties](#4-common-item-properties-in-items)
29
+ * [5. The `MediaObject` Object](#5-the-mediaobject-object)
30
+ * [6. Item Types](#6-item-types-itemtype)
31
+ * [7. Helper Data Structures](#7-helper-data-structures)
32
+ * [8. Complete Example File](#8-complete-example-file)
33
+ * [9. Validation Rules and Constraints](#9-validation-rules-and-constraints)
34
+ * [10. Error Handling](#10-error-handling-error-handling-policy)
35
+ * [11. Extensibility and Versioning](#11-extensibility-and-versioning)
36
+ * [12. Best Practices](#12-best-practices-for-set-creators)
37
+ * [13. Validation and Tools](#13-validation-and-tools)
38
+ * [14. Contributing and Community](#14-contributing-and-community)
39
+
40
+ ## 1. Global Rules and Root Structure
41
+
42
+ * **File Format:** JSON (`.json`)
43
+ * **MIME Types:**
44
+ * Standalone JSON file: `application/vnd.oqse+json`
45
+ * OQSE container (ZIP): `application/vnd.oqse+zip`
46
+ * For HTTP `Content-Type` header, use the appropriate MIME type
47
+ * **Text Encoding:** UTF-8 (REQUIRED, without BOM)
48
+ * **Validation:** Every OQSE file SHOULD be valid according to the JSON Schema at the URI specified in `$schema`
49
+ * **Unique IDs:** All `id` values MUST be generated as **UUIDv7** to ensure global uniqueness and native temporal ordering of records in databases. Applications MUST accept UUID versions 4 and 7 when loading existing sets. Applications MAY accept other versions (1-6), but this is not required. Applications creating new items MUST use UUIDv7. When re-exporting an existing set, the original UUID MUST be preserved regardless of version. Validation checks only the format (8-4-4-4-12 hex characters), not the specific UUID version. Exception: `SourceMaterial.id` MAY be an alphanumeric string unique only within that file, and `id` within internal item objects such as `TimelineEvent` or `CategorizeItem` MAY also be an alphanumeric string.
50
+ * **Text Formatting:** The specification distinguishes two types of text fields:
51
+ * **Plain Text:** Fields intended for metadata and identifiers (e.g., titles, tags, alt texts). Interpreted as plain text without any formatting.
52
+ * **Rich Content:** Fields intended for educational content. Support **GitHub Flavored Markdown (GFM)**, **LaTeX** mathematics compatible with KaTeX/MathJax (inline `$x^2$` and blocks `$$...$$` rendered as display style on a separate line), and media references using Media Tag syntax `<asset:key />`.
53
+ * **Media Embedding System (Assets):** The format uses a centralized "assets" system for embedding media in Rich Content fields. Media are defined in `item.assets` and referenced using the special tag `<asset:key />`. **[(See section 5.1 for details)](#51-media-reference-syntax-media-tags)**.
54
+ * **Backward Compatibility:** Applications MUST ignore unknown keys (forward compatibility). New specification versions MUST NOT change the meaning of existing keys within the same MAJOR version.
55
+ * **Case Sensitivity of Files:** All keys in `assets` and filenames within the OQSE container (ZIP) MUST be written in lowercase. Application MUST immediately convert all filenames and asset keys to lowercase during import (with a warning on change) and MUST reject colliding names that match after conversion. On export, only lowercase names may be generated. References `<asset:... />` are case-insensitive; applications MUST convert the token to lowercase before lookup to maintain consistency across platforms.
56
+
57
+ **Note on Field Types:** Throughout the specification, each text field is explicitly marked as either "Plain Text" or "Rich Content."
58
+
59
+ ### 1.1 Distribution Formats
60
+
61
+ An OQSE set MAY be distributed in two ways:
62
+
63
+ 1. **Standalone JSON file (`.oqse.json` or `.json`)**
64
+ * Suitable for sets whose media are hosted on public URLs or use no media.
65
+ * For standalone JSON files, `MediaObject.value` MUST contain an absolute URL [(see section 9.4)](#94-uri-and-path-security-validation-rules).
66
+ * RECOMMENDED MIME type for HTTP transmission: `application/json` (custom APIs MAY use `application/vnd.oqse+json` for strict content negotiation).
67
+
68
+ 2. **OQSE container (`.oqse`)**
69
+ * Technically a ZIP archive with MIME type `application/vnd.oqse+zip`.
70
+ * **OQSE container structure:**
71
+ ```
72
+ archive.oqse
73
+ ├── index.json (REQUIRED)
74
+ ├── assets/ (OPTIONAL)
75
+ │ ├── images/
76
+ │ ├── audio/
77
+ │ └── ...
78
+ └── [other files] (IGNORED)
79
+ ```
80
+ * `index.json` references local media using relative paths (e.g., `assets/audio/countdown.mp3`). Relative paths MUST NOT contain `..` or start with `/`.
81
+ * Hierarchy within `assets/` is flexible, but all files MUST be within this folder (e.g., `assets/images/diagram.png`).
82
+ * ZIP MUST NOT contain executable files or scripts. It is recommended to apply SHA-256 `checksum` for each asset [(see section 5)](#5-the-mediaobject-object).
83
+ * **Application MUST:**
84
+ * Load `index.json` from the root
85
+ * Load files from `assets/` according to JSON references
86
+ * IGNORE all other files (e.g., OS metadata, `.DS_Store`, thumbnails)
87
+ * MUST NOT fail due to the presence of other files
88
+
89
+ Exporters should choose `.json` if the set does not need local binary files. Once a set contains custom media (e.g., user-uploaded), the editor MUST create a `.oqse` package [(see section 2.3)](#23-the-handshake-matching-process).
90
+
91
+ -----
92
+
93
+ ### 1.2 Root Structure
94
+
95
+ The root object of an OQSE file consists of 4 keys:
96
+
97
+ ```json
98
+ {
99
+ "$schema": "https://memizy.com/schemas/oqse/v0.1.json",
100
+ "version": "0.1",
101
+ "meta": { ... },
102
+ "items": [ ... ]
103
+ }
104
+ ```
105
+
106
+ * `$schema` (string): **Recommended.** URL reference to the JSON Schema specification for automatic validation.
107
+ * `version` (string): **Required.** Version of the OQSE specification (e.g., "1.0").
108
+ * `meta` (object): **Required.** Object containing metadata about the entire set. [See section 3](#3-the-meta-object).
109
+ * `items` (array): **Required.** Array containing individual study items. May be empty (e.g., for a template or work-in-progress set). [See section 4](#4-common-item-properties-in-items).
110
+
111
+ -----
112
+
113
+ ## 2. Capability-Based Interoperability, Application Manifest & Progress Data
114
+
115
+ OQSE v0.1 uses a **capability-based negotiation system**. Every application, micro-frontend, or plugin MUST declare exactly what it can do using an **Application Manifest**. Interoperability is achieved by matching the **Requirements** of a study set with the **Capabilities** of the application.
116
+
117
+ ### 2.1. The Application Manifest
118
+
119
+ Applications MUST declare their capabilities in a standardized JSON format. This allows host environments to automatically determine if an application or plugin can safely process a given study set.
120
+
121
+ **Delivery:** Applications delivered via HTML SHOULD embed their manifest using a `<script type="application/oqse-manifest+json">` tag. This pattern is known as an **HTML Data Island**: the browser ignores the script block entirely (it does not execute it), but the host environment can locate and parse it cheaply via a DOM query (`document.querySelector('script[type="application/oqse-manifest+json"]')`), without requiring a separate network request. Applications that are not HTML-based (e.g., server-side importers, CLI tools) SHOULD expose their manifest as a static `oqse-manifest.json` file at the root of their deployment, allowing host environments to fetch it without rendering the application. When served over HTTP, this file SHOULD be delivered with the standard `Content-Type: application/json` MIME type; this avoids the need for developers to configure custom MIME types on their static file servers. Host environments MAY also support manifest discovery via an HTTP `Link` response header: `Link: <oqse-manifest.json>; rel="oqse-manifest"`.
122
+
123
+ **Manifest Structure Example:**
124
+
125
+ ```json
126
+ {
127
+ "$schema": "https://memizy.com/schemas/oqse-manifest/v0.1.json",
128
+ "version": "0.1",
129
+ "pluginVersion": "2.1.0",
130
+ "minOqseVersion": "1.0",
131
+ "maxOqseVersion": "1.99",
132
+ "id": "https://memizy.com/universal-player",
133
+ "appName": "Memizy Universal Player",
134
+ "description": "A universal flashcard and quiz player with spaced repetition.",
135
+ "author": "Memizy Team",
136
+ "authorUrl": "https://memizy.com",
137
+ "locales": ["en", "cs"],
138
+ "tags": ["flashcards", "spaced-repetition", "quiz"],
139
+ "emoji": "🧠",
140
+ "studyMode": "fun",
141
+ "questionDensity": "medium",
142
+ "capabilities": {
143
+ "actions": [
144
+ "render",
145
+ "edit"
146
+ ],
147
+ "types": [
148
+ "flashcard",
149
+ "mcq-single",
150
+ "match-pairs"
151
+ ],
152
+ "assets": {
153
+ "image": ["image/jpeg", "image/png", "image/svg+xml", "image/webp"],
154
+ "audio": ["audio/mpeg", "audio/ogg"],
155
+ "video": ["video/mp4"],
156
+ "model": null
157
+ },
158
+ "features": [
159
+ "math",
160
+ "markdown",
161
+ "text-to-speech"
162
+ ],
163
+ "latexPackages": [
164
+ "mhchem",
165
+ "amsmath"
166
+ ],
167
+ "itemProperties": [
168
+ "hints",
169
+ "explanation",
170
+ "sources"
171
+ ],
172
+ "metaProperties": [
173
+ "license",
174
+ "contributors"
175
+ ]
176
+ }
177
+ }
178
+ ```
179
+
180
+ #### 2.1.1 Manifest Root Object
181
+
182
+ | Key | Type | Required | Description |
183
+ | :--- | :--- | :--- | :--- |
184
+ | `$schema` | string | No | **Recommended.** URL reference to the JSON Schema specification for automatic validation. |
185
+ | `version` | string | Yes | Version of the Application Manifest format. MUST follow `"MAJOR.MINOR"` format (e.g., `"1.0"`). The current manifest format version is `"1.0"`. |
186
+ | `pluginVersion` | string | No | Version of the application/plugin itself using SemVer (e.g., `"2.1.0"`). Independent of the manifest format version. |
187
+ | `minOqseVersion` | string | No | Minimum OQSE spec version this application requires. MUST follow `"MAJOR.MINOR"` format (e.g., `"1.0"`). Version comparison is performed numerically field-by-field. Host environments MAY use this for compatibility filtering. |
188
+ | `maxOqseVersion` | string | No | Maximum OQSE spec version this application is compatible with. MUST follow `"MAJOR.MINOR"` format (e.g., `"1.99"`). Version comparison is performed numerically field-by-field. Host environments MAY use this to prevent loading a plugin with a newer, potentially incompatible OQSE version. If absent, no upper bound is assumed. |
189
+ | `id` | string | Yes | Unique identifier for the application. MUST be either a controlled URL (preferred — MAY lead to the official page of the plugin, e.g., `"https://memizy.com/player"`) or a URN-format UUID (e.g., `"urn:uuid:019cba2f-7ea1-7607-8f16-be10714d790e"`). |
190
+ | `appName` | string | Yes | Human-readable name of the application. |
191
+ | `description` | string | No | **Plain Text.** Human-readable description of what the application does. Displayed in host environments and plugin catalogs. |
192
+ | `author` | string | No | **Plain Text.** Name of the author or organization. |
193
+ | `authorUrl` | string | No | **URI (absolute URL).** URL of the author or organization. |
194
+ | `locales` | string[] | No | BCP 47 locale codes for the application's supported UI languages (e.g., `["en", "cs"]`). Host environments MAY use this when choosing a plugin for a specific user locale. If absent or `[]`, the host SHOULD assume the application supports English. |
195
+ | `tags` | string[] | No | Array of plain text keywords describing the application's purpose or focus area (e.g., `["flashcards", "spaced-repetition", "chess"]`). Used by host environments and catalogs for filtering and discovery. Uses the same convention as `meta.tags` in study sets. |
196
+ | `emoji` | string | No | A single emoji character used as the application's visual identity in catalogs and host UIs where loading a full icon image is impractical (e.g., `"🧠"`, `"🚀"`, `"🎮"`). SHOULD contain a single visually-rendered emoji glyph (note: some emoji such as flags or family sequences are composed of multiple Unicode code points — this is acceptable). If a future `iconUrl` field is also declared in an extension, hosts SHOULD prefer that over `emoji`. |
197
+ | `studyMode` | string | No | Enum indicating the application's primary experience type. MUST be one of: `"game"` (entertainment-first; gameplay mechanics take priority over traditional study), `"fun"` (balanced blend of interactivity and learning), `"drill"` (maximally focused, distraction-free repetition). Helps host environments and users choose an appropriate plugin for their goals. If absent, no assumption is made. See `questionDensity` for a complementary signal about quiz frequency within the experience. |
198
+ | `questionDensity` | string | No | Enum describing the ratio of quiz items relative to non-question game elements. MUST be one of: `"low"` (questions appear rarely, gameplay dominates), `"medium"` (questions and gameplay are roughly balanced), `"high"` (nearly every interaction is a question). Primarily meaningful for plugins with `studyMode: "game"` or `"fun"`. If the plugin declares `studyMode: "drill"`, host environments SHOULD treat `questionDensity` as `"high"` regardless of the declared value. If absent, no assumption is made. See `studyMode` for the primary classification. |
199
+ | `appSpecific` | object | No | Object for application-specific metadata not covered by the standard fields. Top-level keys in this object MUST be namespaced by the application identifier (e.g., `"memizy"`) to prevent collisions between applications (e.g., `{ "memizy": { "editorState": {...} } }`). Applications declaring custom `x-` features SHOULD document their semantics here so that host environments can surface that information. Other applications MUST ignore this object. |
200
+ | `capabilities` | object | Yes | Object defining application's capabilities. Extends **FeatureProfile** (adds `types` and `assets`). |
201
+
202
+ #### 2.1.2 Capabilities Object (extends FeatureProfile)
203
+
204
+ | Key | Type | Required | Description |
205
+ | :--- | :--- | :--- | :--- |
206
+ | `actions` | string[] | Yes | Array of actions the application can perform on supported types (e.g., `["render", "edit"]`). MUST contain at least one action from the [Action Registry](#213-action-registry). Custom actions MAY be declared using the `x-` prefix (e.g., `"x-preview"`). |
207
+ | `types` | string[] | Conditional | Array of OQSE item types the application supports (e.g., `["flashcard", "mcq-single"]`). Use `["*"]` to declare support for all item types. **Required** (at least one type or `["*"]`) when `actions` includes `render` or `edit`; MAY be omitted for `validate`, `import`, or `export`-only applications (though declaring `["*"]` is recommended). An empty array `[]` MUST be treated as `null` (no types declared); such a manifest would fail the `render`/`edit` rule above. See [Wildcard and Null Semantics](#wildcard-and-null-semantics). |
208
+ | `assets` | object | No | Map of asset categories (`image`, `audio`, `video`, `model`) to arrays of supported MIME types. If `assets` is omitted entirely, the application declares no media support (equivalent to all categories being `null`). See [Wildcard and Null Semantics](#wildcard-and-null-semantics). |
209
+ | `features` | string[] | No | **(Inherited)** Array of supported features from the **Official Feature Registry** and/or custom `x-` prefixed extensions. See [Extension Rules](#extension-rules). |
210
+ | `latexPackages` | string[] | No | **(Inherited)** Array of supported LaTeX packages. |
211
+ | `itemProperties` | string[] | No | **(Inherited)** Array of supported item properties. |
212
+ | `metaProperties` | string[] | No | **(Inherited)** Array of supported metadata properties. |
213
+
214
+ #### Wildcard and Null Semantics
215
+
216
+ The `types` array and each value in the `assets` map follow a unified convention:
217
+
218
+ | Value | Meaning |
219
+ | :--- | :--- |
220
+ | `["*"]` | **All accepted.** Supports all item types (for `types`) or any MIME type within that asset category (for `assets`). Useful for validators, export tools, or applications designed to handle any content. |
221
+ | `["image/jpeg", ...]` | **Explicit list.** Only the enumerated values are supported. |
222
+ | `null` or key absent | **Not supported.** The application does not support this category at all. Hosts MUST NOT pass such content to the application. |
223
+ | `[]` | **Equivalent to `null`.** An empty array carries the same meaning as `null` or key omission — nothing is supported in that category. Applications SHOULD NOT emit an empty array (prefer `null` or key omission for clarity), but parsers MUST treat it identically to `null`. |
224
+
225
+ #### 2.1.3 Action Registry
226
+
227
+ The `actions` array in the `capabilities` object defines the functional roles an application performs on supported item `types`. The interpretation of `itemProperties` is directly tied to these declared actions.
228
+
229
+ | Action | Description |
230
+ | :--- | :--- |
231
+ | **`render`** | **Learning/Player Mode:** The application can display the item in a fully interactive study mode. It MUST handle user input, feedback cycles, and all other behaviors declared as supported in `itemProperties` and `metaProperties`. |
232
+ | **`edit`** | **Authoring Mode:** The application provides a user interface for creating or modifying item content. An editor SHOULD provide a faithful visual preview of the data (rendering Markdown and media) so the author sees the final result, but it is not required to implement interactive scoring or study logic. |
233
+ | **`validate`** | **Verification Mode:** The application checks the structural, technical, and logical correctness of the item without necessarily providing a user interface (e.g., CI/CD linters). |
234
+ | **`import`** | **Data Ingestion:** The application can parse OQSE data and transform it into an internal system or database. |
235
+ | **`export`** | **Data Generation:** The application can serialize internal data into a valid OQSE format (including normalization of lowercase asset filenames). |
236
+
237
+ **Custom actions:** Applications MAY define proprietary action roles using the `x-` prefix (e.g., `"x-preview"`, `"x-sync"`). Custom actions follow the same `x-` prefix convention as custom features (see [Extension Rules](#extension-rules)). Host environments that do not recognize a custom action MUST NOT fail manifest loading solely for that reason; they SHOULD ignore the unknown action.
238
+
239
+ **Note on UI Expectations:**
240
+ An application declaring the `edit` action MUST NOT be a mere raw JSON text editor; to comply with the standard's intent, it SHOULD offer the author visual context. If an application declares only `edit` (without `render`), it serves as a content creation tool while leaving the actual study process (answering and evaluation) to applications with `render` declared.
241
+
242
+ **Contextual Property Application:**
243
+ Properties defined in `itemProperties` and `metaProperties` are interpreted context-aware based on the active action, for example:
244
+ * **`hints`**: In `render` mode, these are displayed as interactive prompts; in `edit` mode, they appear as input fields for the author.
245
+ * **`topic`**: In `render` mode, this is used for filtering or as a chapter heading; in `edit` mode, it is used to categorize the item within the set structure and can be changed.
246
+ * **`explanation`**: In `render` mode, it is revealed after the user answers; in `edit` mode, it can be changed within a Rich Content editor.
247
+
248
+ ### 2.2. Feature Profile & Official Registry
249
+
250
+ To avoid redundancy, OQSE defines a base object called **FeatureProfile**. This structure is used in both the Application Manifest (in `capabilities`) and the Study Set (in `meta.requirements`).
251
+
252
+ #### FeatureProfile Structure
253
+
254
+ | Key | Type | Description |
255
+ | :--- | :--- | :--- |
256
+ | `features` | string[] | Array of feature flags. MUST contain only values from the **Official Feature Registry** or custom `x-` prefixed values (e.g., `"x-my-feature"`). See [Extension Rules](#extension-rules). |
257
+ | `latexPackages` | string[] | Array of LaTeX packages (e.g., `["mhchem"]`). Relevant only if `features` includes `"math"`. |
258
+ | `itemProperties` | string[] | Array of item properties supported/required. [See **Registry of Properties**](#registry-of-properties-itemproperties--metaproperties). |
259
+ | `metaProperties` | string[] | Array of metadata properties supported/required. [See **Registry of Properties**](#registry-of-properties-itemproperties--metaproperties). |
260
+
261
+ #### Official Feature Registry
262
+
263
+ The `features` array MUST contain only values from this registry:
264
+
265
+ | Feature Key | Category | Description |
266
+ | :--- | :--- | :--- |
267
+ | `markdown` | Formatting | Application MUST support parsing **GitHub Flavored Markdown (GFM)** syntax in Rich Content fields. This ensures support for basic formatting, tables, strikethrough, and fenced code blocks (` ``` `). GFM natively parses fenced blocks into unstyled `<pre><code>` structures. For colored keywords, the `syntax-highlighting` feature MUST also be declared. This flag requires support for Markdown syntax only. If the set relies on raw HTML embedded within the Markdown, `html-safe` MUST also be declared. |
268
+ | `html-safe` | Formatting | Application allows and safely renders intentional raw HTML tags (e.g., `<span style="color:red">`, `<ruby>`) embedded in text fields. Applications MUST perform strict sanitization [See section 12.4.](#124-security). |
269
+ | `math` | Formatting | Application MUST detect `$` and `$$` delimiters in text and render them as LaTeX mathematical formulas. |
270
+ | `ruby-annotations` | Typography | Application MUST render standard HTML5 `<ruby>`, `<rt>`, and `<rp>` tags for phonetic guides (Furigana/Pinyin). Implicitly requires handling these tags even if `html-safe` is off. Application SHOULD style `<rt>` with smaller font size above base text. |
271
+ | `rtl` | Typography | Application MUST support Right-to-Left text direction and alignment for languages like Arabic or Hebrew. |
272
+ | `syntax-highlighting`| Blocks | Application applies syntax coloring (highlighting) to the structural code blocks generated by the GFM parser (e.g., coloring Java keywords in ` ```java `). **Requires `markdown`:** only meaningful when the GFM parser is active. Applications SHOULD NOT declare this feature without also declaring `markdown`. |
273
+ | `mermaid` | Blocks | Application can render Mermaid diagrams from code blocks (via ` ```mermaid `). **Requires `markdown`:** only meaningful when the GFM parser is active. Applications SHOULD NOT declare this feature without also declaring `markdown`. |
274
+ | `smiles` | Blocks | Application can render 2D chemical structures from SMILES strings in code blocks (via ` ```smiles `). **Requires `markdown`:** only meaningful when the GFM parser is active. Applications SHOULD NOT declare this feature without also declaring `markdown`. |
275
+ | `abc-notation` | Blocks | Application can render music notation from ABC syntax in code blocks (via ` ```abc `). **Requires `markdown`:** only meaningful when the GFM parser is active. Applications SHOULD NOT declare this feature without also declaring `markdown`. |
276
+ | `text-to-speech` | Interaction | Application supports reading text aloud (e.g., interpreting hidden pronunciation hints). |
277
+ | `voice-input` | Interaction | Application allows input via voice dictation. |
278
+ | `handwriting-recognition` | Interaction | Application allows input via handwriting (useful for math/languages on tablets). |
279
+
280
+ #### Registry of Properties (itemProperties & metaProperties)
281
+
282
+ Values in `itemProperties` and `metaProperties` indicate support for specific fields or complex behaviors.
283
+
284
+ **itemProperties (Item Level Support):**
285
+
286
+ | Property Key | Description |
287
+ | :--- | :--- |
288
+ | `hints` | Application displays optional hints to the user. |
289
+ | `explanation` | Application displays explanation after answering. |
290
+ | `incorrectFeedback` | Application displays specific feedback for incorrect answers. |
291
+ | `sources` | Application can display references to source materials for each item. |
292
+ | `relatedItems` | Application can navigate to or display explicitly related items. |
293
+ | `dependencyItems` | Application respects logical dependencies between items (prerequisites), declared via the `dependencyItems` field. |
294
+ | `timeLimit` | Application enforces or visibly displays time limits for items. |
295
+ | `lang` | Application respects per-item language overrides (e.g., for TTS or font rendering). |
296
+ | `topic` | Application supports grouping, filtering, or displaying items by their primary `topic`. |
297
+ | `pedagogy` | Application utilizes advanced pedagogical data (IRT, forgetting curves). |
298
+
299
+ **metaProperties (Set Level Support):**
300
+
301
+ | Property Key | Description |
302
+ | :--- | :--- |
303
+ | `description` | Application displays the set description to the user (e.g., on a detail or preview screen). |
304
+ | `thumbnail` | Application displays the set cover image as a visual identifier in lists or cards. |
305
+ | `subject` | Application uses the `subject` field for categorization, browsing, or search filtering. |
306
+ | `language` | Application respects the declared set language (e.g., for TTS engine selection, font rendering, or UI locale hints). |
307
+ | `author` | Application attributes the primary author of the set (e.g., in a detail view or export header). |
308
+ | `contributors` | Application attributes all listed contributors of the set. |
309
+ | `license` | Application displays licensing information and respects usage rights. |
310
+ | `ageRestriction` | Application respects `ageMin` and `ageMax` recommendations (e.g., hides or warns for age-inappropriate content). |
311
+ | `tags` | Application supports browsing, filtering, or searching by set-level tags. |
312
+ | `tagDefinitions` | Application uses tag definitions to display semantic descriptions or Wikidata-linked tooltips. |
313
+ | `sourceMaterials` | Application displays the range of sources used in the set. |
314
+ | `estimatedTime` | Application displays or uses the estimated completion time (e.g., progress indicators, scheduling). |
315
+ | `prerequisites` | Application respects prerequisite sets and warns or prevents access if they have not been completed. |
316
+ | `translations` | Application can suggest or navigate to translated versions of the set. |
317
+ | `relatedSets` | Application can suggest related study sets (e.g., in a "Study next" recommendation). |
318
+
319
+ > **Note on Declaration Semantics:** The `itemProperties` and `metaProperties` arrays do **not** enumerate all fields that a set *happens to contain*. They are an **intentional signal** from the set creator that specific behaviors are meaningful and expected. For example: a set creator who wants attribution explicitly displayed declares `"author"` in `metaProperties`; a set targeting children where age enforcement is important declares `"ageRestriction"`. An application that does not support a declared property MUST still preserve the data when saving, but is not required to actively implement it.
320
+
321
+ #### Parsing Rules for Blocks
322
+ For features `syntax-highlighting`, `mermaid`, `smiles`, and `abc-notation`, content MUST be written using standard Markdown Fenced Code Blocks with appropriate language identifiers (e.g., ` ```java `, ` ```mermaid `, ` ```smiles `, ` ```abc `). Applications that do not support a specific feature MUST gracefully degrade by rendering the content as a plain text code block.
323
+
324
+ #### Extension Rules
325
+
326
+ To prevent fragmentation, the following rules apply to the `features` array:
327
+ 1. **Official values only** (without prefix): Developers MUST NOT invent new unprefixed string values. Only values from the **Official Feature Registry** above are valid without a prefix.
328
+ 2. **Custom features with `x-` prefix:** Developers MAY define proprietary feature flags using the `x-` prefix (e.g., `x-memizy-3d-voxel`, `x-spaced-repetition`). These values MUST NOT collide with future official registry entries and SHOULD be documented in the Application Manifest's own `appSpecific` field.
329
+ 3. **Proprietary Behavior (no feature contract):** Application-specific configurations or internal UI behaviors that do not need to be negotiated via the handshake MUST be placed inside the `appSpecific` object instead.
330
+ 4. **New Data Structures:** If a completely new data structure is needed, a new `item.type` MUST be defined (following the same `x-` prefix convention per [section 11.1](#111-adding-custom-item-types)).
331
+
332
+ ### 2.3. The Handshake (Matching Process)
333
+
334
+ Before an application attempts to process a set, it MUST perform a compatibility check:
335
+
336
+ 1. **Item Type Check:** If the set contains an item of type `X`, the application MUST have `X` in its `types` array, or declare `["*"]` as `types`. This check applies equally to `x-` prefixed custom types.
337
+ 2. **Asset Check:** For each asset used in the set, the application MUST either list its MIME type explicitly in the corresponding `capabilities.assets` category (e.g., `"image/webp"` in `capabilities.assets.image`) or declare `["*"]` for that asset category. If `capabilities.assets` is absent, or the relevant category key is `null` or `[]`, that asset category is not supported.
338
+ 3. **OQSE Version Check:** If the application declares `minOqseVersion`, the host MUST verify that the OQSE file's `version` is greater than or equal to it. If `maxOqseVersion` is declared, the host MUST verify that the file's `version` does not exceed it. A host MUST NOT load the set into the application if the version check fails; it MUST inform the user of the incompatibility.
339
+ 4. **Feature Profile Check:** If `meta.requirements` is present, the application MUST compare it with `capabilities`:
340
+ * **Features:** Application MUST support all features listed in `meta.requirements.features`.
341
+ * **LaTeX Packages:** Application MUST support all packages listed in `meta.requirements.latexPackages`.
342
+ * **Properties:** Application SHOULD support all properties listed in `meta.requirements.itemProperties` and `meta.requirements.metaProperties`.
343
+
344
+ ### 2.4. Graceful Degradation
345
+
346
+ If an application lacks a required capability:
347
+ * **Missing `types`:** The application MUST NOT crash. It MUST either ignore the unsupported items (allowing a partial study session), display a generic fallback message, or allow their raw editing as JSON to prevent data loss.
348
+ * **Missing `features` or `latexPackages`:** The application SHOULD warn the user.
349
+ * **Missing `itemProperties` or `metaProperties`:** The application MAY ignore these fields but MUST preserve them if editing/saving the set.
350
+ * **Missing `assets` capability:** The application declares no support for one or more asset categories (either `capabilities.assets` is absent, or the specific category is `null` or `[]`). The application SHOULD display a placeholder for the unsupported media and MUST NOT crash when encountering asset references for those categories.
351
+ * **Missing `x-` prefixed feature:** Treated identically to a missing official feature — the application SHOULD warn the user. Unlike official features, the application MAY additionally display a message identifying the specific proprietary extension that is not supported.
352
+ * **OQSE Version Mismatch:** If the set's `version` falls outside the application's declared `minOqseVersion`–`maxOqseVersion` range, the host MUST NOT load the set into the application. The host MUST display a clear error message indicating the version incompatibility and SHOULD suggest an alternative compatible application if one is available in the catalog.
353
+
354
+ -----
355
+
356
+ ### 2.5. User Progress Portability (OQSEP)
357
+
358
+ Applications that implement spaced repetition or other adaptive learning algorithms accumulate significant per-user, per-item learning data over time. Without a standard for this data, users are entirely dependent on a single platform — a form of **vendor lock-in** that makes migrating to a better tool, or continuing study on a different device, practically impossible.
359
+
360
+ **OQSEP** (Open Quiz & Study Exchange — Progress) is a lightweight, companion JSON format that standardizes the representation and export of this learning progress. An OQSEP file is always associated with a specific OQSE study set (identified by `meta.setId`) and contains one progress record per item in that set.
361
+
362
+ #### Rationale & Design Goals
363
+
364
+ * **Portability over precision.** Progress data MUST be exportable and importable across applications using different spaced repetition algorithms (e.g., Leitner, SM-2, FSRS). No single algorithm's internal state is mandated.
365
+ * **Raw statistics as a universal bridge.** Complex algorithms such as FSRS require parameters (stability, difficulty, retrievability) that are meaningless outside their own mathematical model. OQSEP solves this by always storing the **raw outcome statistics** (`attempts`, `incorrect`, `streak`) which any algorithm can use as a **Cold Start** seed when importing progress from a foreign system.
366
+ * **Lightweight by design.** Simple applications and plugins MAY rely solely on `bucket` and `nextReviewAt` without ever parsing `appSpecific` data. This allows a plugin with a minimal implementation to still consume OQSEP files meaningfully.
367
+ * **No answer content stored.** OQSEP deliberately does not record the actual text of a user's answers. Only the outcome (`isCorrect`) and optional confidence (`confidence`) are stored. This keeps the progress file lean and avoids reproducing the set content redundantly.
368
+
369
+ #### Distribution Format
370
+
371
+ OQSEP files MUST be valid UTF-8 encoded JSON. The RECOMMENDED file extension is `.oqsep.json` (plain `.json` is also fully permitted; the compound extension improves discoverability in editors and file managers without requiring special tooling). The RECOMMENDED MIME type for HTTP transmission is `application/json` (custom APIs MAY use `application/vnd.oqsep+json` for strict content negotiation). An OQSEP file MUST NOT be embedded inside an OQSE container (`.oqse` / `.json`); it is always a separate, standalone artifact.
372
+
373
+ #### 2.5.1 Data Model
374
+
375
+ An OQSEP document is a single JSON object with the following structure.
376
+
377
+ ##### Root Object
378
+
379
+ | Key | Type | Required | Description |
380
+ | :--- | :--- | :--- | :--- |
381
+ | `$schema` | string | No | **Recommended.** URL reference to the JSON Schema for the OQSEP format for automatic validation. |
382
+ | `version` | string | Yes | Version of the OQSEP format. MUST follow `"MAJOR.MINOR"` format (e.g., `"1.0"`). The current format version is `"0.1"`. |
383
+ | `meta` | object | Yes | Metadata block describing the origin of this progress data. See [Progress Meta Object](#progress-meta-object). |
384
+ | `records` | object | Yes | A map of item UUIDs (strings) to their individual [Progress Record](#progress-record) objects. The absence of an item from this map implicitly signals an unseen state (equivalent to bucket 0). An explicit record with `bucket: 0` MAY, however, appear when an application stores pre-study initialization data or algorithm-specific state for an item the user has not yet encountered. |
385
+
386
+ ##### Progress Meta Object
387
+
388
+ | Key | Type | Required | Description |
389
+ | :--- | :--- | :--- | :--- |
390
+ | `setId` | string | Yes | **Plain Text.** The UUID of the OQSE study set this progress file corresponds to. Importing applications MUST verify this matches the set being loaded before applying the progress data. |
391
+ | `exportedAt` | string | Yes | ISO 8601 timestamp (e.g., `"2026-03-04T12:00:00Z"`) indicating when this progress file was generated. |
392
+ | `algorithm` | string | No | Identifier of the spaced repetition algorithm last used to update these records. The value MUST be lowercase and alphanumeric, with hyphens permitted (pattern: `^[a-z0-9\-]+$`). When using a widely recognized algorithm, applications MUST use one of the following official identifiers: `"leitner"`, `"sm2"`, `"sm15"`, `"fsrs"`, `"anki"`. This field is informational only; importing applications MUST NOT refuse to import a file solely because this value differs from their own algorithm. |
393
+
394
+ ##### Progress Record
395
+
396
+ Each entry in the `records` map is keyed by an item UUID and contains the following object. The absence of an item in the `records` map implicitly means it is unseen and has no stored state. However, an explicit record MAY exist even for unseen items if an application needs to store pre-study initialization data or specific `appSpecific` state.
397
+
398
+ | Key | Type | Required | Description |
399
+ | :--- | :--- | :--- | :--- |
400
+ | `bucket` | integer | Yes | Current knowledge level of the item. MUST be an integer in the range **0–4**. See [Bucket Scale](#bucket-scale). Value `0` explicitly denotes an item that is in a *new*, *unseen*, or *reset* state; if `bucket` is `0`, the value of `stats.attempts` SHOULD logically be `0`. Values **1–4** represent increasing levels of knowledge. |
401
+ | `nextReviewAt` | string | No | ISO 8601 timestamp indicating when the item is next due for review. Applications utilizing only a simple scheduler MAY rely solely on this field and `bucket`, ignoring `appSpecific`. |
402
+ | `stats` | object | Yes | Aggregate outcome statistics across all past attempts. See [Stats Object](#stats-object). |
403
+ | `lastAnswer` | object | No | Details of the most recent single answer session. See [Last Answer Object](#last-answer-object). |
404
+ | `appSpecific` | object | No | A namespaced object for storing algorithm-specific internal state that cannot be expressed through the standard fields. Top-level keys MUST be the application identifier (e.g., `"memizy"`), with algorithm-specific data nested beneath (e.g., `{ "memizy": { "fsrs": { "stability": 0.42 } } }`). This namespacing prevents key collisions when multiple applications round-trip the same file. Applications MUST preserve all keys in this object when saving, modifying only their own namespace. |
405
+
406
+ ##### Stats Object
407
+
408
+ | Key | Type | Required | Description |
409
+ | :--- | :--- | :--- | :--- |
410
+ | `attempts` | integer | Yes | Total number of times this item has ever been answered. MUST be ≥ 0. |
411
+ | `incorrect` | integer | Yes | Total count of incorrect answers (lapses) across all attempts. MUST be ≥ 0 and ≤ `attempts`. |
412
+ | `streak` | integer | Yes | Current consecutive correct answers streak. Reset to `0` on any incorrect answer. MUST be ≥ 0. |
413
+
414
+ ##### Last Answer Object
415
+
416
+ | Key | Type | Required | Description |
417
+ | :--- | :--- | :--- | :--- |
418
+ | `isCorrect` | boolean | Yes | Whether the most recent answer was correct. If `isSkipped` is `true`, this field SHOULD be `false` and MUST be ignored by scheduling logic. |
419
+ | `confidence` | integer | No | User's self-assessed confidence rating after the most recent answer. MUST be an integer in the range **1–4** if present. See [Confidence Scale](#confidence-scale). |
420
+ | `answeredAt` | string | Yes | ISO 8601 timestamp of when the answer was submitted. |
421
+ | `timeSpent` | integer | No | Time spent on this specific answer attempt, in milliseconds. MUST be ≥ 0 if present. |
422
+ | `hintsUsed` | integer | No | Number of hints the user revealed during this attempt. MUST be ≥ 0 if present. Defaults to `0` when absent. |
423
+ | `isSkipped` | boolean | No | Whether the user skipped the item without submitting an answer. When `true`, `isCorrect` SHOULD be `false` and MUST be ignored by scheduling logic. |
424
+
425
+ #### 2.5.2 Bucket Scale
426
+
427
+ The `bucket` field encodes the current knowledge level of an item on a **0–4 integer scale**, directly inspired by the Leitner box system. It provides a universal, algorithm-agnostic signal that all importing applications can map to their own scheduling tiers.
428
+
429
+ The absence of an item from the `records` map is the canonical representation of an unseen item. Value `0` acts as an explicit, storable equivalent of that unseen state, provided to allow applications to attach pre-study initialization data (e.g., seeding `appSpecific` parameters before a first session) without implying the user has already encountered the item.
430
+
431
+ | Bucket | Label | Meaning |
432
+ | :---: | :--- | :--- |
433
+ | **0** | *New / Reset* | Item is in an initial or reset state. The user has not meaningfully engaged with it. MAY be stored explicitly when pre-initialization data is needed; otherwise the item is simply absent from `records`. `stats.attempts` SHOULD be `0` when this value is stored. |
434
+ | **1** | *Learning* | Item is newly introduced or was recently answered incorrectly. Requires frequent, short-interval review. |
435
+ | **2** | *Familiar* | Item has been answered correctly at least once but is not yet reliably recalled. Review intervals are moderate. |
436
+ | **3** | *Consolidated* | Item is recalled reliably under normal conditions. Review intervals are longer. |
437
+ | **4** | *Mastered* | Item is recalled effortlessly and consistently. Review intervals are long-term. |
438
+
439
+ #### 2.5.3 Confidence Scale
440
+
441
+ The `confidence` field, when present, captures the user's **subjective assessment of their own recall quality** immediately after answering. It uses a **forced four-point scale (1–4)** with no middle value.
442
+
443
+ The deliberate absence of a middle option prevents the central-tendency bias common in three-point or odd-numbered scales without a forced choice — users statistically gravitate towards a neutral midpoint when one is available, producing data that is too uniform to be useful for adaptive scheduling. A four-point scale forces a genuine lean toward either weak or strong recall.
444
+
445
+ | Rating | Label | Meaning |
446
+ | :---: | :--- | :--- |
447
+ | **1** | *Complete Blackout* | No recall whatsoever. The answer was wrong and even seeing the correct answer did not trigger recognition. |
448
+ | **2** | *Familiar but Forgotten* | The answer was incorrect, but upon seeing the correct answer it felt familiar or obvious in hindsight. Some trace memory exists. |
449
+ | **3** | *Correct with Effort* | The answer was correct, but required noticeable hesitation, conscious effort, or felt uncertain throughout. |
450
+ | **4** | *Effortless Recall* | The answer was correct and came immediately, without hesitation or doubt. |
451
+
452
+ #### 2.5.4 Example
453
+
454
+ A minimal but complete OQSEP document with two item records:
455
+
456
+ ```json
457
+ {
458
+ "$schema": "https://memizy.com/schemas/oqsep/v0.1.json",
459
+ "version": "0.1",
460
+ "meta": {
461
+ "setId": "019cb880-acf3-7bb1-a717-96bb05e220c1",
462
+ "exportedAt": "2026-03-04T14:30:00Z",
463
+ "algorithm": "leitner"
464
+ },
465
+ "records": {
466
+ "019cb880-d607-7944-8630-66b9b432109a": {
467
+ "bucket": 3,
468
+ "nextReviewAt": "2026-03-11T09:00:00Z",
469
+ "stats": {
470
+ "attempts": 8,
471
+ "incorrect": 1,
472
+ "streak": 5
473
+ },
474
+ "lastAnswer": {
475
+ "isCorrect": true,
476
+ "confidence": 4,
477
+ "answeredAt": "2026-03-04T14:28:11Z"
478
+ }
479
+ },
480
+ "019cb880-ee7f-7e91-83ed-44bec565b69d": {
481
+ "bucket": 1,
482
+ "nextReviewAt": "2026-03-05T09:00:00Z",
483
+ "stats": {
484
+ "attempts": 3,
485
+ "incorrect": 2,
486
+ "streak": 0
487
+ },
488
+ "lastAnswer": {
489
+ "isCorrect": false,
490
+ "confidence": 1,
491
+ "answeredAt": "2026-03-04T14:29:05Z"
492
+ },
493
+ "appSpecific": {
494
+ "memizy": {
495
+ "fsrs": {
496
+ "stability": 0.42,
497
+ "difficulty": 7.81,
498
+ "retrievability": 0.61
499
+ }
500
+ }
501
+ }
502
+ }
503
+ }
504
+ }
505
+ ```
506
+
507
+ Note that an item belonging to the set might be entirely absent from `records`. The absence of a record implicitly means the user has never been shown that item (and it defaults to bucket 0 behavior). However, explicit records with `bucket: 0` are also perfectly valid for unseen items.
508
+
509
+ #### 2.5.5 Portability Best Practices
510
+
511
+ **On omitting answer content:** OQSEP deliberately does not store the literal text or selection of the user's answers (e.g., the exact string they typed). The `lastAnswer` object records only whether the answer was correct (`isCorrect`) and an optional confidence rating. This is a conscious design choice to keep the progress file lean. Spaced repetition algorithms require outcome signals (correctness, confidence, timing), not a replay of what was typed. If an application strictly requires saving the exact user input for custom features, it MUST place this data inside its `appSpecific` namespace.
512
+
513
+ **On `appSpecific` usage:** Algorithm-specific parameters that are opaque outside a given algorithm's mathematical model — such as FSRS stability (S) and difficulty (D) values — MUST be stored exclusively inside the `appSpecific` sub-object of the relevant record. Top-level keys within `appSpecific` MUST be the application identifier (e.g., `"memizy"`), with algorithm-specific data nested beneath (e.g., `{ "memizy": { "fsrs": { ... } } }`). This namespacing is mandatory to prevent silent key collisions when a file passes through multiple applications. Applications MUST preserve all keys in `appSpecific` when saving, modifying only their own namespace. Storing proprietary data at the top level of a Progress Record — or directly as an un-namespaced key in `appSpecific` — is not permitted and WILL cause validation failures.
514
+
515
+ **On cold-start import:** When an application using a complex algorithm (e.g., FSRS) receives a progress file produced by a simpler one (e.g., Leitner), the `appSpecific` block for its own algorithm will be absent. The application SHOULD gracefully initialize its internal parameters using the universally available `stats` (`attempts`, `incorrect`, `streak`) and `bucket` as seed data. This is the **Cold Start** scenario: deriving reasonable initial parameter estimates from raw statistics rather than refusing the import.
516
+
517
+ **On `setId` verification:** Importing applications MUST compare `meta.setId` against the UUID of the OQSE set being opened. If the values do not match, the application SHOULD warn the user and refrain from applying the progress data automatically to prevent data corruption.
518
+
519
+ **On partial records and unseen items:** The `records` map need not contain an entry for every item in the set. Items absent from the map are treated as unseen. However, applications MAY explicitly generate a record with `bucket: 0` for an item that has never been presented if they need to store pre-study initialization data, suspended states, or `appSpecific` metadata. While permitted, applications SHOULD NOT unnecessarily synthesize placeholder records for all unseen items just to fill the file, as this bloats the file size.
520
+
521
+ -----
522
+
523
+ ## 3. The `meta` Object
524
+
525
+ Contains all information describing the entire set.
526
+
527
+ | Key | Type | Required | Description |
528
+ | :--- | :--- | :--- | :--- |
529
+ | `id` | string | Yes | Unique UUID of the set. |
530
+ | `language` | string | Yes | Language code of the set according to BCP 47 (e.g., `cs`, `en-US`, `zh-Hans`). Application SHOULD be tolerant when reading formatting (e.g., accept and normalize `cs_CZ` and `cs-CZ`). |
531
+ | `title` | string | Yes | **Plain Text.** Title of the study set. |
532
+ | `description` | string | No | **Rich Content.** Short description of the set. |
533
+ | `thumbnail` | string | No | **Plain Text.** Key from `meta.assets` that defines the cover image of the set. If the specified key does not exist in `meta.assets`, the Application MUST continue loading the set (without a cover image). |
534
+ | `assets` | object | No | Dictionary (map) of media for the entire set. Used for: 1) set metadata (e.g., cover image), 2) **shared media** used in multiple items (e.g., a map that appears in 20 questions). The key is a local identifier, the value is a `MediaObject` object. Application when replacing `<asset:key />` uses fallback: first `item.assets`, then `meta.assets`. |
535
+ | `ageMin` | number | No | Minimum recommended age (0 = preschool, but recommended >= 3). |
536
+ | `ageMax` | number | No | Maximum recommended age. |
537
+ | `subject` | string | No | **Plain Text.** Subject/field in the set's language (e.g., `mathematics`, `biology`, `history`). |
538
+ | `createdAt` | string | Yes | Date and time of creation in ISO 8601 format (UTC recommended). |
539
+ | `updatedAt` | string | Yes | Date and time of last modification in ISO 8601 format. (UTC recommended)|
540
+ | `author` | PersonObject | No | Information about the main author of the set. [See section 3.4](#34-the-personobject-object-in-metaauthor-and-metacontributors). |
541
+ | `contributors` | PersonObject[] | No | Array of information about other contributors. |
542
+ | `license` | string | No | [SPDX license](https://spdx.org/licenses/) identifier (e.g., `CC-BY-SA-4.0`, `CC0-1.0`). Determines usage rights for the set. |
543
+ | `licenseUrl` | string | No | **URI (absolute URL).** Link to the full text of the license. |
544
+ | `requirements` | object | No | A **FeatureProfile** object describing set's explicit requirements (e.g., `features`, `latexPackages`). **Note:** Required item types and assets are implicit to the content and MUST NOT be listed here. |
545
+ | `tags` | string[] | No | **Plain Text.** Array of text labels (tags) for the entire set. |
546
+ | `tagDefinitions` | object | No | Dictionary (map), where the key is a string (tag) and the value is a TagDefinition object. [See section 3.3](#33-the-tagdefinition-object-as-value-in-tagdefinitions). Used for tag meanings in `meta.tags` and `item.tags`. |
547
+ | `translations` | TranslationObject[] | No | Array of references to translations of this set in other languages. [See **section 7.5**](#75-translationobject-for-metatranslations). |
548
+ | `sourceMaterials` | SourceMaterial[] | No | Array of objects describing sources from which the set draws. |
549
+ | `estimatedTime` | number | No | Estimated time to complete the set in minutes. |
550
+ | `prerequisites` | LinkedSetObject[] | No | Array of references to sets that should be completed before this set. [See section 7.6](#76-linkedsetobject-for-metaprerequisites-and-metarelatedsets). |
551
+ | `relatedSets` | LinkedSetObject[] | No | Array of references to related OQSE sets (for recommendations). [See section 7.6](#76-linkedsetobject-for-metaprerequisites-and-metarelatedsets). |
552
+ | `customData` | object | No | Object for metadata determined by the author/creator (e.g., internal notes, course ID at school). Not intended for application logic. |
553
+ | `appSpecific` | object | No | Object for metadata specific to a particular software application (e.g., editor configuration, display state, last position). Top-level keys MUST be namespaced by the application identifier to prevent collisions (e.g., `{ "memizy": { "lastPosition": 42 } }`). Other applications MUST ignore this object. |
554
+
555
+ ### 3.1. The `SourceMaterial` Object (in `sourceMaterials`)
556
+
557
+ | Key | Type | Required | Description |
558
+ | :--- | :--- | :--- | :--- |
559
+ | `id` | string | Yes | **Plain Text.** Unique ID (within the file) for this source (e.g., `src-1`, `book-physics`). Not a global identifier. |
560
+ | `type` | string | Yes | Source type from selection (`url`, `doi`, `isbn`, `pdf`, `textbook`, `video`, `audio`, `image`, `model`, `other`). |
561
+ | `value` | string | Yes | **Plain Text** (for identifiers: `doi`, `isbn`, `textbook`, `other`) or **URI (absolute URL)** (for types: `url`, `pdf`, `video`, `audio`, `image`, `model`). MUST match the type specified in `type`. |
562
+ | `title` | string | Yes | **Plain Text.** Source title (e.g., "Wikipedia: Raptor", "Physics for High Schools"). |
563
+ | `description` | string | No | **Plain Text.** Additional source description (e.g., "Chapter 5, pp. 42-50", "Official SpaceX website"). |
564
+ | `authors` | string[] | No | **Plain Text.** Source authors. |
565
+ | `publishedDate` | string | No | Source publication date (ISO 8601 - UTC recommended). |
566
+ | `retrievedAt` | string | No | Date (ISO 8601 - UTC recommended) when the source was used (important for online sources). |
567
+ | `license` | string | No | License of the original source (SPDX ID). |
568
+
569
+ ### 3.2. Description of Source Material Types (`type` in `SourceMaterial`)
570
+
571
+ The following table defines what content we expect in the `value` field for individual types:
572
+
573
+ | Type (`type`) | Expected content in `value` | Example `value` |
574
+ | :--- | :--- | :--- |
575
+ | `url` | Full URI address of the source. | `https://en.wikipedia.org/wiki/Raptor...` |
576
+ | `doi` | **Digital Object Identifier** – permanent identifier for scientific articles. | `10.1038/s41586-021-04106-z` |
577
+ | `isbn` | **International Standard Book Number** – unique book number. | `978-80-7357-124-7` |
578
+ | `pdf` | Direct URI address to PDF file. | `https://example.com/study.pdf` |
579
+ | `textbook` | Book title (if ISBN is not available or unknown, e.g., old scripts). | `Physics for High Schools (8th edition)` |
580
+ | `video` | URI address to video (e.g., YouTube, Vimeo). | `https://www.youtube.com/watch?v=...` |
581
+ | `audio` | URI address to audio file/stream (podcast, lecture recording). | `https://example.com/podcast/ep1.mp3` |
582
+ | `image` | URI address to image (infographic, map, artwork). | `https://example.com/map.png` |
583
+ | `model` | URI address to 3D model file (glTF/GLB). | `https://example.com/anatomy.glb` |
584
+ | `other` | Descriptive string for any other source type. | `Internal company document no. 123` |
585
+
586
+ ### 3.3. The `TagDefinition` Object (as value in `tagDefinitions`)
587
+
588
+ The key in `tagDefinitions` is a human-readable tag (string, Plain Text).
589
+
590
+ | Key | Type | Required | Description |
591
+ | :--- | :--- | :--- | :--- |
592
+ | `wikidataId` | string | No | "Q" identifier from Wikidata for semantic linking. |
593
+ | `description` | string | No | **Plain Text.** More detailed tag description. |
594
+
595
+ ### 3.4. The `PersonObject` Object (in `meta.author` and `meta.contributors`)
596
+
597
+ Complex type for identifying persons involved in the set.
598
+
599
+ | Key | Type | Required | Description |
600
+ | :--- | :--- | :--- | :--- |
601
+ | `name` | string | Yes | **Plain Text.** Full name of the person (First and Last Name). |
602
+ | `role` | string | No | **Plain Text.** Role in the project (e.g., "Editor", "Translator", "Biology Expert"). |
603
+ | `email` | string | No | **Plain Text.** Contact email. |
604
+ | `url` | string | No | **URI (absolute URL).** Link to website or author profile. |
605
+
606
+ -----
607
+
608
+ ## 4. Common Item Properties in `items`
609
+
610
+ Each object in the `items` array is one study item. All items share these basic keys:
611
+
612
+ | Key | Type | Required | Description |
613
+ | :--- | :--- | :--- | :--- |
614
+ | `id` | string | Yes | Unique UUID of the item. |
615
+ | `type` | string | Yes | Defines the item type and its other properties. [See section 6](#6-item-types-itemtype). |
616
+ | `assets` | object | No | Dictionary (map) of media, where the key is a local identifier (e.g., `"img1"`) and the value is a `MediaObject` object. [See section 5](#5-the-mediaobject-object). |
617
+ | `lang` | string | No | **Plain Text.** Language code (BCP 47) for *this specific* item. Overrides `meta.language`. |
618
+ | `tags` | string[] | No | **Plain Text.** Array of text labels (tags) for this item. |
619
+ | `topic` | string | No | **Plain Text.** A primary category, chapter, or lecture name to which the item belongs (e.g., "Lecture 1: Introduction"). Useful for grouping items in the UI independently of multiple `tags`. |
620
+ | `difficulty` | number | No | Numeric difficulty from range 1 (easy) to 5 (hard). Used for simple categorization in UI. For advanced psychometric models, use `pedagogy.irtDifficulty`. |
621
+ | `timeLimit` | number | No | Recommended time limit in seconds for answering this item. |
622
+ | `hints` | string[] | No | **Rich Content.** Array of hints from first to last. |
623
+ | `explanation` | string | No | **Rich Content.** Explanation of the correct answer. |
624
+ | `incorrectFeedback` | string | No | **Rich Content.** Message displayed to the user after an incorrect answer before it is appropriate to reveal the full `explanation`. Serves for gentle guidance or pointing out the error without revealing the solution. |
625
+ | `sources` | SourceReference[] | No | Array of references to sources (`sourceMaterials`) related to this item. [See section 7.7](#77-sourcereference-for-itemsources). |
626
+ | `relatedItems` | string[] | No | Array of IDs of related items in this set. All IDs MUST exist in the `items` array. Item MUST NOT reference its own `id`. Application MUST ignore non-existent references with a warning. |
627
+ | `dependencyItems` | string[] | No | Array of `id`s of items that should logically precede this item (prerequisites). All IDs MUST exist in the `items` array. Item MUST NOT reference its own `id`. Circular dependencies are allowed – if the Application supports dependencies, it MUST detect them; Applications that do not support dependencies MAY ignore this field. Application MUST ignore non-existent references with a warning. |
628
+ | `pedagogy` | Pedagogy | No | Object containing advanced data for adaptive learning and psychometrics. [See section 7.8](#78-the-pedagogy-object-adaptive-difficulty). |
629
+ | `customData` | object | No | Field for static creator metadata. |
630
+ | `appSpecific` | object | No | Field for application-specific static metadata. Top-level keys MUST be namespaced by the application identifier to prevent collisions (e.g., `{ "memizy": { "displayHint": "compact" } }`). Other applications MUST ignore this object. |
631
+
632
+ -----
633
+
634
+ ## 5. The `MediaObject` Object
635
+
636
+ Standardized object for embedding media. Used:
637
+ - As a value in `meta.assets` (dictionary of media at set level, e.g., cover image).
638
+ - As a value in `item.assets` (dictionary of media at item level).
639
+
640
+ | Key | Type | Required | Description |
641
+ | :--- | :--- | :--- | :--- |
642
+ | `type` | string | Yes | Media type from selection (`image`, `audio`, `video`, `model`). The specific allowed file formats are negotiated via the Application Manifest (`capabilities.assets`). For the `model` type, the glTF format (`model/gltf-binary` for `.glb` or `model/gltf+json` for `.gltf`) is highly RECOMMENDED as the universal baseline for interoperability. However, applications and sets MAY use other formats (like `.obj` or `.fbx`) provided they are explicitly declared and matched during the capability handshake. |
643
+ | `value` | string | Yes | URI of the resource. Can be **absolute URL** (e.g., `https://.../image.png`) or **relative path** to file in package (e.g., `assets/diagram.png`). |
644
+ | `mimeType` | string | Recommended | MIME type of file (e.g., `image/png`, `audio/mpeg`, `video/mp4`, `model/gltf-binary`). |
645
+ | `altText` | string | **REQUIRED for images**, optional for audio/video | **Plain Text.** Alternative text for accessibility. Must be plain text without formatting for screen readers. |
646
+ | `transcript` | string | No | **Rich Content.** Verbatim transcript of spoken word (for audio/video). For time codes, use WebVTT format (time codes MUST be on separate line, e.g., `00:00:15.000`). For inline formatting, use Markdown. MAY contain mathematical formulas in LaTeX. If present, enables fulltext search and indexing by AI agents without need to download and analyze binary file. Takes precedence over external subtitles. **Accessibility note:** Applications MUST remove all formatting when processing for screen readers and leave only plain text. |
647
+ | `caption` | string | No | **Rich Content.** Media caption displayed below/next to media. |
648
+ | `width` | number | No | Preferred width in pixels (rendering hint). |
649
+ | `height` | number | No | Preferred height in pixels (rendering hint). |
650
+ | `start` | number | No | Time in seconds where media playback should start (for audio/video). Default: 0. |
651
+ | `end` | number | No | Time in seconds where media playback should end. If not specified, plays to the end. |
652
+ | `loop` | boolean | No | For audio/video only. Determines whether media should loop. Default value is `false`. **Rules for combination with `start`/`end`:** (1) `loop: true` + `end`: Loop from `start` to `end`. (2) `loop: true` without `end`: Loop entire file from `start`. (3) `loop: false` + `end`: Play once from `start` to `end`. (4) `loop: false` without `end`: Play once from `start` to end of file (default behavior). **Validation rules:** `start` MUST be >= 0. If `end` is specified, `end > start` MUST hold. If `end` > file length, use end of file. If `start >= end`, Application MUST declare error. If `loop: true` and `start == end`, Application MUST declare error (empty segment). |
653
+ | `subtitles` | array | No | For audio/video only. Array of objects defining subtitles. Each object contains: `lang` (string, required): Language code (BCP 47, e.g., `cs`, `en`), `value` (string, required): URI to `.vtt` or `.srt` file (absolute URL or relative path in `.oqse` package), `label` (string, optional): Display name (e.g., "Czech", "English"), `kind` (string, optional): Subtitle type - `captions`, `subtitles`, or `descriptions` per WebVTT specification (default: `subtitles`). Example: `[{"lang": "cs", "value": "assets/subtitles/cs.vtt", "label": "Czech", "kind": "captions"}, {"lang": "en", "value": "assets/subtitles/en.vtt", "label": "English"}]`. **Validation rules:** File format MUST be `.vtt` (recommended) or `.srt`. Value `kind` MUST be one of: `captions`, `subtitles`, `descriptions`. If `label` is missing, use value of `lang`. Application MUST ignore invalid formats with warning. |
654
+ | `license` | string | No | SPDX license for this specific media (if different from set). |
655
+ | `attribution` | string | No | **Plain Text.** Media author/source attribution (e.g., "Photo: NASA"). |
656
+ | `checksums` | object | No | Object for file integrity verification. Key is algorithm name (`"sha256"`, `"sha512"`, `"md5"`, etc.) and value is hexadecimal hash. Example: `{"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "md5": "098f6bcd4621d373cade4e832627b4f6"}`. Hash is calculated from original **binary data** of file (not from URI string). **Security rules:** For files in `.oqse` package, Application MUST calculate hash after unpacking and compare with values in object. Application MUST support at minimum the `sha256` algorithm. For external URLs: If file is small (< 10 MB), Application MAY download and verify. If file is large (>= 10 MB), Application MAY skip validation and display warning. Application SHOULD allow user to disable checksum validation for streaming media. If hash does not match, Application MUST reject asset and display security warning. If `checksums` is missing, Application MAY use file but SHOULD display warning about unverified integrity. |
657
+
658
+ **Note:** To use different segments of the same file in one set, define multiple assets in `assets` with the same `value` but different `start` and `end` values.
659
+
660
+ **Rule for asset keys:** Asset identifier (key used in `assets` and in tag `<asset:key />`) MUST match regular expression `^[a-z0-9_-]+$` (lowercase only). Only lowercase letters, digits, underscores, and hyphens are allowed. Spaces or other special characters are not permitted. Application MUST:
661
+ * On import: Accept uppercase and automatically convert to lowercase with warning
662
+ * On import: Reject (with error) situation where two different names match after conversion to lowercase (collision prevention)
663
+ * On export: Always generate lowercase keys
664
+ * When processing `<asset:... />` tags: Normalize captured token to lowercase before lookup, so syntax is case-insensitive even in user-written text
665
+
666
+ ### 5.1. Media Reference Syntax (Media Tags)
667
+
668
+ In all text fields with "rich content" (Rich Content), media defined in `assets` are referenced exclusively using the unified self-closing Media Tag syntax:
669
+
670
+ ```
671
+ <asset:media_key />
672
+ ```
673
+
674
+ This syntax is designed to be unambiguous and not collide with regular text or Markdown formatting.
675
+
676
+ #### Recommended Processing (Rendering Pipeline)
677
+
678
+ Given support for GFM (which allows HTML) and custom `<asset />` tags, application MUST follow safe processing order:
679
+
680
+ 1. **Asset Tokenization:** Before rendering, find all occurrences of `<asset:key />` and replace them with unique text placeholder that Markdown processor won't change (e.g., `[[__ASSET_XYZ__]]`).
681
+ 2. **Markdown Rendering:** Convert text (containing placeholders) using GFM processor to HTML.
682
+ 3. **Sanitization:** Clean resulting HTML of dangerous elements (scripts, iframes, event handlers) using sanitizer (e.g., DOMPurify).
683
+ 4. **Detokenization:** Replace back placeholders `[[__ASSET_XYZ__]]` with final safe HTML elements (`<img>`, `<audio>`, etc.).
684
+
685
+ #### Application Rules
686
+
687
+ * Application MUST search text for strings matching pattern `<asset:([^ >]+)\s*/>`.
688
+ * Tag functions as placeholder. Application MUST completely replace it with corresponding HTML element according to `MediaObject.type` (e.g., `<img>`, `<audio>`, `<video>`).
689
+ * Rich Content fields therefore never contain direct HTML tags for media – only these references.
690
+ * Application MUST NOT support alternative syntaxes (e.g., Markdown links `[...](asset://...)`).
691
+ * Visible media caption is rendered from `MediaObject.caption`, alternative text from `MediaObject.altText`.
692
+ * **Time parameters:** If `start` or `end` parameters are defined for YouTube/Vimeo videos, Application **SHOULD** attempt to project them into embed URL (e.g., `?start=30`), since standard HTML attributes for `<iframe>` don't handle this.
693
+
694
+ **Processing example:**
695
+
696
+ * Input: `"Look at the graph: <asset:pressure_graph />"`
697
+ * Asset definition: `{ "type": "image", "altText": "Pressure over time graph" }`
698
+ * Output for renderer: `Look at the graph: <img src="..." alt="Pressure over time graph">`
699
+
700
+ #### Scope Resolution (Global vs. Local assets)
701
+
702
+ Media can be defined at two levels:
703
+ * `meta.assets` (global) – for media shared between multiple items or set metadata
704
+ * `item.assets` (local) – for media specific to one item
705
+
706
+ **Search rule (Fallback logic):**
707
+
708
+ 1. Application first searches for `key` in `item.assets` dictionary. If found, use this media.
709
+ 2. If not in `item.assets`, search for `key` in `meta.assets`.
710
+ 3. If not found anywhere, it is an error (missing media).
711
+
712
+ If same key exists in both scopes, local definition (`item.assets`) takes precedence. **Note:** Application MUST log warning about shadowing of global asset and SHOULD recommend author use unique keys to prevent confusion.
713
+
714
+ **Example JSON (local assets):**
715
+
716
+ ```json
717
+ {
718
+ "assets": {
719
+ "img_raptor": { "type": "image", "value": "https://.../raptor.jpg", "altText": "Official Raptor engine image" },
720
+ "snd_start": { "type": "audio", "value": "assets/start.mp3" }
721
+ },
722
+ "question": "What is in the image? <asset:img_raptor /> And what sound does it make? <asset:snd_start />"
723
+ }
724
+ ```
725
+
726
+ **Example JSON (reusability with global assets):**
727
+
728
+ ```json
729
+ {
730
+ "meta": {
731
+ "title": "Geography of Europe",
732
+ "assets": {
733
+ "europe_map": {
734
+ "type": "image",
735
+ "value": "https://example.com/europe-map.jpg",
736
+ "altText": "Political map of Europe",
737
+ "license": "CC-BY-SA-4.0"
738
+ }
739
+ }
740
+ },
741
+ "items": [
742
+ {
743
+ "id": "019aa5eb-5c91-7bce-93db-fb427ea4f333",
744
+ "type": "short-answer",
745
+ "question": "Where is Prague on the map? <asset:europe_map />",
746
+ "answers": ["Prague"]
747
+ },
748
+ {
749
+ "id": "019aa5eb-902b-781e-81b7-0fd9e37b43f1",
750
+ "type": "short-answer",
751
+ "question": "Where is Berlin on the map? <asset:europe_map />",
752
+ "answers": ["Berlin"]
753
+ }
754
+ ]
755
+ }
756
+ ```
757
+
758
+ **Note:** Both questions use the same `europe_map` media from `meta.assets` – no need to duplicate. Application ensures replacement of `<asset:europe_map />` with correct HTML tag in each item.
759
+
760
+ -----
761
+
762
+ ## 6. Item Types (`item.type`)
763
+
764
+ The specification in the current version defines **22 official item types**. Each application MUST clearly identify the type using the `type` key, which determines what other processing logic is applied.
765
+
766
+ ### 6.1. `type: "note"` (Study Note)
767
+
768
+ * `title` (string, optional): **Plain Text.** Note heading.
769
+ * `content` (string, required): **Rich Content.** Main educational content of the item (text, Markdown, LaTeX, media references).
770
+ * `hiddenContent` (string, optional): **Rich Content.** Additional content that is initially hidden (e.g., answer to a self-check question, detailed derivation, or spoiler). User must perform an action to reveal it.
771
+
772
+ **Example:**
773
+ ```json
774
+ {
775
+ "id": "019aa5ec-3daa-796f-9289-01c6214ec2b3",
776
+ "type": "note",
777
+ "title": "Introduction to Thermodynamics",
778
+ "content": "Thermodynamics is a branch of physics dealing with heat, work, and temperature. What is the First Law?",
779
+ "hiddenContent": "The **First Law of Thermodynamics** states that energy cannot be created or destroyed, only transferred or changed from one form to another."
780
+ }
781
+ ```
782
+
783
+ ### 6.2. `type: "flashcard"` (Flashcard)
784
+
785
+ * `front` (string, required): **Rich Content.** Text on front side.
786
+ * `back` (string, required): **Rich Content.** Text on back side.
787
+
788
+ **Example:**
789
+ ```json
790
+ {
791
+ "id": "019aa5ef-1d0e-7cb5-b894-ce83cfd04f70",
792
+ "type": "flashcard",
793
+ "assets": {
794
+ "img1": {
795
+ "type": "image",
796
+ "value": "https://example.com/raptor.jpg",
797
+ "altText": "Raptor Engine"
798
+ }
799
+ },
800
+ "front": "What is the name of the engine in the image? <asset:img1 />",
801
+ "back": "**Raptor** - engine developed by SpaceX."
802
+ }
803
+ ```
804
+
805
+ ### 6.3. `type: "true-false"` (True/False)
806
+
807
+ * `question` (string, required): **Rich Content.** Statement to be evaluated.
808
+ * `answer` (boolean, required): Correct answer (`true` or `false`).
809
+
810
+ **Example:**
811
+ ```json
812
+ {
813
+ "id": "019aa5ef-acd6-78ce-b953-d0ef601d13aa",
814
+ "type": "true-false",
815
+ "question": "Earth is the closest planet to the Sun.",
816
+ "answer": false,
817
+ "explanation": "Mercury is the closest planet to the Sun. Earth is the third planet from the Sun."
818
+ }
819
+ ```
820
+
821
+ ### 6.4. `type: "mcq-single"` (Single Choice)
822
+
823
+ * `question` (string, required): **Rich Content.** Question text.
824
+ * `options` (string[], required): **Rich Content.** Array of text options. Must contain at least 2 items.
825
+ * `correctIndex` (number, required): Index (0, 1, 2...) of correct answer in `options` array.
826
+ * `shuffleOptions` (boolean, optional): Whether application should shuffle options. Default: `true`.
827
+ * `optionExplanations` (Array<string | null>, optional): **Rich Content.** Array of explanations specific to each option. Index in this array corresponds to index in `options` array. If specific option doesn't require explanation (e.g., it's obvious), value `null` may be at that position.
828
+
829
+ **Example:**
830
+ ```json
831
+ {
832
+ "id": "019aa5f0-60c7-790c-abca-4c6e97af05da",
833
+ "type": "mcq-single",
834
+ "question": "Which engine was used on the 1st stage of the **Saturn V** rocket?",
835
+ "options": [
836
+ "Raptor",
837
+ "Merlin",
838
+ "F-1"
839
+ ],
840
+ "correctIndex": 2,
841
+ "shuffleOptions": true,
842
+ "explanation": "Five **F-1** engines powered the first stage of Saturn V."
843
+ }
844
+ ```
845
+
846
+ ### 6.5. `type: "mcq-multi"` (Multiple Choice)
847
+
848
+ * `question` (string, required): **Rich Content.** Question text.
849
+ * `options` (string[], required): **Rich Content.** Array of text options. Must contain at least 2 items.
850
+ * `correctIndices` (number[], required): Array of indices of correct answers. Must contain at least 1 index.
851
+ * `minSelections` (number, optional): Minimum number of answers user must select.
852
+ * `maxSelections` (number, optional): Maximum number of answers user can select.
853
+ * `shuffleOptions` (boolean, optional): Whether application should shuffle options. Default: `true`.
854
+ * `optionExplanations` (Array<string | null>, optional): **Rich Content.** Array of explanations specific to each option. Index in this array corresponds to index in `options` array. If specific option doesn't require explanation (e.g., it's obvious), value `null` may be at that position.
855
+
856
+ **Example:**
857
+ ```json
858
+ {
859
+ "id": "019aa5f0-ad74-7e5a-ad52-aedbc02c6b33",
860
+ "type": "mcq-multi",
861
+ "question": "Which of the following planets are gas giants?",
862
+ "options": [
863
+ "Jupiter",
864
+ "Mars",
865
+ "Saturn",
866
+ "Earth",
867
+ "Neptune"
868
+ ],
869
+ "correctIndices": [0, 2, 4],
870
+ "minSelections": 1,
871
+ "shuffleOptions": true,
872
+ "explanation": "The gas giants of the solar system are: **Jupiter**, **Saturn**, **Uranus**, and **Neptune**."
873
+ }
874
+ ```
875
+
876
+ ### 6.6. `type: "short-answer"` (Short Text Answer)
877
+
878
+ * `question` (string, required): **Rich Content.** Question text.
879
+ * `answers` (string[], required): **Plain Text.** Array of acceptable text answers (for checking variants). Must contain at least 1 item.
880
+ * `caseSensitive` (boolean, optional): Distinguish letter case. Default: `false`.
881
+ * `trimWhitespace` (boolean, optional): Ignore spaces at beginning/end. Default: `true`.
882
+ * `acceptPartial` (boolean, optional): Accept approximate match (fuzzy matching). Allows answers with minor typos using Levenshtein distance (max. 1-2 edit operations: insertion, deletion, character substitution). Default: `false`.
883
+ * `ignoreDiacritics` (boolean, optional): If `true`, application removes diacritics from user input and from `answers` array before comparison (e.g., "citron" will match "citrón"). Default: `false`.
884
+
885
+ **Example:**
886
+ ```json
887
+ {
888
+ "id": "019aa5f1-7cb7-7f8a-a085-98ae208b25ec",
889
+ "type": "short-answer",
890
+ "question": "What is the capital of the Czech Republic?",
891
+ "answers": ["Prague", "praha"],
892
+ "caseSensitive": false,
893
+ "trimWhitespace": true,
894
+ "hints": ["City on the Vltava River", "Starts with letter P"]
895
+ }
896
+ ```
897
+
898
+ ### 6.7. `type: "fill-in-blanks"` (Fill in the Blanks)
899
+
900
+ * `question` (string, optional): **Rich Content.** Heading or instructions.
901
+ * `text` (string, required): **Rich Content.** Text with blanks marked using blank tags `<blank:token />` (e.g., `<blank:1 />`). **Token rules:** Token MUST be alphanumeric identifier (`a-z`, `A-Z`, `0-9`, `_`, `-`). Tokens are case-sensitive (`<blank:A />` ≠ `<blank:a />`). Maximum length: 64 characters. Regex: `^[a-zA-Z0-9_-]{1,64}$`. **Valid token examples:** `<blank:1 />`, `<blank:q1 />`, `<blank:answer_A />`, `<blank:first-name />`. Syntax is consistent with Media Tag (`<asset:key />`) for easier parsing. **Duplicate tokens:** If same token (e.g., `<blank:1 />`) occurs multiple times in text, all its occurrences represent **the same** input field. Application SHOULD ensure that when one occurrence is filled, other occurrences with same token are automatically filled as well.
902
+ * `blanks` (object, required): Dictionary (map), where key is `token` and value is array `string[]` (**Plain Text**) of correct answers. Must contain at least 1 `token`.
903
+ * `caseSensitive` (boolean, optional): Distinguish letter case when checking answers. Default: `false`.
904
+ * `trimWhitespace` (boolean, optional): Ignore spaces at beginning and end of answer. Default: `true`.
905
+
906
+ **Validation rules:**
907
+ - All keys in `blanks` object MUST exist as blank tags `<blank:key />` in `text` field
908
+ - All blank tags `<blank:key />` in `text` field MUST have corresponding key in `blanks` object
909
+ - Application MUST declare error on mismatch
910
+ - Application MUST search for blank tags using regex pattern `<blank:([a-zA-Z0-9_-]{1,64})\s*/>`
911
+
912
+ **Example:**
913
+ ```json
914
+ {
915
+ "id": "019aa5f2-453b-7c46-8a8b-7d868cfd79cd",
916
+ "type": "fill-in-blanks",
917
+ "question": "Fill in the missing information about planet Earth:",
918
+ "text": "Earth is the <blank:1 /> planet from the Sun. It has <blank:2 /> natural satellite called <blank:3 />.",
919
+ "blanks": {
920
+ "1": ["third", "3rd", "3"],
921
+ "2": ["one", "1"],
922
+ "3": ["Moon", "the Moon"]
923
+ }
924
+ }
925
+ ```
926
+
927
+ ### 6.8. `type: "fill-in-select"` (Fill in with Selection)
928
+
929
+ * `question` (string, optional): **Rich Content.** Heading or instructions.
930
+ * `text` (string, required): **Rich Content.** Text with blanks marked using blank tags `<blank:token />` (e.g., `<blank:1 />`).
931
+ * `blanks` (object, required): Dictionary (map), where key is `token` (e.g., `"1"`) and value is `SelectBlankObject` object ([see section 7.1](#71-selectblankobject-for-fill-in-select)).
932
+
933
+ **Example:**
934
+ ```json
935
+ {
936
+ "id": "019aa5f3-b525-7146-be57-8f010e276845",
937
+ "type": "fill-in-select",
938
+ "question": "Select the correct answers in the text:",
939
+ "text": "The Sun is a <blank:1 /> and Earth orbits around it every <blank:2 /> days.",
940
+ "blanks": {
941
+ "1": {
942
+ "options": ["star", "planet", "moon"],
943
+ "correctIndex": 0
944
+ },
945
+ "2": {
946
+ "options": ["30", "365", "24"],
947
+ "correctIndex": 1
948
+ }
949
+ }
950
+ }
951
+ ```
952
+
953
+ ### 6.9. `type: "match-pairs"` (Match Pairs)
954
+
955
+ * `question` (string, optional): **Rich Content.** Instructions.
956
+ * `prompts` (string[], required): **Rich Content.** Left side (what is being matched) - array of strings. Must contain at least 2 items.
957
+ * `matches` (string[], required): **Rich Content.** Right side (target) - array of strings. Must have same length as `prompts`. Minimum length: 2 (at least 2 pairs). Maximum length: 100 items per [section 9.1](#91-length-and-size-constraints).
958
+ * **Note:** `prompts[0]` belongs to `matches[0]`, etc. Application shuffles pairs before display.
959
+
960
+ **Example:**
961
+ ```json
962
+ {
963
+ "id": "019aa5f4-187a-7bb1-8a16-c461ea8a6839",
964
+ "type": "match-pairs",
965
+ "question": "Match planets with their characteristics:",
966
+ "prompts": [
967
+ "Mars",
968
+ "Jupiter",
969
+ "Saturn"
970
+ ],
971
+ "matches": [
972
+ "Red planet",
973
+ "Largest planet",
974
+ "Planet with rings"
975
+ ]
976
+ }
977
+ ```
978
+
979
+ ### 6.10. `type: "match-complex"` (Advanced Matching)
980
+
981
+ * `question` (string, optional): **Rich Content.** Instructions.
982
+ * `leftItems` (string[], required): **Rich Content.** Items on left side.
983
+ * `rightItems` (string[], required): **Rich Content.** Items on right side.
984
+ * `connections` (number[][], required): Array of index pairs `[left_index, right_index]` defining correct pairs.
985
+ * `minCorrect` (number, optional): Minimum number of correct connections required for success. If not specified, user must find all connections in `connections`.
986
+ * **Note:** This type explicitly supports more complex graph structures:
987
+ * Multiple connections are allowed in both directions (`1:N`, `N:1`, `N:M`).
988
+ * Isolated items (distractors) are allowed.
989
+ * Application MUST validate that each index in pair `[left, right]` falls within range of respective array. If index doesn't exist, import MUST stop with error.
990
+ * If two different connections reference same `[left, right]`, Application MUST reject duplicity (has no benefit and complicates evaluation).
991
+
992
+ **Example:**
993
+ ```json
994
+ {
995
+ "id": "019aa5f5-3f94-78d4-a9dc-6a0cd3a1a7df",
996
+ "type": "match-complex",
997
+ "question": "Match capitals with countries (some cities are extra):",
998
+ "leftItems": ["France", "Germany", "Spain"],
999
+ "rightItems": ["Paris", "Berlin", "Madrid", "London", "Rome"],
1000
+ "connections": [
1001
+ [0, 0],
1002
+ [1, 1],
1003
+ [2, 2]
1004
+ ]
1005
+ }
1006
+ ```
1007
+
1008
+ **Example with multiple connections and distractors:**
1009
+ ```json
1010
+ {
1011
+ "id": "019aa5f5-907f-7373-aeed-128158901d17",
1012
+ "type": "match-complex",
1013
+ "question": "Match rockets to their main engines (some options are extra):",
1014
+ "leftItems": ["Starship", "Falcon 9", "Space Launch System"],
1015
+ "rightItems": ["Raptor", "Merlin", "RS-25", "BE-4"],
1016
+ "connections": [
1017
+ [0, 0],
1018
+ [0, 3],
1019
+ [1, 1],
1020
+ [2, 2]
1021
+ ],
1022
+ "minCorrect": 3
1023
+ }
1024
+
1025
+ ```
1026
+ In the example above, `Starship` has two correct connections (`Raptor` and `BE-4`), demonstrating the `1:N` matching capability. All four right-side items are referenced as correct answers — an item without any connection to the left side would be a distractor. Application MUST still maintain index order and ensure no index exceeds defined arrays.
1027
+
1028
+ ### 6.11. `type: "sort-items"` (Sorting)
1029
+
1030
+ * `question` (string, required): **Rich Content.** Instructions.
1031
+ * `items` (string[], required): **Rich Content.** Array of items in **correct** order. Must contain at least 2 items and application MUST shuffle them.
1032
+
1033
+ **Example:**
1034
+ ```json
1035
+ {
1036
+ "id": "019aa5f6-e38b-70f2-898e-a13562b77963",
1037
+ "type": "sort-items",
1038
+ "question": "Sort planets by distance from the Sun:",
1039
+ "items": [
1040
+ "Mercury",
1041
+ "Venus",
1042
+ "Earth",
1043
+ "Mars"
1044
+ ]
1045
+ }
1046
+ ```
1047
+
1048
+ ### 6.12. `type: "slider"` (Slider / Numeric Answer)
1049
+
1050
+ * `question` (string, required): **Rich Content.** Question text.
1051
+ * `min` (number, required): Minimum value on slider.
1052
+ * `max` (number, required): Maximum value on slider.
1053
+ * `step` (number, required): Slider step (e.g., 1, 0.1). Must be > 0. RECOMMENDED: `step` SHOULD be such that `(max - min) / step` gives whole number (e.g., `max: 100, min: 0, step: 5` → 20 steps).
1054
+ * `correctAnswer` (number, required): Correct value. MUST be reachable by slider: `(correctAnswer - min) % step === 0` and `correctAnswer` MUST lie in interval `<min, max>`.
1055
+ * `tolerance` (number, required): Allowed deviation (can be `0`). **Validation rules:** `tolerance >= 0` and `tolerance <= (max - min) / 2`. RECOMMENDED: `tolerance` should be multiple of `step` (e.g., `step: 5, tolerance: 10`), otherwise inconsistent answer evaluation may occur. If `tolerance % step != 0`, Application MUST issue warning about possible inconsistency.
1056
+ * `unit` (string, optional): **Plain Text.** Unit (e.g., "year", "m", "°C").
1057
+
1058
+ **Example:**
1059
+ ```json
1060
+ {
1061
+ "id": "019aa5f8-7e0b-70e6-b69b-16ce3886b447",
1062
+ "type": "slider",
1063
+ "question": "In what year did the first human land on the Moon?",
1064
+ "min": 1950,
1065
+ "max": 2000,
1066
+ "step": 1,
1067
+ "correctAnswer": 1969,
1068
+ "tolerance": 0,
1069
+ "unit": "year",
1070
+ "explanation": "Neil Armstrong and Buzz Aldrin landed on the Moon on July 20, **1969** during the Apollo 11 mission."
1071
+ }
1072
+ ```
1073
+
1074
+ ### 6.13. `type: "pin-on-image"` (Pin on Image)
1075
+
1076
+ * `question` (string, required): **Rich Content.** Instructions.
1077
+ * `targetAsset` (string, required): **Key from `assets`**, which determines the image to be clicked on.
1078
+ * `hotspots` (HotspotObject[], required): Array defining correct areas. Must contain at least 1 hotspot. [See section 7.2](#72-hotspotobject-for-pin-on-image).
1079
+ * `multipleCorrect` (boolean, optional): Determines whether user must mark more than one hotspot. Default: `false` (one click is enough).
1080
+ * `minCorrect` (number, optional): If `multipleCorrect: true`, determines minimum number of correct hotspots user must find. If `multipleCorrect: false` or missing, this field is ignored.
1081
+
1082
+ **Example:**
1083
+ ```json
1084
+ {
1085
+ "id": "019aa5f8-eff5-7bb4-8cec-964b6664e476",
1086
+ "type": "pin-on-image",
1087
+ "assets": {
1088
+ "map1": {
1089
+ "type": "image",
1090
+ "value": "https://example.com/europe-map.jpg",
1091
+ "altText": "Map of Europe"
1092
+ }
1093
+ },
1094
+ "question": "Click on Prague on the map.",
1095
+ "targetAsset": "map1",
1096
+ "hotspots": [
1097
+ {
1098
+ "type": "circle",
1099
+ "x": 50,
1100
+ "y": 50,
1101
+ "radius": 5,
1102
+ "label": "Prague"
1103
+ }
1104
+ ]
1105
+ }
1106
+ ```
1107
+
1108
+ ### 6.14. `type: "categorize"` (Categorization)
1109
+
1110
+ * `question` (string, required): **Rich Content.** Instructions.
1111
+ * `categories` (string[], required): **Plain Text.** Array of category names. Must contain at least 2 items.
1112
+ * `items` (CategorizeItem[], required): Items to sort. Each item references category using 0-based index. [See section 7.3](#73-categorizeitem-for-categorize).
1113
+ * **Note:** User assigns each item to a category.
1114
+
1115
+ **Example:**
1116
+ ```json
1117
+ {
1118
+ "id": "019aa5fa-086e-76c5-9731-a872689f1112",
1119
+ "type": "categorize",
1120
+ "question": "Sort animals into categories:",
1121
+ "categories": ["Mammals", "Birds", "Fish"],
1122
+ "items": [
1123
+ {
1124
+ "id": "item1",
1125
+ "text": "Dolphin",
1126
+ "correctCategoryIndex": 0
1127
+ },
1128
+ {
1129
+ "id": "item2",
1130
+ "text": "Eagle",
1131
+ "correctCategoryIndex": 1
1132
+ },
1133
+ {
1134
+ "id": "item3",
1135
+ "text": "Shark",
1136
+ "correctCategoryIndex": 2
1137
+ },
1138
+ {
1139
+ "id": "item4",
1140
+ "text": "Penguin",
1141
+ "correctCategoryIndex": 1
1142
+ }
1143
+ ]
1144
+ }
1145
+ ```
1146
+
1147
+ ### 6.15. `type: "timeline"` (Timeline)
1148
+
1149
+ * `question` (string, required): **Rich Content.** Instructions.
1150
+ * `events` (TimelineEvent[], required): Array of events in correct chronological order. Must contain at least 2 events. [See section 7.4](#74-timelineevent-for-timeline) for detail on date handling.
1151
+ * `randomize` (boolean, optional): Determines whether application should shuffle events before displaying to user. Default: `true`. Set to `false` if you want to display events as study material in chronological order.
1152
+
1153
+ **Example:**
1154
+ ```json
1155
+ {
1156
+ "id": "019aa5fb-932c-7ac0-900b-af794143124a",
1157
+ "type": "timeline",
1158
+ "question": "Sort events from space flight history:",
1159
+ "events": [
1160
+ {
1161
+ "id": "ev1",
1162
+ "text": "First human in space (Yuri Gagarin)",
1163
+ "date": "1961-04-12T00:00:00Z",
1164
+ "precision": "day"
1165
+ },
1166
+ {
1167
+ "id": "ev2",
1168
+ "text": "First Moon landing (Apollo 11)",
1169
+ "date": "1969-07-20T00:00:00Z",
1170
+ "precision": "day"
1171
+ },
1172
+ {
1173
+ "id": "ev3",
1174
+ "text": "First space shuttle flight (Columbia)",
1175
+ "date": "1981-04-12T00:00:00Z",
1176
+ "precision": "day"
1177
+ }
1178
+ ]
1179
+ }
1180
+ ```
1181
+
1182
+ ### 6.16. `type: "matrix"` (Matrix / Table Answer)
1183
+
1184
+ * `question` (string, required): **Rich Content.** Instructions.
1185
+ * `rows` (string[], required): **Plain Text.** Row labels. Must not be empty array.
1186
+ * `columns` (string[], required): **Plain Text.** Column labels. Must not be empty array.
1187
+ * `correctCells` (number[][], required): Array of coordinates of correct answers in format `[row, column]`. Indices are 0-based. Example: `[[0, 1], [2, 0]]` marks correct answer in 1st row/2nd column and 3rd row/1st column.
1188
+ * `multiplePerRow` (boolean, optional): Can user select multiple answers in one row? Default: `false`.
1189
+ * **Note:** Useful for "Which of the following statements are true?" type questions.
1190
+
1191
+ **Example:**
1192
+ ```json
1193
+ {
1194
+ "id": "019aa5fb-d486-7225-8741-0f714b2ae057",
1195
+ "type": "matrix",
1196
+ "question": "Mark which properties apply to which planets:",
1197
+ "rows": ["Mars", "Jupiter", "Saturn"],
1198
+ "columns": ["Has rings", "Is gas giant", "Has red color"],
1199
+ "correctCells": [
1200
+ [0, 2],
1201
+ [1, 1],
1202
+ [2, 0],
1203
+ [2, 1]
1204
+ ],
1205
+ "multiplePerRow": true
1206
+ }
1207
+ ```
1208
+
1209
+ ### 6.17. `type: "math-input"` (Math Input)
1210
+
1211
+ * `question` (string, required): **Rich Content.** Question text.
1212
+ * `correctAnswer` (string, required): Correct answer in LaTeX format (e.g., `$2x + 2$`).
1213
+ * `alternativeAnswers` (string[], optional): Array of other **LaTeX strings** (same format as `correctAnswer`) that should also be considered correct (e.g., `$2*x + 2$`, `$2+2x$`).
1214
+ * `tolerance` (number, optional): For purely numeric answers, allowed numeric deviation.
1215
+ * **Implementation note:** Application **SHOULD** support symbolic checking of mathematical equivalence (e.g., recognize `$2+2x$` same as `$2x+2$`). All LaTeX notations MUST be compatible with web rendering libraries (KaTeX, MathJax). If application does not support symbolic checking (e.g., due to technical limitations), it **MUST** use plain string comparison. **String comparison rules:** (1) Normalize whitespace (remove all spaces inside LaTeX). (2) Compare string with `correctAnswer` and each item in `alternativeAnswers`. (3) Case-sensitive. (4) DO NOT convert symbols (e.g., `*` stays `*`, not rewritten to `\cdot`). **Recommendations for set authors:** Prefer normalized form without spaces if Application doesn't support symbolic mathematical checking (CAS). Include all common notation variants in `alternativeAnswers`. Example: `correctAnswer: '$2x+2$', alternativeAnswers: ['$2*x+2$', '$2 x + 2$', '$2+2x$']`.
1216
+
1217
+ **Example:**
1218
+
1219
+ ```json
1220
+ {
1221
+ "id": "018c84f1-5e1a-7b3d-9f2e-4a6c8d0f1e3a",
1222
+ "type": "math-input",
1223
+ "difficulty": 4,
1224
+ "question": "Simplify the following expression: $2(x+1)$",
1225
+ "correctAnswer": "$2x + 2$",
1226
+ "alternativeAnswers": [
1227
+ "$2*x + 2$"
1228
+ ]
1229
+ }
1230
+ ```
1231
+
1232
+ ### 6.18. `type: "diagram-label"` (Diagram Labeling)
1233
+
1234
+ * `question` (string, required): **Rich Content.** Instructions.
1235
+ * `targetAsset` (string, required): **Key from `assets`**, which determines the diagram image.
1236
+ * `labels` (string[], required): **Rich Content.** Array of text labels for user to assign. Must contain all correct answers and may contain distractors (extra labels).
1237
+ * `caseSensitive` (boolean, optional): Distinguish letter case when comparing labels. Default: `false`. Applies globally to all zones in this item.
1238
+ * `requireTyping` (boolean, optional): Default `false`. If `true`, application MUST NOT display draggable labels; instead render text fields for each zone and compare user input with `labels[correctLabelIndex]` (respects `caseSensitive`, also recommended to trim whitespace). Value `true` therefore activates "type answer" mode without needing to change `labels` structure.
1239
+ * `zones` (object[], required): Array of zones on image. Each zone is defined as extended `HotspotObject` ([see section 7.2](#72-hotspotobject-for-pin-on-image)) with added `correctLabelIndex` key.
1240
+
1241
+ **Zone structure in `zones` array:**
1242
+ * All properties of `HotspotObject` (`type`, `x`, `y`...).
1243
+ * `correctLabelIndex` (number, required): Index of correct label from `labels` array (0-based).
1244
+ * For point definition, circle with small radius can be used
1245
+
1246
+ **Example:**
1247
+ ```json
1248
+ {
1249
+ "id": "019aa5fd-24da-7d70-ba5e-d81414c25743",
1250
+ "type": "diagram-label",
1251
+ "question": "Assign names to parts of the cell:",
1252
+ "targetAsset": "img_cell",
1253
+ "assets": {
1254
+ "img_cell": {
1255
+ "type": "image",
1256
+ "value": "https://example.com/cell.png",
1257
+ "altText": "Cell diagram"
1258
+ }
1259
+ },
1260
+ "labels": ["Nucleus", "Mitochondria", "Ribosome"],
1261
+ "zones": [
1262
+ {
1263
+ "type": "rect",
1264
+ "x": 10,
1265
+ "y": 10,
1266
+ "width": 20,
1267
+ "height": 20,
1268
+ "correctLabelIndex": 0
1269
+ },
1270
+ {
1271
+ "type": "circle",
1272
+ "x": 50,
1273
+ "y": 50,
1274
+ "radius": 10,
1275
+ "correctLabelIndex": 1
1276
+ }
1277
+ ]
1278
+ }
1279
+ ```
1280
+ If item should require independent writing of names instead of dragging, set `requireTyping: true`. In that case, application displays input field for each zone and answer is compared with text from `labels` array according to corresponding `correctLabelIndex`.
1281
+
1282
+ ### 6.19. `type: "open-ended"` (Open-Ended Answer)
1283
+
1284
+ Intended for questions requiring free text answer that cannot be easily machine-compared (as with `short-answer`). Evaluation often requires teacher or advanced AI.
1285
+
1286
+ * `question` (string, required): **Rich Content.** Question text.
1287
+ * `minWords` (number, optional): Minimum required word count.
1288
+ * `maxWords` (number, optional): Maximum allowed word count.
1289
+ * `sampleAnswer` (string, optional): **Rich Content.** Sample (model) answer for evaluators.
1290
+ * `rubric` (object, optional): Structured evaluation criteria.
1291
+
1292
+ **Structure of `rubric` object:**
1293
+ * `criteria` (object[]): Array of criteria. Each criterion has:
1294
+ * `label` (string, **required**): Criterion name (e.g., "Grammar", "Argumentation").
1295
+ * `percentage` (number, **required**): Criterion weight in percentages (0-100). **Validation rules:** Each `percentage` MUST be >= 0 and <= 100. Sum of all `percentage` in `criteria` array SHOULD be 100. If sum ≠ 100: If sum > 0, application normalizes proportionally (e.g., [30, 30] → [50%, 50%]). If sum == 0, Application MUST declare error (cannot evaluate). If `criteria` array is empty, `rubric` is ignored.
1296
+ * `description` (string, optional): Description of what is evaluated in this criterion.
1297
+
1298
+ **Example:**
1299
+ ```json
1300
+ {
1301
+ "id": "019aa5fe-75da-7a8e-8c0b-f2297a6524a8",
1302
+ "type": "open-ended",
1303
+ "question": "Explain the impacts of the Industrial Revolution on society.",
1304
+ "minWords": 100,
1305
+ "maxWords": 500,
1306
+ "rubric": {
1307
+ "criteria": [
1308
+ {
1309
+ "label": "Factual Accuracy",
1310
+ "percentage": 50,
1311
+ "description": "Stating correct dates and events."
1312
+ },
1313
+ {
1314
+ "label": "Text Structure",
1315
+ "percentage": 30,
1316
+ "description": "Introduction, body, conclusion."
1317
+ },
1318
+ {
1319
+ "label": "Grammar and Style",
1320
+ "percentage": 20
1321
+ }
1322
+ ]
1323
+ }
1324
+ }
1325
+ ```
1326
+
1327
+ ### 6.20. `type: "numeric-input"` (Numeric Input)
1328
+
1329
+ Intended for physics, chemistry, statistics, or math problems where answer is specific number (integer or float), not algebraic expression (for that use `math-input`). This type ensures numeric keyboard display on mobile devices.
1330
+
1331
+ * `question` (string, required): **Rich Content.** Question text.
1332
+ * `value` (number, required): Correct numeric value (float). Used for evaluation when `range` is not specified. If `range` is present, `value` is not used for evaluation; it SHOULD nonetheless be set to a representative value within the range (e.g., the midpoint) to keep the field consistently populated.
1333
+ * `tolerance` (number, optional): Absolute allowed deviation. Default: `0`. Answer is correct if: `|user_value - value| <= tolerance`. Ignored when `range` is specified.
1334
+ * `range` (object, optional): Alternative to `value`+`tolerance`. Object `{ "min": number, "max": number }`. If specified, overrides `value`+`tolerance`. Answer is correct if it lies in closed interval `<min, max>`.
1335
+ * `unit` (string, optional): **Plain Text.** Unit displayed after input field (e.g., "kg", "m/s", "%"). Serves only for visual context, not compared.
1336
+
1337
+ **Implementation rules for applications:**
1338
+ Applications **MUST** normalize user input before evaluation to handle localization differences:
1339
+
1340
+ 1. Replace decimal comma `,` with period `.`.
1341
+ 2. Remove spaces (e.g., from `1 000` to `1000`).
1342
+ 3. Applications SHOULD accept input formatting per device locale, but internally always compare with `value` as type `number` (float).
1343
+
1344
+ **Example:**
1345
+
1346
+ ```json
1347
+ {
1348
+ "id": "019aa5fe-9a6c-79dc-8b0e-dae22f9247fe",
1349
+ "type": "numeric-input",
1350
+ "question": "What is the approximate value of gravitational acceleration on Earth?",
1351
+ "value": 9.81,
1352
+ "tolerance": 0.1,
1353
+ "unit": "m/s²"
1354
+ }
1355
+ ```
1356
+
1357
+ ### 6.21. `type: "pin-on-model"` (Pin on 3D Model)
1358
+
1359
+ * `question` (string, required): **Rich Content.** Instructions.
1360
+ * `targetAsset` (string, required): **Key from `assets`**, which determines the 3D model to be used.
1361
+ * `hotspots` (HotspotObject[], required): Array defining correct meshes/parts of the model.
1362
+ * `multipleCorrect` (boolean, optional): Determines whether user must find more than one target. Default: `false`.
1363
+ * `camera` (object, optional): Recommended initial camera setup.
1364
+ * `position` (object): `{x, y, z}` coordinates for the camera's location.
1365
+ * `target` (object): `{x, y, z}` coordinates of the point the camera is looking at and orbiting around.
1366
+ * **Note:** If `camera` is omitted, the application SHOULD automatically compute the bounding box of the model, set the target to its center, and position the camera to fit the entire model in the view.
1367
+ * **Validation rules:** Referenced asset in `targetAsset` **MUST** be of type `model` ([see section 5.](#5-the-mediaobject-object)).
1368
+
1369
+ **Example:**
1370
+ ```json
1371
+ {
1372
+ "id": "019aa5ff-1b3c-745a-93de-cfe33451a9fe",
1373
+ "type": "pin-on-model",
1374
+ "question": "Find and click on the **Femur** (thigh bone).",
1375
+ "targetAsset": "model_lower_limb",
1376
+ "hotspots": [
1377
+ { "type": "mesh", "targetName": "Femur" }
1378
+ ],
1379
+ "camera": {
1380
+ "position": { "x": 10, "y": 5, "z": 10 },
1381
+ "target": { "x": 0, "y": 1.5, "z": 0 }
1382
+ },
1383
+ "assets": {
1384
+ "model_lower_limb": {
1385
+ "type": "model",
1386
+ "value": "assets/lower-limb.glb",
1387
+ "mimeType": "model/gltf-binary"
1388
+ }
1389
+ }
1390
+ }
1391
+ ```
1392
+
1393
+ ### 6.22. `type: "chess-puzzle"` (Chess Puzzle)
1394
+
1395
+ * `question` (string, required): **Rich Content.** Instructions or puzzle description (e.g., "White to move and checkmate in 2").
1396
+ * `fen` (string, required): The board position in Forsyth–Edwards Notation (FEN).
1397
+ * `answers` (string[][], required): Array of correct move sequences. Each sequence is an array of strings in Standard Algebraic Notation (SAN) (e.g., `["e4", "e5", "Nf3"]`). Supports multiple valid solutions.
1398
+ * `elo` (number, optional): ELO rating of the puzzle difficulty.
1399
+ * **Note:** Applications SHOULD provide an interactive chessboard. The user solves the puzzle by making moves on the board.
1400
+
1401
+ **Example:**
1402
+ ```json
1403
+ {
1404
+ "id": "019aa600-abc1-7234-b678-c0ffee000001",
1405
+ "type": "chess-puzzle",
1406
+ "question": "Find the best move for White.",
1407
+ "fen": "r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4",
1408
+ "answers": [
1409
+ ["Ng5"]
1410
+ ],
1411
+ "elo": 1200
1412
+ }
1413
+ ```
1414
+
1415
+ -----
1416
+
1417
+ ## 7. Helper Data Structures
1418
+
1419
+ ### 7.1. `SelectBlankObject` (for `fill-in-select`)
1420
+
1421
+ | Key | Type | Required | Description |
1422
+ | :--- | :--- | :--- | :--- |
1423
+ | `options` | string[] | Yes | **Rich Content.** Array of text options to choose from. Must not be empty array. |
1424
+ | `correctIndex` | number | Yes | Index of correct answer in `options` array (0-based). |
1425
+
1426
+ ### 7.2. `HotspotObject` (for `pin-on-image` and `pin-on-model`)
1427
+
1428
+ Defines clickable zone on image or named mesh in a 3D model. For `pin-on-image`, all coordinates and dimensions are in **percentages** (0.0 to 100.0) relative to the image. For `pin-on-model`, the `mesh` type is used instead (no coordinates).
1429
+
1430
+ * `type` (string, required): Zone shape (`rect`, `circle`, `polygon`).
1431
+ * `label` (string, optional): **Plain Text.** Zone label.
1432
+
1433
+ **For `type: "rect"` (rectangle):**
1434
+
1435
+ * `x` (number): Percent X from left edge.
1436
+ * `y` (number): Percent Y from top edge.
1437
+ * `width` (number): Width in percentages.
1438
+ * `height` (number): Height in percentages.
1439
+
1440
+ **For `type: "circle"` (circle):**
1441
+
1442
+ * `x` (number): Center X in percentages.
1443
+ * `y` (number): Center Y in percentages.
1444
+ * `radius` (number): Radius in percentages.
1445
+
1446
+ **For `type: "polygon"` (polygon):**
1447
+
1448
+ * `points` (object[]): Array of points defining shape. Each point is `{ "x": number, "y": number }`.
1449
+
1450
+ **For `type: "mesh"` (3D model part):**
1451
+ * `targetName` (string): Name (or partial name) of the object/mesh in the 3D scene (glTF node name).
1452
+
1453
+ ### 7.3. `CategorizeItem` (for `categorize`)
1454
+
1455
+ | Key | Type | Required | Description |
1456
+ | :--- | :--- | :--- | :--- |
1457
+ | `id` | string | Recommended | **Plain Text.** Unique ID of item (within question, doesn't have to be UUIDv7). |
1458
+ | `text` | string | Yes | **Rich Content.** Item text. |
1459
+ | `correctCategoryIndex` | number | Yes | 0-based index into `categories` array in parent item. |
1460
+
1461
+ **Example:**
1462
+ ```json
1463
+ {
1464
+ "id": "019aa600-0cf8-7dc7-8493-925641accc35",
1465
+ "type": "categorize",
1466
+ "question": "Sort animals into categories:",
1467
+ "categories": ["Mammals", "Birds", "Fish"],
1468
+ "items": [
1469
+ {
1470
+ "id": "item1",
1471
+ "text": "Dolphin",
1472
+ "correctCategoryIndex": 0
1473
+ },
1474
+ {
1475
+ "id": "item2",
1476
+ "text": "Eagle",
1477
+ "correctCategoryIndex": 1
1478
+ }
1479
+ ]
1480
+ }
1481
+ ```
1482
+
1483
+ ### 7.4. `TimelineEvent` (for `timeline`)
1484
+
1485
+ | Key | Type | Required | Description |
1486
+ | :--- | :--- | :--- | :--- |
1487
+ | `id` | string | Recommended | **Plain Text.** Unique ID of event (within question, doesn't have to be UUIDv7). |
1488
+ | `text` | string | Yes | **Rich Content.** Event description. |
1489
+ | `date` | string | Yes | **Plain Text.** Event date in strict format per section 9.5. Field `date` MUST always contain full date in ISO 8601 format (e.g., `1969-01-01` or `1969-01-01T12:00:00Z`), even if `precision` is set to only `year` or `month`. Field `precision` determines only display format to user, but for internal sorting and machine processing, complete date is always required. MUST NOT be displayed to user before answering. Event without `date` is invalid and application MUST reject import. |
1490
+ | `precision` | string | No | **Plain Text.** Determines mask for displaying date to user. Possible values: `year`, `month`, `day`, `datetime`. If not specified, full precision defined in `date` field is used. This field is key for displaying "imprecise" historical dates (e.g., "year 1969" stored technically as `1969-01-01`).
1491
+
1492
+ **Rules for sorting events:** (1) If two events have same date base corresponding to lower precision (e.g., both have year `1969`, one with `precision: "year"` and other with `precision: "month"` or more precise), both orders are considered correct. (2) Events with same precision and same date are considered simultaneous (any order is correct). (3) For comparison purposes, least precise value from pair is used. Example: `{"date": "1969-01-01", "precision": "year"}` and `{"date": "1969-07-20", "precision": "day"}` are considered concurrent (both in year 1969). |
1493
+
1494
+ **Example:**
1495
+ ```json
1496
+ {
1497
+ "id": "019aa600-3e91-73cf-b990-c05ecea1a4b9",
1498
+ "type": "timeline",
1499
+ "question": "Sort events from space flight history:",
1500
+ "events": [
1501
+ {
1502
+ "id": "ev1",
1503
+ "text": "First human in space (Yuri Gagarin)",
1504
+ "date": "1961-04-12T00:00:00Z",
1505
+ "precision": "day"
1506
+ },
1507
+ {
1508
+ "id": "ev2",
1509
+ "text": "First Moon landing (Apollo 11)",
1510
+ "date": "1969-07-20T00:00:00Z",
1511
+ "precision": "day"
1512
+ }
1513
+ ]
1514
+ }
1515
+ ```
1516
+
1517
+ ### 7.5. `TranslationObject` (for `meta.translations`)
1518
+
1519
+ Defines reference to translated version of this set.
1520
+
1521
+ | Key | Type | Required | Description |
1522
+ | :--- | :--- | :--- | :--- |
1523
+ | `lang` | string | Yes | **Plain Text.** Language code (BCP 47) of translated set (e.g., "en-US"). |
1524
+ | `id` | string | Yes | Unique UUID (`meta.id`) of translated set. Each language version MUST have unique UUID. `meta.id` of translated set MUST NOT be same as original. |
1525
+ | `title` | string | Yes | **Plain Text.** Name of translated set (for quick display in menu). |
1526
+ | `downloadUrl` | string | No | **URI (absolute URL).** Link to download translated set if application doesn't have it available. |
1527
+
1528
+ **Best practice:**
1529
+ - Recommended to create "sibling" relationship between translations (each translation references other translations in `translations` array)
1530
+
1531
+ ### 7.6. `LinkedSetObject` (for `meta.prerequisites` and `meta.relatedSets`)
1532
+
1533
+ | Key | Type | Required | Description |
1534
+ | :--- | :--- | :--- | :--- |
1535
+ | `id` | string | Yes | UUID of OQSE set being referenced. |
1536
+ | `title` | string | Yes | **Plain Text.** Name of referenced set for UI display. |
1537
+ | `downloadUrl` | string | No | **URI (absolute URL).** Absolute address for downloading set if application doesn't have it available. |
1538
+
1539
+ ### 7.7. `SourceReference` (for `item.sources`)
1540
+
1541
+ | Key | Type | Required | Description |
1542
+ | :--- | :--- | :--- | :--- |
1543
+ | `id` | string | Yes | **Plain Text.** ID of source defined in `meta.sourceMaterials`. All `id` in `SourceReference` MUST exist in `meta.sourceMaterials`. Application MUST ignore non-existent references with warning. Application SHOULD NOT block item display due to missing source. |
1544
+ | `location` | string | No | **Plain Text.** Specification of place in source (e.g., "page 42", "time 12:30", "paragraph 3"). |
1545
+ | `quote` | string | No | **Rich Content.** Exact citation or text segment from source that directly supports correctness of answer. MAY contain formatting (bold text, italics) and mathematical formulas in LaTeX. This key is critical for automated fact-checking and AI systems like RAG (Retrieval Augmented Generation), so they can verify that generated question corresponds to source materials. |
1546
+
1547
+ ### 7.8. The `Pedagogy` Object (Adaptive Difficulty)
1548
+
1549
+ This optional object (`item.pedagogy`) serves to store advanced metadata about didactic properties of item. It is key for adaptive testing (CAT) and data sharing between systems.
1550
+
1551
+ | Key | Type | Description |
1552
+ | :--- | :--- | :--- |
1553
+ | `bloomLevel` | string | Level according to revised Bloom's taxonomy. Allowed values: `remember`, `understand`, `apply`, `analyze`, `evaluate`, `create`. Application MUST declare error for other value (including typos like `analyse`). |
1554
+ | `irtDifficulty` | number | IRT Parameter *b* (Difficulty). Determines what level of student ability is needed for 50% chance of correct answer. Typically range -3.0 to +3.0. If present and application supports IRT, takes precedence over simple `item.difficulty`. |
1555
+ | `irtDiscrimination` | number | IRT Parameter *a* (Discrimination). How well question distinguishes between strong and weak students. |
1556
+ | `irtGuessing` | number | IRT Parameter *c* (Pseudo-guessing). Probability of guessing answer by chance (e.g., 0.25 for ABCD test). |
1557
+ | `avgTime` | number | Average time (in seconds) students actually need to solve (measured from data). |
1558
+ | `cognitiveLoad` | string | Subjective cognitive load (`low`, `medium`, `high`). |
1559
+ | `partialCredit` | boolean | Whether item supports partial scoring (e.g., 2 out of 3 correct answers in `mcq-multi`). |
1560
+ | `penaltyPerWrong` | number | Penalty for each wrong choice (0.0 - 1.0). E.g., 0.5 means loss of half of total question points for wrong answer. |
1561
+
1562
+ **Example:**
1563
+ ```json
1564
+ "pedagogy": {
1565
+ "bloomLevel": "analyze",
1566
+ "irtDifficulty": 0.5,
1567
+ "irtDiscrimination": 0.8,
1568
+ "irtGuessing": 0.25,
1569
+ "avgTime": 45,
1570
+ "cognitiveLoad": "medium"
1571
+ }
1572
+ ```
1573
+
1574
+ -----
1575
+
1576
+ ## 8. Complete Example File
1577
+
1578
+ ```json
1579
+ {
1580
+ "$schema": "https://memizy.com/schemas/oqse/v0.1.json",
1581
+ "version": "0.1",
1582
+ "meta": {
1583
+ "id": "019aa606-cbb5-7b2e-9e61-ac335db1eb4b",
1584
+ "title": "Rocket Science Basics 🚀",
1585
+ "language": "en",
1586
+ "ageMin": 15,
1587
+ "subject": "physics",
1588
+ "createdAt": "2025-11-14T08:00:00Z",
1589
+ "updatedAt": "2025-11-17T10:00:00Z",
1590
+ "author": {
1591
+ "name": "John Smith",
1592
+ "role": "Content Author",
1593
+ "url": "https://github.com/johnsmith"
1594
+ },
1595
+ "contributors": [
1596
+ {
1597
+ "name": "Jane Doe",
1598
+ "role": "Editor"
1599
+ },
1600
+ {
1601
+ "name": "Dr. Robert Brown",
1602
+ "role": "Subject Matter Expert",
1603
+ "email": "robert.brown@example.edu"
1604
+ }
1605
+ ],
1606
+ "license": "CC-BY-SA-4.0",
1607
+ "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0/",
1608
+ "thumbnail": "cover_image",
1609
+ "assets": {
1610
+ "cover_image": {
1611
+ "type": "image",
1612
+ "value": "https://example.com/rocket-cover.jpg",
1613
+ "altText": "Starship rocket on launch pad",
1614
+ "license": "CC-BY-SA-4.0",
1615
+ "attribution": "SpaceX"
1616
+ }
1617
+ },
1618
+ "estimatedTime": 20,
1619
+ "tags": ["Physics", "Rocket Engine"],
1620
+ "requirements": {
1621
+ "features": ["markdown", "html-safe", "syntax-highlighting"],
1622
+ "itemProperties": ["hints", "explanation"]
1623
+ },
1624
+ "tagDefinitions": {
1625
+ "Physics": {
1626
+ "wikidataId": "Q413"
1627
+ },
1628
+ "Rocket Engine": {
1629
+ "wikidataId": "Q335225",
1630
+ "description": "Engines based on reaction principle."
1631
+ }
1632
+ },
1633
+ "sourceMaterials": [
1634
+ {
1635
+ "id": "srcWikiRaptor",
1636
+ "type": "url",
1637
+ "title": "Wikipedia: Raptor (rocket engine)",
1638
+ "description": "Main article on English Wikipedia.",
1639
+ "value": "https://en.wikipedia.org/wiki/SpaceX_Raptor",
1640
+ "retrievedAt": "2025-11-14T08:00:00Z"
1641
+ }
1642
+ ],
1643
+ "appSpecific": {
1644
+ "memizy": {
1645
+ "folderId": "019cb89c-fbc7-73e6-be8c-dbebb9e6d797",
1646
+ "themeColor": "#FF5733",
1647
+ "isFavorite": true
1648
+ }
1649
+ }
1650
+ },
1651
+ "items": [
1652
+ {
1653
+ "id": "019aa608-07c5-71c9-87b2-d03ab3364668",
1654
+ "type": "flashcard",
1655
+ "tags": ["Rocket Engine"],
1656
+ "topic": "History of Spaceflight",
1657
+ "difficulty": 2,
1658
+ "assets": {
1659
+ "raptor": {
1660
+ "type": "image",
1661
+ "value": "https://example.com/raptor-engine.jpg",
1662
+ "altText": "Raptor Engine",
1663
+ "license": "CC-BY-SA-4.0",
1664
+ "attribution": "SpaceX"
1665
+ },
1666
+ "ignition_sound": {
1667
+ "type": "audio",
1668
+ "value": "https://example.com/ignition.mp3",
1669
+ "transcript": "Stage zero propellant loading complete. Three, two, one, ignition.",
1670
+ "loop": false,
1671
+ "subtitles": [
1672
+ {"lang": "en", "value": "https://example.com/ignition_en.vtt"}
1673
+ ]
1674
+ }
1675
+ },
1676
+ "front": "What is the name of the main engine of **Starship** spacecraft?\n\n<asset:raptor />\n\nListen to launch sequence: <asset:ignition_sound />",
1677
+ "back": "**Raptor** - methane rocket engine developed by SpaceX.",
1678
+ "sources": [
1679
+ {
1680
+ "id": "srcWikiRaptor",
1681
+ "location": "Introduction paragraph",
1682
+ "quote": "Raptor is a full-flow staged combustion, methane-fueled rocket engine."
1683
+ }
1684
+ ],
1685
+ "pedagogy": {
1686
+ "irtDiscrimination": 0.62,
1687
+ "irtGuessing": 0.33,
1688
+ "avgTime": 15
1689
+ },
1690
+ "appSpecific": {
1691
+ "memizy": {
1692
+ "isPinnedInEditor": true,
1693
+ "lastCursorPosition": 42,
1694
+ "aiGenerated": true
1695
+ }
1696
+ }
1697
+ },
1698
+ {
1699
+ "id": "019aa609-4dfd-7f42-9513-f6901129682f",
1700
+ "type": "mcq-single",
1701
+ "tags": ["Rocket Engine", "History"],
1702
+ "topic": "History of Spaceflight",
1703
+ "difficulty": 4,
1704
+ "timeLimit": 30,
1705
+ "question": "Which engine was used on the 1st stage of the **Saturn V** rocket?",
1706
+ "options": [
1707
+ "Raptor",
1708
+ "Merlin",
1709
+ "F-1"
1710
+ ],
1711
+ "correctIndex": 2,
1712
+ "shuffleOptions": true,
1713
+ "optionExplanations": [
1714
+ "Raptor powers Starship spacecraft, not Saturn V.",
1715
+ "Merlin is an engine for Falcon 9 and Falcon Heavy rockets.",
1716
+ null
1717
+ ],
1718
+ "explanation": "Five **F-1** engines powered the first stage of Saturn V. It remains the most powerful single-chamber liquid-fueled rocket engine ever developed."
1719
+ },
1720
+ {
1721
+ "id": "019aa609-8a22-7814-93fc-0df947ad8e3b",
1722
+ "type": "match-complex",
1723
+ "tags": ["Rocket Engine"],
1724
+ "difficulty": 3,
1725
+ "question": "Match countries with their main spaceports (some items are extra):",
1726
+ "leftItems": ["USA", "France", "Kazakhstan"],
1727
+ "rightItems": ["Cape Canaveral", "Kourou", "Baikonur", "Spaceport Cornwall"],
1728
+ "connections": [
1729
+ [0, 0],
1730
+ [1, 1],
1731
+ [2, 2]
1732
+ ]
1733
+ },
1734
+ {
1735
+ "id": "019aa609-d8c6-7c3f-adcb-9b87c375726e",
1736
+ "type": "pin-on-image",
1737
+ "tags": ["Rocket Engine"],
1738
+ "difficulty": 3,
1739
+ "assets": {
1740
+ "starship": {
1741
+ "type": "image",
1742
+ "value": "https://example.com/starship-diagram.jpg",
1743
+ "altText": "Starship spacecraft diagram",
1744
+ "license": "CC-BY-SA-4.0"
1745
+ }
1746
+ },
1747
+ "question": "Click on the location where Raptor engines are mounted on Starship spacecraft.",
1748
+ "targetAsset": "starship",
1749
+ "hotspots": [
1750
+ {
1751
+ "type": "circle",
1752
+ "x": 50,
1753
+ "y": 90,
1754
+ "radius": 5,
1755
+ "label": "Raptor Engines"
1756
+ }
1757
+ ]
1758
+ },
1759
+ {
1760
+ "id": "019aa60a-f1d4-7281-b5e2-4c2847a192fc",
1761
+ "type": "pin-on-model",
1762
+ "question": "Find and click on the **Femur** (thigh bone).",
1763
+ "assets": {
1764
+ "model_skeleton": {
1765
+ "type": "model",
1766
+ "value": "assets/models/skeleton.glb",
1767
+ "altText": "Human Skeleton"
1768
+ }
1769
+ },
1770
+ "targetAsset": "model_skeleton",
1771
+ "hotspots": [
1772
+ { "type": "mesh", "targetName": "Femur" }
1773
+ ],
1774
+ "camera": {
1775
+ "position": { "x": 0, "y": 1.5, "z": 2 },
1776
+ "target": { "x": 0, "y": 1.0, "z": 0 }
1777
+ }
1778
+ }
1779
+ ]
1780
+ }
1781
+ ```
1782
+
1783
+ -----
1784
+
1785
+ ## 9. Validation Rules and Constraints
1786
+
1787
+ To ensure consistency and practical implementability, the specification defines the following rules:
1788
+
1789
+ ### 9.1. Length and Size Constraints
1790
+
1791
+ | Item | Maximum Value | Note |
1792
+ | :--- | :--- | :--- |
1793
+ | `meta.title` | 500 characters | For UI display |
1794
+ | `meta.description` | 5,000 characters | Short set description (including Markdown) |
1795
+ | `item.question` | 10,000 characters | Including Markdown formatting |
1796
+ | `item.explanation` | 10,000 characters | Including Markdown formatting |
1797
+ | `item.hints[]` (one hint) | 2,000 characters | Each hint separately |
1798
+ | `options[]` (one option) | 2,000 characters | For MCQ and similar types |
1799
+ | `items[]` (items array) | 10,000 items | Maximum count in one set |
1800
+ | `options[]` (options array) | 100 options | For MCQ types |
1801
+ | `prompts[]` / `matches[]` (match-pairs) | 100 items each | Both arrays must have equal length |
1802
+ | `hints[]` (hints array) | 20 hints | Per one item |
1803
+ | File in `assets/` (.oqse) | 25 MB | Host larger media externally (set fragmentation is not recommended and not supported, [see section 12.5](#125-performance)) |
1804
+ | `customData` / `appSpecific` | 10 levels depth | Prevention of stack overflows |
1805
+
1806
+ **Note:** Applications SHOULD support at least these limits. If specific environment doesn't allow same or higher values, it MUST be documented and user must be warned before import.
1807
+
1808
+ ### 9.2. Validation Rules for Indices and Arrays
1809
+
1810
+ - All indices (`correctIndex`, `correctIndices[]`) must be 0-based
1811
+ - Indices must not be negative
1812
+ - Indices must be within range of available options
1813
+ - For `correctIndices[]`: must not contain duplicates
1814
+
1815
+ **For `ageMin` and `ageMax`:**
1816
+ - `ageMin >= 0` (value 0 = preschool age, but recommended is >= 3)
1817
+ - `ageMax >= 0`
1818
+ - If both values are present, must hold: `ageMin <= ageMax`
1819
+ - If `ageMin == ageMax == 0`, considered "unspecified" (same as omission)
1820
+ - Recommended minimum range is 2 years (e.g., `ageMin: 6, ageMax: 8`)
1821
+
1822
+ **For `irtDifficulty` (in `pedagogy`):**
1823
+ - Typical range: -3.0 to +3.0
1824
+ - Application MUST accept any real number (no hard limit exists)
1825
+ - Values outside range [-3.0, +3.0] SHOULD trigger warning (extreme difficulty)
1826
+ - Values outside range [-5.0, +5.0] MAY be considered maximum or minimum value of allowed interval
1827
+
1828
+ **For `difficulty` (in section 4):**
1829
+ - Must be in range 1-5 (integer)
1830
+ - Values outside range are invalid
1831
+ - Used for simple categorization; for advanced psychometric models use `pedagogy.irtDifficulty`
1832
+
1833
+ **For `optionExplanations` (in `mcq-single` and `mcq-multi`):**
1834
+ - If present, length MUST be same as `options`
1835
+ - Empty array `[]` is equivalent to field omission
1836
+ - Missing explanation = `null` (not empty string `""`)
1837
+ - Example:
1838
+ ```json
1839
+ "options": ["A", "B", "C"]
1840
+ "optionExplanations": ["Correct!", null, "Wrong"] // Correct
1841
+ "optionExplanations": ["Correct!"] // Wrong (missing 2 elements)
1842
+ "optionExplanations": [] // Correct (no explanations)
1843
+ ```
1844
+
1845
+ **For `timeLimit`:**
1846
+ - Must be `> 0` or be omitted
1847
+ - Recommended: `timeLimit >= 5` (less than 5 seconds is unrealistic)
1848
+ - No max value (some tasks may take hours)
1849
+
1850
+ **For `minCorrect` (in `match-complex` and `pin-on-image`):**
1851
+ - Must hold: `0 < minCorrect <= number_of_correct_answers`
1852
+ - If `minCorrect` is missing, user must find all correct answers
1853
+
1854
+ **For `minSelections` and `maxSelections` (in `mcq-multi`):**
1855
+ - Must hold: `0 < minSelections <= maxSelections <= options.length`
1856
+ - If `minSelections` is missing, default value is 1
1857
+ - If `maxSelections` is missing, default value is `options.length`
1858
+
1859
+ **For `slider`:**
1860
+ - `correctAnswer` MUST lie in interval `<min, max>`.
1861
+ - Value MUST be reachable: `(correctAnswer - min) % step === 0`. Otherwise Application MUST reject import.
1862
+ - If `tolerance > 0`, it SHOULD also be a multiple of `step`, otherwise a warning about inconsistent evaluation is expected ([see section 6.12](#612-type-slider-slider--numeric-answer)).
1863
+
1864
+ **For `range` in `numeric-input`:**
1865
+ - If `range` is present, `range.min < range.max` MUST hold. Application MUST reject import if `range.min >= range.max`.
1866
+ - If `range` is present, `value` SHOULD lie within `<range.min, range.max>` (consistency recommendation).
1867
+
1868
+ **For `correctCells` in `matrix`:**
1869
+ - If `multiplePerRow: false`, each row may have maximum one correct cell
1870
+ - If `multiplePerRow: true`, rows can have multiple correct cells
1871
+ - Invalid when `multiplePerRow: false` and `correctCells` contains multiple cells from same row
1872
+
1873
+ ### 9.2a. Referential Integrity Validation
1874
+
1875
+ **References to other items:**
1876
+ - `relatedItems`, `dependencyItems`: Application MUST ignore non-existent IDs with warning
1877
+ - `sources[].id`: Application MUST ignore non-existent IDs with warning ([see section 7.7](#77-sourcereference-for-itemsources))
1878
+ - `meta.translations[].id`, `meta.prerequisites[].id`, `meta.relatedSets[].id`: External references, cannot validate locally
1879
+
1880
+ **References to assets:**
1881
+ - `<asset:key />` in Rich Content: Application MUST display placeholder and warning (recoverable error)
1882
+ - `thumbnail`, `targetAsset`: Application MUST declare error if asset doesn't exist (critical property)
1883
+
1884
+ **Field synchronization:**
1885
+ - `optionExplanations` vs `options`: Lengths MUST be same OR `optionExplanations` is empty/omitted
1886
+ - `blanks` tokens vs `text` tokens: Must match exactly ([see section 6.7](#67-type-fill-in-blanks-fill-in-the-blanks))
1887
+
1888
+ ### 9.3. UUID Validation Rules
1889
+
1890
+ The standard for OQSE v0.1 is **UUIDv7**. For applications simplification, it is recommended to support only **UUIDv7** (preferred) and **UUIDv4** (fallback).
1891
+
1892
+ **Rules for Applications:**
1893
+ 1. **Import:** Application MUST accept UUID versions 4 and 7 when loading existing sets. Application MAY accept other versions (1-6), but this is not required.
1894
+ 2. **Export of new items:** Application MUST generate new items with UUIDv7 to ensure chronological ordering and optimal indexing in databases.
1895
+ 3. **Re-export:** Existing UUID MUST be preserved unchanged regardless of version (due to referential integrity).
1896
+ 4. **Validation:** Application checks only syntactic UUID format (8-4-4-4-12 hex characters), not specific version.
1897
+ 5. **Uniqueness:** All UUIDs in file MUST be globally unique.
1898
+
1899
+ **Exception:** `SourceMaterial.id` MAY be alphanumeric string unique only within that file, and `id` within internal item objects such as `TimelineEvent` or `CategorizeItem` MAY also be alphanumeric string.
1900
+
1901
+ ### 9.4. URI and Path Security Validation Rules
1902
+
1903
+ - All fields marked as URI for remote resources (e.g., `licenseUrl`, `SourceMaterial.value` with types `url|pdf|video|audio|image`, `MediaObject.value` referencing external media) MUST use absolute URI including protocol. `https://` is recommended, `http://` is acceptable only with warning to user.
1904
+
1905
+ - **Security rules for URI schemes:**
1906
+ * Application MUST reject URI schemes: `javascript:`, `data:`, `vbscript:`, `file:`
1907
+ * Allowed schemes: `https:`, `http:` (with warning)
1908
+
1909
+ - **Local Media Path Validation (Local Assets):**
1910
+ If string in `MediaObject.value` doesn't start with specified network protocol (e.g., `https://`, `http://`), application MUST consider it relative path to file inside `.oqse` package. For these paths, strict security rules (Sanitization) apply:
1911
+
1912
+ 1. **No absolute paths:** Path MUST NOT start with slash `/`, backslash `\`, or drive letter identifier (e.g., `C:`, `D:`). Application MUST immediately reject such values as invalid to prevent access to host filesystem (e.g., `/etc/passwd`, `C:\Windows\win.ini`).
1913
+ 2. **No Path Traversal:** Path MUST NOT contain sequences for moving to parent directory (`..`, `../`, `..\`).
1914
+ 3. **Whitelist of allowed characters:** Relative path MUST contain only alphanumeric characters (`a-z`, `A-Z`, `0-9`), underscores (`_`), hyphens (`-`), periods (`.`), and forward slashes (`/`) as directory separators. Any other character (including spaces) leads to path rejection or strict URL encoding.
1915
+ 4. **File extensions:** Files in `assets/` folder **SHOULD** have extension matching their content (e.g., `.jpg`, `.mp3`). If JSON contains reference `"value": "assets/image_no_extension"`, browsers and applications may have trouble determining `Content-Type` and properly rendering file.
1916
+ 5. **Case Sensitivity:** Application MUST perform case-insensitive path comparison (due to differences between Windows/Linux/macOS). On export, editor MUST convert all filenames to lowercase for consistency.
1917
+
1918
+ - **Valid examples:**
1919
+ * `assets/img/diagram_01.png` (Valid)
1920
+ * `/User/data/img.png` (Invalid - absolute path)
1921
+ * `../../system/config` (Invalid - path traversal)
1922
+ * `assets/image (1).jpg` (Invalid - contains space and parentheses, must rename or encode)
1923
+
1924
+ - **Media Tag:** Tag `<asset:key />` is internal reference and MUST NOT be considered URL. Application always evaluates key in order `item.assets` → `meta.assets`.
1925
+
1926
+ **Asset key rules:**
1927
+ - Keys in `assets` (JSON) MUST consist only of lowercase letters, digits, underscores, and hyphens (regex: `^[a-z0-9_-]+$`).
1928
+ - Keys MUST NOT contain spaces or forbidden special characters. The Application MUST reject keys containing such invalid characters.
1929
+ - If a key contains uppercase letters (`A-Z`), the Application MUST NOT immediately reject it during import or render. Instead, it MUST automatically normalize the key to lowercase and handle any resulting collisions as defined in the global Case Sensitivity rules ([see section 1.](#1-global-rules-and-root-structure)).
1930
+
1931
+ ### 9.5. Date and Time Validation Rules
1932
+ To ensure maximum application compatibility and correct chronological ordering, specification **restricts** allowed formats to subset of ISO 8601 norm corresponding to **RFC 3339** (Internet Date/Time Format).
1933
+
1934
+ * **Required format:** Must use so-called **Extended Format** (with separators `-` and `:`).
1935
+ * Date: `YYYY-MM-DD` (e.g., `2025-11-21`)
1936
+ * Date and time: `YYYY-MM-DDTHH:MM:SS` plus time zone (e.g., `2025-11-21T14:30:00Z` or `2025-11-21T15:30:00+01:00`).
1937
+ * **Forbidden formats:**
1938
+ * Basic format without separators (e.g., `20251121` is **INVALID**).
1939
+ * Week dates (e.g., `2025-W47` is **INVALID**).
1940
+ * Ordinal dates (e.g., `2025-325` is **INVALID**).
1941
+ * Slashes as separators (e.g., `2025/11/21` is **INVALID**).
1942
+ * **Time zones:**
1943
+ * For machine times (`createdAt`, `updatedAt`), it is **RECOMMENDED** to use exclusively UTC marked with suffix `Z`.
1944
+ * Applications **MUST** also support numeric offsets (e.g., `+01:00`).
1945
+ * **Historical dates (negative years):**
1946
+ * For years before Common Era (BC), use minus sign and year padded to 4 digits (e.g., Battle of Thermopylae: `-0480-08-01`).
1947
+ * Year 0 in this format corresponds to 1 BC (astronomical numbering). Application SHOULD account for this when displaying to user.
1948
+
1949
+ ### 9.6. Security Rules for OQSE Container (ZIP)
1950
+
1951
+ Applications processing `.oqse` packages (ZIP) MUST implement following protections against filesystem and availability attacks:
1952
+
1953
+ **Protection against "Zip Slip":** Application MUST verify canonical path of each file during unpacking. Target path of unpacked file MUST start with root directory designated for extraction. If file contains sequences `../` or absolute paths that would lead to writing outside this directory, application MUST immediately abort extraction and declare error.
1954
+
1955
+ **Ban on Symbolic Links (Symlinks):** Application MUST NOT follow or create symbolic links (symlinks) or hard links contained in archive. If archive contains such items, application MUST ignore them or declare error.
1956
+
1957
+ **"ZIP Bomb" Prevention:** Application MUST check compression ratio and total size of unpacked data.
1958
+ * **Recommended:** Abort extraction if total size of unpacked data exceeds 10x archive size or fixed limit (e.g., 250 MB, considering 25 MB limit for assets).
1959
+ * Application MUST reject files with extremely high compression ratio that could cause memory exhaustion (DoS).
1960
+
1961
+ **File count limit:** Application SHOULD limit maximum number of files extracted from one archive (e.g., to 10,000) to prevent flooding inode table of filesystem.
1962
+
1963
+ ### 9.7. Prohibited Values and Duplication
1964
+
1965
+ **Empty/whitespace strings:**
1966
+ - `meta.title`, `meta.language`, `item.type` MUST NOT be empty or contain only whitespace
1967
+ - Application MUST declare error when detecting empty required fields
1968
+
1969
+ **Duplicate values:**
1970
+ - Field `options` in MCQ types MUST NOT contain duplicate strings (case-insensitive comparison)
1971
+ - Field `categories` in `categorize` MUST NOT contain duplicate names
1972
+ - All `id` values of top-level `items` MUST be unique within the set. `id` values in `TimelineEvent` and `CategorizeItem` MUST be unique within their parent item (not globally).
1973
+
1974
+ **Invalid references:**
1975
+ - Plain Text fields MUST NOT contain Media Tags `<asset:...>` (only in Rich Content)
1976
+ - Rich Content fields MAY contain Media Tags; HTML tags are sanitized
1977
+
1978
+ ### 9.8. Shuffling Rules
1979
+
1980
+ - Shuffling is performed on each new attempt, not per-session
1981
+ - Original order from JSON is "correct order" and MUST be preserved in application logic
1982
+ - For `mcq-single/multi`: If `shuffleOptions: false`, options display in order from `options` array
1983
+ - For `sort-items`: Items are always shuffled (otherwise task would be meaningless)
1984
+ - For `timeline`: Respect `randomize` flag (default: `true`)
1985
+ - For `match-pairs`: Application MUST shuffle both sides before display
1986
+
1987
+ ### 9.9. Rules for `lang` Override
1988
+
1989
+ - `item.lang` overrides `meta.language` only for this item
1990
+ - Application SHOULD use correct font and text direction (RTL for `ar`, `he`)
1991
+ - In fulltext search, application SHOULD use correct tokenizer for language
1992
+ - `item.lang` DOES NOT affect application UI language, only item content
1993
+
1994
+ ### 9.10. Clarification of `customData` vs `appSpecific`
1995
+
1996
+ **`customData`:**
1997
+ - For metadata from **set creator** (teacher, author)
1998
+ - Portable between applications (e.g., "school class", "topic of week")
1999
+ - Example: `{"schoolGrade": "9A", "topicId": "physics-101"}`
2000
+
2001
+ **`appSpecific`:**
2002
+ - For metadata from **application** (editor, player)
2003
+ - Not portable, other applications MUST ignore
2004
+ - Top-level keys MUST be the application identifier (namespacing requirement)
2005
+ - Example: `{"memizy": {"editorState": {...}, "lastPosition": 42}}`
2006
+
2007
+ **Constraints:**
2008
+ - Maximum depth: 10 nesting levels (stack overflow prevention)
2009
+ - Recommended maximum size: 50 KB serialized JSON
2010
+ - MUST NOT contain sensitive data (passwords, tokens)
2011
+
2012
+ ### 9.11. Key Order for Optimization (Streaming)
2013
+
2014
+ Although JSON standard doesn't require key order, for memory efficiency optimization when parsing large sets (High Performance Parsing), OQSE file generators MUST write root object in this order:
2015
+
2016
+ 1. **Identifiers** (`$schema`, `version`)
2017
+ 2. **Metadata** (`meta`)
2018
+ 3. **Data** (`items`)
2019
+
2020
+ This order ensures that:
2021
+ - Application can validate version and schema before loading data
2022
+ - Metadata are available for UI (progress bar, set name) before parsing items
2023
+ - Large `items` array is loaded last, enabling streaming parsing
2024
+ - Format is "future-proof" for pagination implementation without breaking changes
2025
+
2026
+ **Example of correct order:**
2027
+ ```json
2028
+ {
2029
+ "$schema": "https://memizy.com/schemas/oqse/v0.1.json",
2030
+ "version": "0.1",
2031
+ "meta": { ... },
2032
+ "items": [ ... ]
2033
+ }
2034
+ ```
2035
+
2036
+ ### 9.12. Manifest Validation Rules
2037
+
2038
+ This section is the single reference for all constraints on Application Manifest fields. For the normative definition of each field and its behavioral semantics, see [§2.1 – The Application Manifest](#21-the-application-manifest). A host environment MUST reject a manifest that violates any MUST rule below and MUST report an appropriate structured error to the user.
2039
+
2040
+ #### 9.12.1. Required Fields
2041
+
2042
+ - `version`, `id`, `appName`, and `capabilities` MUST be present. A manifest missing any of these MUST be rejected.
2043
+ - `capabilities.actions` MUST be present and non-empty. A manifest with no declared actions is inoperable.
2044
+
2045
+ #### 9.12.2. Root Field Constraints
2046
+
2047
+ **`version`, `minOqseVersion`, `maxOqseVersion`:**
2048
+ - MUST match the pattern `^\d+\.\d+$` (e.g., `"1.0"`, `"1.99"`, `"2.0"`). Both components MUST be non-negative integers.
2049
+ - Version comparison is performed numerically field-by-field: `"1.10"` is greater than `"1.9"`.
2050
+ - A malformed `version` value MUST cause manifest rejection (it is a required field). A malformed `minOqseVersion` or `maxOqseVersion` SHOULD be treated as absent; the host SHOULD warn.
2051
+ - If both `minOqseVersion` and `maxOqseVersion` are present, `minOqseVersion` MUST be less than or equal to `maxOqseVersion`. A manifest violating this MUST be rejected.
2052
+
2053
+ **`pluginVersion`:**
2054
+ - SHOULD follow SemVer format `MAJOR.MINOR.PATCH` (e.g., `"2.1.0"`).
2055
+ - Applications MUST NOT reject a manifest solely because `pluginVersion` does not follow SemVer.
2056
+
2057
+ **`id`:**
2058
+ - MUST be either a controlled absolute URL (e.g., `"https://memizy.com/player"`) or a URN-format UUID (e.g., `"urn:uuid:019aa600-abc1-7234-b678-c0ffee000001"`).
2059
+ - A value that is neither a valid absolute URL nor a valid URN UUID SHOULD trigger a warning.
2060
+
2061
+ **`appName`:**
2062
+ - MUST NOT be empty or contain only whitespace.
2063
+
2064
+ **`studyMode`:**
2065
+ - MUST be one of: `"game"`, `"fun"`, `"drill"`.
2066
+ - An unrecognized value SHOULD be treated as absent; the host SHOULD warn.
2067
+
2068
+ **`questionDensity`:**
2069
+ - MUST be one of: `"low"`, `"medium"`, `"high"`.
2070
+ - An unrecognized value SHOULD be treated as absent; the host SHOULD warn.
2071
+ - If `studyMode` is `"drill"` and `questionDensity` is anything other than `"high"`, hosts SHOULD emit an inconsistency warning (the host treats it as `"high"` per [§2.1.1](#211-manifest-root-object)).
2072
+
2073
+ **`emoji`:**
2074
+ - SHOULD contain a single visually-rendered emoji glyph.
2075
+ - An empty string or a value containing no recognizable emoji SHOULD trigger a warning.
2076
+ - Hosts MUST NOT reject a manifest solely due to a multi-codepoint emoji sequence (flags, ZWJ family sequences, etc.).
2077
+
2078
+ **`locales`:**
2079
+ - Each entry MUST be a valid BCP 47 language tag (e.g., `"en"`, `"cs"`, `"zh-Hans"`).
2080
+ - `[]` MUST be treated identically to key omission; the host SHOULD assume English in both cases.
2081
+
2082
+ #### 9.12.3. Capabilities Validation
2083
+
2084
+ For the full semantics of the capabilities object, see [§2.1.2 – Capabilities Object](#212-capabilities-object-extends-featureprofile) and [Wildcard and Null Semantics](#wildcard-and-null-semantics).
2085
+
2086
+ **`capabilities.actions`:**
2087
+ - MUST contain at least one value from the [Action Registry (§2.1.3)](#213-action-registry).
2088
+ - Values that are not in the Action Registry and do not carry an `x-` prefix SHOULD trigger a warning; a host MAY reject the manifest.
2089
+ - If `render` or `edit` is declared in `actions`, then `types` MUST also be declared and non-null (see below).
2090
+
2091
+ **`capabilities.types`:**
2092
+ - If `render` or `edit` is in `actions`, at least one type or `["*"]` MUST be declared; a null or empty result makes the manifest inoperable for its declared purpose and MUST be reported as an error.
2093
+ - `[]` MUST be treated identically to `null` per [Wildcard and Null Semantics](#wildcard-and-null-semantics).
2094
+ - `["*"]` is valid and declares support for all item types.
2095
+ - Custom type values not among the official item types defined in [§6](#6-item-types-itemtype) MUST use the `x-` prefix per [§11.1](#111-adding-custom-item-types).
2096
+
2097
+ **`capabilities.assets`:**
2098
+ - Each asset category key (`image`, `audio`, `video`, `model`) follows the [Wildcard and Null Semantics](#wildcard-and-null-semantics): `["*"]`, an explicit MIME type list, `null`, or `[]` (equivalent to `null`).
2099
+ - MIME type strings SHOULD conform to IANA media type format (e.g., `"image/webp"`, `"audio/mpeg"`).
2100
+ - An unrecognized MIME type SHOULD trigger a warning but MUST NOT cause manifest rejection.
2101
+
2102
+ **`capabilities.features`:**
2103
+ - Each value MUST either appear in the [Official Feature Registry (§2.2)](#official-feature-registry) or carry the `x-` prefix per [Extension Rules (§2.2)](#extension-rules).
2104
+ - A value without an `x-` prefix that is not in the registry is invalid; the host SHOULD warn and MAY reject the manifest.
2105
+ - The features `syntax-highlighting`, `mermaid`, `smiles`, and `abc-notation` are only meaningful when `markdown` is also declared (they operate on GFM fenced code blocks). Declaring any of these without `markdown` SHOULD trigger a warning.
2106
+ - `html-safe` is only meaningful when `markdown` is also declared, since raw HTML in Rich Content fields is only parsed and rendered as part of the GFM pipeline. Declaring `html-safe` without `markdown` SHOULD trigger a warning.
2107
+
2108
+ **`capabilities.latexPackages`:**
2109
+ - Only meaningful when `"math"` is present in `capabilities.features`. Declaring `latexPackages` without `"math"` SHOULD trigger a warning.
2110
+
2111
+ **`capabilities.itemProperties` / `capabilities.metaProperties`:**
2112
+ - Each value SHOULD appear in the [Registry of Properties (§2.2)](#registry-of-properties-itemproperties--metaproperties) or carry the `x-` prefix.
2113
+ - Unrecognized values without an `x-` prefix SHOULD trigger a warning but MUST NOT cause manifest rejection.
2114
+
2115
+ -----
2116
+
2117
+ ## 10. Error Handling (Error Handling Policy)
2118
+
2119
+ Application implementation MUST follow **"Best Effort"** strategy.
2120
+
2121
+ ### 10.1. Atomic Validation
2122
+
2123
+ The validation unit is one item in the `items` array. A validation error within one item (e.g., missing required field, unknown type, invalid index value) is considered a local error.
2124
+
2125
+ ### 10.2. Error Recovery
2126
+
2127
+ When local error occurs, Application:
2128
+
2129
+ 1. **MUST** ignore that item (not load into memory).
2130
+ 2. **MUST NOT** interrupt parsing of rest of file (if JSON is syntactically valid).
2131
+ 3. **MUST** record this event in internal error log.
2132
+
2133
+ ### 10.3. Reporting
2134
+
2135
+ Application performing import **SHOULD** inform user about operation result, especially if some items were skipped (e.g., "Imported 48 of 50 items, 2 skipped due to errors").
2136
+
2137
+ ### 10.4. Error Handling and Fallback Strategies
2138
+
2139
+ To ensure consistent behavior across implementations, specification defines following error handling rules:
2140
+
2141
+ ### 10.4.1. Critical Errors (Application MUST abort loading)
2142
+
2143
+ 1. **Invalid JSON syntax:** Immediate failure with error message about error position.
2144
+ 2. **Missing required field in `meta`:** Error with list of all missing fields (`id`, `title`, `language`, `createdAt`, `updatedAt`).
2145
+ 3. **Invalid `version` value:** If version is not supported or missing.
2146
+
2147
+ ### 10.4.2. Recoverable Errors (Application continues with warning)
2148
+
2149
+ 1. **Unknown `item.type`:**
2150
+ - Application MUST skip item
2151
+ - Record warning with `item.id` and unknown type
2152
+ - Continue processing other items
2153
+
2154
+ 2. **Missing asset reference:**
2155
+ - When `<asset:key />` references non-existent media
2156
+ - Application MUST display placeholder or text replacement
2157
+ - Record warning with path to missing asset
2158
+
2159
+ 3. **Invalid UUID format:**
2160
+ - Application MUST reject item (cannot guarantee uniqueness)
2161
+ - Application MAY offer automatic UUID regeneration with UUIDv7
2162
+
2163
+ 4. **Non-existent ID in `relatedItems` or `dependencyItems`:**
2164
+ - Application MUST ignore invalid reference
2165
+ - Record warning with item ID and non-existent reference
2166
+
2167
+ 5. **Exceeding limits (section 9.1):**
2168
+ - **Soft limit:** Warning on import, data preserved
2169
+ - **Hard limit:** Offer trimming or rejection
2170
+
2171
+ 6. **Empty `items` array:**
2172
+ - The set may be a template or work-in-progress
2173
+ - Application MUST load the set successfully and display an appropriate empty-state message
2174
+ - Record warning (e.g., "Set loaded with 0 items")
2175
+
2176
+ ### 10.4.3. Fallback Strategies
2177
+
2178
+ **For unknown fields:**
2179
+ - Application MUST ignore unknown keys (forward compatibility)
2180
+ - On re-export, unknown data MUST be preserved unchanged
2181
+
2182
+ **For incomplete data:**
2183
+ - Missing optional fields: use default values per specification
2184
+ - Missing `altText` for image: generate from filename or use placeholder
2185
+
2186
+ **For versions:**
2187
+ - Higher MAJOR version: display warning and offer to continue in "best effort" mode
2188
+ - Higher MINOR version: safely ignore new keys
2189
+
2190
+ ### 10.4.4. Structured Error Log
2191
+
2192
+ Application SHOULD provide structured log of errors and warnings:
2193
+
2194
+ ```json
2195
+ {
2196
+ "errors": [
2197
+ {
2198
+ "severity": "error",
2199
+ "code": "MISSING_REQUIRED_FIELD",
2200
+ "message": "Missing required field 'meta.id'",
2201
+ "path": "meta.id"
2202
+ }
2203
+ ],
2204
+ "warnings": [
2205
+ {
2206
+ "severity": "warning",
2207
+ "code": "UNKNOWN_ITEM_TYPE",
2208
+ "message": "Unknown item type 'x-custom-quiz'",
2209
+ "path": "items[5].type",
2210
+ "itemId": "019aa7f8-bdf4-7093-b7e1-37730c3e48fb"
2211
+ }
2212
+ ]
2213
+ }
2214
+ ```
2215
+
2216
+ -----
2217
+
2218
+ ## 11. Extensibility and Versioning
2219
+
2220
+ ### 11.1. Adding Custom Item Types
2221
+
2222
+ Applications may define custom item types using `x-` prefix (e.g., `x-code-challenge`, `x-pronunciation`). These types:
2223
+ * MUST preserve all common properties from section 4
2224
+ * SHOULD be documented in `appSpecific` object in `meta`
2225
+ * MUST NOT collide with future official types
2226
+
2227
+ ### 11.2. Versioning and Compatibility
2228
+
2229
+ **Semantic Versioning:** OQSE uses format `MAJOR.MINOR` (e.g., `1.0`, `2.0`, `2.1`).
2230
+
2231
+ * **MAJOR change:** Fundamental breaking changes (e.g., change of required fields, type removal).
2232
+ * **MINOR change:** Addition of new optional fields or types (backward compatible).
2233
+
2234
+ **Rules for applications:**
2235
+ * Application MUST ignore unknown keys (forward compatibility)
2236
+ * Application MUST check the `version` field and apply version-specific handling; the value MUST conform to `MAJOR.MINOR` format (see [§9.12](#912-manifest-validation-rules) for format rules applied equally to OQSE file version and all manifest version fields)
2237
+ * When importing higher MAJOR version: display warning to user
2238
+ * When importing higher MINOR version: safely ignore new keys
2239
+
2240
+ -----
2241
+
2242
+ ## 12. Best Practices for Set Creators
2243
+
2244
+ ### 12.1. Accessibility
2245
+ * Always fill `altText` for images in `assets`
2246
+ * For each audio/video asset, provide `transcript` or subtitles in at least one language, so content remains accessible to users with hearing impairments
2247
+ * Use clear and unambiguous language
2248
+ * Test with screen readers
2249
+
2250
+ ### 12.2. Optimization for AI Generation
2251
+ * **Prefer simple structures**: AI models work better with flat arrays (`string[]`, `number[]`) than deep nesting. Conversion to JSON is then deterministic.
2252
+ * **Centralize media**: Define all media in `assets` (at item or `meta` level). Content should contain only text and Media Tags, never direct URLs.
2253
+ * **Use Media Tags**: Reference media exclusively using `<asset:key />`. AI prompt can explicitly request this format: "Add image as `<asset:diagram1 />`".
2254
+ * **Reusability without duplication**: Place shared resources in `meta.assets` and reference from multiple items using same key. AI thus doesn't have to repeatedly generate identical `MediaObject`.
2255
+ * **Prompt minimally**: "Create 5 questions with options A, B, C, D" → AI returns `options: ["A...", "B...", "C...", "D..."]`. Fewer descriptive words, more consistent structures.
2256
+ * **Prepare asset dictionary**: If AI generates references to `<asset:img1 />`, pre-fill `assets` and let AI just add descriptions (`altText`, `caption`).
2257
+
2258
+ ### 12.3. Localization
2259
+ * Main set in one language (`meta.language`)
2260
+ * For translations create separate OQSE files with different `id` and reference in `meta.translations` (TranslationObject)
2261
+ * Use Unicode normalization (NFC) for consistency
2262
+
2263
+ ### 12.4. Security
2264
+ * **HTML Sanitization:** GitHub Flavored Markdown (GFM) allows HTML, which is important for formatting that pure Markdown cannot handle (e.g., text colors, highlighting, or typographic subscripts outside math mode). GFM is superset of CommonMark. Specification: [https://github.github.com/gfm/](https://github.github.com/gfm/)
2265
+ * Application **MUST** perform strict sanitization of resulting HTML (e.g., using library like DOMPurify).
2266
+ * **ALLOWED** tags: Safe formatting tags (e.g., `b`, `i`, `strong`, `em`, `sub`, `sup`, `span`, `br`, `ruby`, `rt`, `rp`).
2267
+ * **FORBIDDEN** tags: All active or dangerous elements (e.g., `script`, `iframe`, `object`, `embed`, `form`, `style` and event handlers like `onclick`).
2268
+ * **Asset Sanitization:** The only HTML that gets into DOM is result of replacing `<asset:... />` tags. Application MUST ensure that attributes of generated elements (e.g., `src`, `alt`) are safe and don't contain injected code.
2269
+ * **NEVER** insert sensitive information (passwords, API keys)
2270
+ * When using external URLs, validate HTTPS
2271
+ * Validate media checksums before use ([see section 5](#5-the-mediaobject-object))
2272
+
2273
+ ### 12.5. Performance
2274
+ * Host very large media externally (e.g., CDN) to keep `.oqse` packages small
2275
+ * For video longer than 1 minute, use external hosting (YouTube, Vimeo)
2276
+ * Compress JSON before distribution (gzip)
2277
+
2278
+ ### 12.6. Metadata for Search
2279
+ * Always fill `description`, `tags`, and `subject`
2280
+ * Use `tagDefinitions` with Wikidata ID for better discoverability
2281
+ * Keep `sourceMaterials` current
2282
+
2283
+ -----
2284
+
2285
+ ## 13. Validation and Tools
2286
+
2287
+ ### 13.1. JSON Schema
2288
+ Official JSON Schema is available at:
2289
+
2290
+ [https://memizy.com/schemas/oqse/v0.1.json](https://memizy.com/schemas/oqse/v0.1.json)
2291
+
2292
+ ### 13.2. Recommended Tools
2293
+ * **Validators:** Ajv, JSON Schema Validator
2294
+ * **Editing:** VS Code with JSON Schema support
2295
+ * **Conversion:** Planned CLI tools for import from Quizlet, Anki, etc.
2296
+
2297
+ ### 13.3. Test Suite
2298
+ Reference test files including all item types are available at:
2299
+
2300
+ [https://github.com/memizy/oqse-test-suite](https://github.com/memizy/oqse-test-suite)
2301
+
2302
+ ### 13.4. Common Pitfalls
2303
+
2304
+ #### **Error: Embedding HTML/Markdown media instead of Media Tag**
2305
+ ```json
2306
+ "question": "Look at the image: <img src=\"https://example.com/img.png\" alt=\"Engine\">"
2307
+ ```
2308
+ **Correct:**
2309
+ ```json
2310
+ "assets": {
2311
+ "img_engine": {
2312
+ "type": "image",
2313
+ "value": "https://example.com/img.png",
2314
+ "altText": "Raptor Engine"
2315
+ }
2316
+ },
2317
+ "question": "Look at the image: <asset:img_engine />"
2318
+ ```
2319
+
2320
+ #### **Error: Reference to undefined `asset` key**
2321
+ ```json
2322
+ "question": "Where is Prague? <asset:europe_map />"
2323
+ ```
2324
+ No `europe_map` is defined in `item.assets` nor `meta.assets`.
2325
+
2326
+ **Correct:**
2327
+ ```json
2328
+ "assets": {
2329
+ "europe_map": {
2330
+ "type": "image",
2331
+ "value": "https://example.com/map.png",
2332
+ "altText": "Political map of Europe"
2333
+ }
2334
+ },
2335
+ "question": "Where is Prague? <asset:europe_map />"
2336
+ ```
2337
+
2338
+ #### **Error: Missing `altText` for images**
2339
+ ```json
2340
+ "assets": {
2341
+ "img1": {
2342
+ "type": "image",
2343
+ "value": "https://example.com/image.jpg"
2344
+ }
2345
+ }
2346
+ ```
2347
+ **Correct:**
2348
+ ```json
2349
+ "assets": {
2350
+ "img1": {
2351
+ "type": "image",
2352
+ "value": "https://example.com/image.jpg",
2353
+ "altText": "Image description for screen readers"
2354
+ }
2355
+ }
2356
+ ```
2357
+
2358
+ #### **Error: Indices out of range**
2359
+ ```json
2360
+ {
2361
+ "type": "mcq-single",
2362
+ "options": ["A", "B", "C"],
2363
+ "correctIndex": 3
2364
+ }
2365
+ ```
2366
+ **Correct:** `correctIndex: 2` (0-based, max index is 2)
2367
+
2368
+ #### **Error: Incorrect `correctCells` format (using object instead of coordinates array)**
2369
+ ```json
2370
+ "correctCells": {
2371
+ "row_0_col_1": true
2372
+ }
2373
+ ```
2374
+ **Correct:** (Array of arrays `[row, column]`)
2375
+ ```json
2376
+ "correctCells": [
2377
+ [0, 1]
2378
+ ]
2379
+ ```
2380
+
2381
+ #### **Error: Using GPS coordinates instead of percentages in hotspots**
2382
+ ```json
2383
+ {
2384
+ "type": "circle",
2385
+ "x": 50.0875,
2386
+ "y": 14.4213,
2387
+ "radius": 2
2388
+ }
2389
+ ```
2390
+ **Correct:** Coordinates must be in percentages (0.0-100.0):
2391
+ ```json
2392
+ {
2393
+ "type": "circle",
2394
+ "x": 50,
2395
+ "y": 50,
2396
+ "radius": 5
2397
+ }
2398
+ ```
2399
+
2400
+ #### **Error: Invalid UUID**
2401
+ ```json
2402
+ "id": "abc-123"
2403
+ ```
2404
+ **Correct:** (UUIDv7 example)
2405
+ ```json
2406
+ "id": "019aa7f8-bdf4-7093-b7e1-37730c3e48fb"
2407
+ ```
2408
+
2409
+ #### **Error: Duplicate indices in `correctIndices`**
2410
+ ```json
2411
+ "correctIndices": [0, 2, 2, 4]
2412
+ ```
2413
+ **Correct:**
2414
+ ```json
2415
+ "correctIndices": [0, 2, 4]
2416
+ ```
2417
+
2418
+ -----
2419
+
2420
+ ## 14. Contributing and Community
2421
+
2422
+ OQSE is an open standard. Suggestions for improvements:
2423
+ * GitHub: [https://github.com/memizy/oqse-spec](https://github.com/memizy/oqse-spec)
2424
+ * Discussions: GitHub Discussions
2425
+ * Email: `oqse@memizy.com`
2426
+
2427
+ **Change acceptance process:**
2428
+ 1. Open issue with proposal
2429
+ 2. Community discussion (min. 2 weeks)
2430
+ 3. Pull request with specification update
2431
+ 4. Review by maintainers
2432
+ 5. Merge and version update
2433
+
2434
+ -----
2435
+
2436
+ **Document Version:** 0.1
2437
+ **Last Updated:** November 20, 2025
2438
+ **Documentation License:** CC-BY-SA-4.0
2439
+
2440
+ -----
2441
+
2442
+ ### Version 0.1 (November 20, 2025)
2443
+ #### First version of this specification created
2444
+